Merge branch 'full-cache' into next

This commit is contained in:
hzrd149 2024-01-12 21:08:36 +00:00
commit 57bb9bb1dd
100 changed files with 624 additions and 435 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Improve display of unknown events

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Upgrade nostr-tools to v2

View File

@ -6,6 +6,8 @@ volumes:
services:
relay:
image: scsibug/nostr-rs-relay:0.8.13
ports:
- 5000:8080
volumes:
- data:/usr/src/app/db
app:

View File

@ -55,7 +55,8 @@
"match-sorter": "^6.3.1",
"nanoid": "^5.0.4",
"ngeohash": "^0.6.3",
"nostr-tools": "^1.17.0",
"nostr-idb": "^0.2.0",
"nostr-tools": "^2.1.3",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",

View File

@ -1,15 +1,11 @@
import { nanoid } from "nanoid";
import stringify from "json-stringify-deterministic";
import { Subject } from "./subject";
import { NostrEvent } from "../types/nostr-event";
import { NostrOutgoingRequest, NostrRequestFilter, RelayQueryMap } from "../types/nostr-query";
import Relay, { IncomingEvent } from "./relay";
import relayPoolService from "../services/relay-pool";
function isFilterEqual(a: NostrRequestFilter, b: NostrRequestFilter) {
return stringify(a) === stringify(b);
}
import { isFilterEqual } from "../helpers/nostr/filter";
export default class NostrMultiSubscription {
static INIT = "initial";

View File

@ -11,7 +11,10 @@ import EventStore from "./event-store";
import { isReplaceable } from "../helpers/nostr/events";
import replaceableEventLoaderService from "../services/replaceable-event-requester";
import deleteEventService from "../services/delete-events";
import { addQueryToFilter, isFilterEqual, mapQueryMap } from "../helpers/nostr/filter";
import { addQueryToFilter, isFilterEqual, mapQueryMap, stringifyFilter } from "../helpers/nostr/filter";
import { localCacheRelay } from "../services/local-cache-relay";
import { SimpleSubscription } from "nostr-idb";
import { Filter } from "nostr-tools";
const BLOCK_SIZE = 100;
@ -106,6 +109,7 @@ export default class TimelineLoader {
name: string;
private log: Debugger;
private subscription: NostrMultiSubscription;
private cacheSubscription?: SimpleSubscription;
private blockLoaders = new Map<string, RelayBlockLoader>();
@ -131,12 +135,12 @@ export default class TimelineLoader {
this.timeline.next(this.events.getSortedEvents().filter((e) => filter(e, this.events)));
} else this.timeline.next(this.events.getSortedEvents());
}
private handleEvent(event: NostrEvent) {
private handleEvent(event: NostrEvent, cache = true) {
// if this is a replaceable event, mirror it over to the replaceable event service
if (isReplaceable(event.kind)) {
replaceableEventLoaderService.handleEvent(event);
}
if (isReplaceable(event.kind)) replaceableEventLoaderService.handleEvent(event);
this.events.addEvent(event);
if (cache) localCacheRelay.publish(event);
}
private handleDeleteEvent(deleteEvent: NostrEvent) {
const cord = deleteEvent.tags.find(isATag)?.[1];
@ -158,6 +162,21 @@ export default class TimelineLoader {
loader.onBlockFinish.unsubscribe(this.updateComplete, this);
}
private loadQueriesFromCache(queryMap: RelayQueryMap) {
const queries: Record<string, Filter[]> = {};
for (const [url, filters] of Object.entries(queryMap)) {
const key = stringifyFilter(filters);
if (!queries[key]) queries[key] = Array.isArray(filters) ? filters : [filters];
}
for (const filters of Object.values(queries)) {
const sub: SimpleSubscription = localCacheRelay.subscribe(filters, {
onevent: (e) => this.handleEvent(e, false),
oneose: () => sub.close(),
});
}
}
setQueryMap(queryMap: RelayQueryMap) {
if (isFilterEqual(this.queryMap, queryMap)) return;
@ -190,6 +209,9 @@ export default class TimelineLoader {
this.queryMap = queryMap;
// load all filters from cache relay
this.loadQueriesFromCache(queryMap);
// update the subscription query map and add limit
this.subscription.setQueryMap(
mapQueryMap(this.queryMap, (filter) => addQueryToFilter(filter, { limit: BLOCK_SIZE / 2 })),

View File

@ -1,6 +1,6 @@
import { Flex, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay } from "@chakra-ui/react";
import { ModalProps } from "@chakra-ui/react";
import { Kind, nip19 } from "nostr-tools";
import { kinds, nip19 } from "nostr-tools";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import RawValue from "./raw-value";
@ -13,7 +13,7 @@ export default function UserDebugModal({ pubkey, ...props }: { pubkey: string }
const npub = nip19.npubEncode(pubkey);
const metadata = useUserMetadata(pubkey);
const nprofile = useSharableProfileId(pubkey);
const relays = replaceableEventLoaderService.getEvent(Kind.RelayList, pubkey).value;
const relays = replaceableEventLoaderService.getEvent(kinds.RelayList, pubkey).value;
const tipMetadata = useUserLNURLMetadata(pubkey);
return (

View File

@ -1,27 +1,109 @@
import { useMemo } from "react";
import { Box, Button, Card, CardBody, CardHeader, CardProps, Flex, Link, Text, useDisclosure } from "@chakra-ui/react";
import { MouseEventHandler, useCallback, useMemo } from "react";
import {
Box,
BoxProps,
Button,
ButtonGroup,
Card,
CardBody,
CardHeader,
CardProps,
Flex,
IconButton,
Link,
Text,
useDisclosure,
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { nip19 } from "nostr-tools";
import { getSharableEventAddress } from "../../../helpers/nip19";
import { NostrEvent } from "../../../types/nostr-event";
import { NostrEvent, Tag, isATag, isETag, isPTag } from "../../../types/nostr-event";
import UserAvatarLink from "../../user-avatar-link";
import UserLink from "../../user-link";
import { truncatedId } from "../../../helpers/nostr/events";
import { aTagToAddressPointer, eTagToEventPointer } from "../../../helpers/nostr/events";
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
import {
embedEmoji,
embedNostrHashtags,
embedNostrLinks,
embedNostrMentions,
renderGenericUrl,
renderImageUrl,
renderVideoUrl,
} from "../../embed-types";
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
import Timestamp from "../../timestamp";
import { CodeIcon } from "../../icons";
import { CodeIcon, ExternalLinkIcon } from "../../icons";
import NoteDebugModal from "../../debug-modals/note-debug-modal";
import { renderAudioUrl } from "../../embed-types/audio";
import { EmbedEventPointer } from "..";
function EventTag({ tag }: { tag: Tag }) {
const expand = useDisclosure();
const content = `[${tag[0]}] ${tag.slice(1).join(", ")}`;
const props = {
fontWeight: "bold",
fontFamily: "monospace",
fontSize: "1.2em",
isTruncated: true,
color: "GrayText",
};
const toggle = useCallback<MouseEventHandler>(
(e) => {
e.preventDefault();
expand.onToggle();
},
[expand.onToggle],
);
if (isETag(tag) && tag[1]) {
const pointer = eTagToEventPointer(tag);
return (
<>
<Link as={RouterLink} to={`/l/${nip19.neventEncode(pointer)}`} onClick={toggle} {...props}>
{content}
</Link>
{expand.isOpen && <EmbedEventPointer pointer={{ type: "nevent", data: pointer }} />}
</>
);
} else if (isATag(tag) && tag[1]) {
const pointer = aTagToAddressPointer(tag);
return (
<>
<Link as={RouterLink} to={`/l/${nip19.naddrEncode(pointer)}`} onClick={toggle} {...props}>
{content}
</Link>
{expand.isOpen && <EmbedEventPointer pointer={{ type: "naddr", data: pointer }} />}
</>
);
} else if (isPTag(tag) && tag[1]) {
const pubkey = tag[1];
return (
<>
<Link as={RouterLink} to={`/l/${nip19.npubEncode(pubkey)}`} onClick={toggle} {...props}>
{content}
</Link>
{expand.isOpen && (
<Flex gap="4" p="2">
<UserAvatarLink pubkey={pubkey} />
<Box>
<UserLink pubkey={pubkey} fontWeight="bold" />
<br />
<UserDnsIdentityIcon pubkey={pubkey} />
</Box>
</Flex>
)}
</>
);
} else
return (
<Text title={content} {...props}>
{content}
</Text>
);
}
export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
const debugModal = useDisclosure();
@ -29,16 +111,15 @@ export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "ch
const alt = event.tags.find((t) => t[0] === "alt")?.[1];
const content = useMemo(() => {
let jsx: EmbedableContent = [alt || event.content];
let jsx: EmbedableContent = [event.content];
jsx = embedNostrLinks(jsx);
jsx = embedNostrMentions(jsx, event);
jsx = embedNostrHashtags(jsx, event);
jsx = embedEmoji(jsx, event);
jsx = embedUrls(jsx, [renderImageUrl, renderVideoUrl, renderAudioUrl, renderGenericUrl]);
return jsx;
}, [event.content, alt]);
}, [event.content]);
return (
<>
@ -47,23 +128,41 @@ export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "ch
<UserAvatarLink pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="md" />
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Link ml="auto" href={address ? buildAppSelectUrl(address) : ""} isExternal>
<Timestamp timestamp={event.created_at} />
</Link>
<Text>kind: {event.kind}</Text>
<Timestamp timestamp={event.created_at} />
<ButtonGroup ml="auto">
<Button
as={Link}
size="sm"
leftIcon={<ExternalLinkIcon />}
isExternal
href={address ? buildAppSelectUrl(address) : ""}
>
Open
</Button>
<IconButton
icon={<CodeIcon />}
aria-label="Raw Event"
size="sm"
variant="outline"
onClick={debugModal.onOpen}
/>
</ButtonGroup>
</CardHeader>
<CardBody p="2">
<Flex gap="2">
<Text>Kind: {event.kind}</Text>
<Link href={address ? buildAppSelectUrl(address) : ""} isExternal color="blue.500">
{address && truncatedId(address)}
</Link>
<Button leftIcon={<CodeIcon />} ml="auto" size="sm" variant="outline" onClick={debugModal.onOpen}>
View Raw
</Button>
</Flex>
<Box whiteSpace="pre-wrap" noOfLines={5}>
{alt && (
<Text isTruncated fontStyle="italic">
{alt}
</Text>
)}
<Box whiteSpace="pre-wrap" noOfLines={3}>
{content}
</Box>
<Flex direction="column" gap="1" px="2" my="2">
{event.tags.map((tag, i) => (
<EventTag key={i} tag={tag} />
))}
</Flex>
</CardBody>
</Card>
{debugModal.isOpen && <NoteDebugModal isOpen={debugModal.isOpen} onClose={debugModal.onClose} event={event} />}

View File

@ -1,7 +1,7 @@
import { lazy } from "react";
import { Suspense, lazy } from "react";
import type { DecodeResult } from "nostr-tools/lib/types/nip19";
import { CardProps } from "@chakra-ui/react";
import { Kind, nip19 } from "nostr-tools";
import { CardProps, Spinner } from "@chakra-ui/react";
import { kinds, nip19 } from "nostr-tools";
import EmbeddedNote from "./event-types/embedded-note";
import useSingleEvent from "../../hooks/use-single-event";
@ -51,46 +51,50 @@ export function EmbedEvent({
goalProps,
...cardProps
}: Omit<CardProps, "children"> & { event: NostrEvent } & EmbedProps) {
switch (event.kind) {
case Kind.Text:
return <EmbeddedNote event={event} {...cardProps} />;
case Kind.Reaction:
return <EmbeddedReaction event={event} {...cardProps} />;
case Kind.EncryptedDirectMessage:
return <EmbeddedDM dm={event} {...cardProps} />;
case STREAM_KIND:
return <EmbeddedStream event={event} {...cardProps} />;
case GOAL_KIND:
return <EmbeddedGoal goal={event} {...cardProps} {...goalProps} />;
case EMOJI_PACK_KIND:
return <EmbeddedEmojiPack pack={event} {...cardProps} />;
case PEOPLE_LIST_KIND:
case NOTE_LIST_KIND:
case BOOKMARK_LIST_KIND:
case COMMUNITIES_LIST_KIND:
case CHANNELS_LIST_KIND:
return <EmbeddedList list={event} {...cardProps} />;
case Kind.Article:
return <EmbeddedArticle article={event} {...cardProps} />;
case Kind.BadgeDefinition:
return <EmbeddedBadge badge={event} {...cardProps} />;
case STREAM_CHAT_MESSAGE_KIND:
return <EmbeddedStreamMessage message={event} {...cardProps} />;
case COMMUNITY_DEFINITION_KIND:
return <EmbeddedCommunity community={event} {...cardProps} />;
case STEMSTR_TRACK_KIND:
return <EmbeddedStemstrTrack track={event} {...cardProps} />;
case TORRENT_KIND:
return <EmbeddedTorrent torrent={event} {...cardProps} />;
case TORRENT_COMMENT_KIND:
return <EmbeddedTorrentComment comment={event} {...cardProps} />;
case FLARE_VIDEO_KIND:
return <EmbeddedFlareVideo video={event} {...cardProps} />;
case Kind.ChannelCreation:
return <EmbeddedChannel channel={event} {...cardProps} />;
}
const renderContent = () => {
switch (event.kind) {
case kinds.ShortTextNote:
return <EmbeddedNote event={event} {...cardProps} />;
case kinds.Reaction:
return <EmbeddedReaction event={event} {...cardProps} />;
case kinds.EncryptedDirectMessage:
return <EmbeddedDM dm={event} {...cardProps} />;
case STREAM_KIND:
return <EmbeddedStream event={event} {...cardProps} />;
case GOAL_KIND:
return <EmbeddedGoal goal={event} {...cardProps} {...goalProps} />;
case EMOJI_PACK_KIND:
return <EmbeddedEmojiPack pack={event} {...cardProps} />;
case PEOPLE_LIST_KIND:
case NOTE_LIST_KIND:
case BOOKMARK_LIST_KIND:
case COMMUNITIES_LIST_KIND:
case CHANNELS_LIST_KIND:
return <EmbeddedList list={event} {...cardProps} />;
case kinds.LongFormArticle:
return <EmbeddedArticle article={event} {...cardProps} />;
case kinds.BadgeDefinition:
return <EmbeddedBadge badge={event} {...cardProps} />;
case STREAM_CHAT_MESSAGE_KIND:
return <EmbeddedStreamMessage message={event} {...cardProps} />;
case COMMUNITY_DEFINITION_KIND:
return <EmbeddedCommunity community={event} {...cardProps} />;
case STEMSTR_TRACK_KIND:
return <EmbeddedStemstrTrack track={event} {...cardProps} />;
case TORRENT_KIND:
return <EmbeddedTorrent torrent={event} {...cardProps} />;
case TORRENT_COMMENT_KIND:
return <EmbeddedTorrentComment comment={event} {...cardProps} />;
case FLARE_VIDEO_KIND:
return <EmbeddedFlareVideo video={event} {...cardProps} />;
case kinds.ChannelCreation:
return <EmbeddedChannel channel={event} {...cardProps} />;
}
return <EmbeddedUnknown event={event} {...cardProps} />;
return <EmbeddedUnknown event={event} {...cardProps} />;
};
return <Suspense fallback={<Spinner />}>{renderContent()}</Suspense>;
}
export function EmbedEventPointer({ pointer, ...props }: { pointer: DecodeResult } & EmbedProps) {

View File

@ -1,5 +1,5 @@
import { Button, Flex, SimpleGrid, SimpleGridProps, Text, useDisclosure } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { Flex, Text } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { NostrEvent } from "../../types/nostr-event";
import UserAvatarLink from "../user-avatar-link";
@ -11,7 +11,7 @@ import Timestamp from "../timestamp";
export default function RepostDetails({ event }: { event: NostrEvent }) {
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${event.id}-reposts`, readRelays, { kinds: [Kind.Repost], "#e": [event.id] });
const timeline = useTimelineLoader(`${event.id}-reposts`, readRelays, { kinds: [kinds.Repost], "#e": [event.id] });
const reposts = useSubject(timeline.timeline);

View File

@ -1,5 +1,5 @@
import { memo } from "react";
import { verifySignature } from "nostr-tools";
import { verifyEvent } from "nostr-tools";
import { NostrEvent } from "../types/nostr-event";
import { CheckIcon, VerificationFailed } from "./icons";
@ -9,7 +9,7 @@ function EventVerificationIcon({ event }: { event: NostrEvent }) {
const { showSignatureVerification } = useAppSettings();
if (!showSignatureVerification) return null;
if (!verifySignature(event)) {
if (!verifyEvent(event)) {
return <VerificationFailed color="red.500" />;
}
return <CheckIcon color="green.500" />;

View File

@ -9,11 +9,10 @@ import {
ModalProps,
} from "@chakra-ui/react";
import dayjs from "dayjs";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { DraftNostrEvent, NostrEvent, isDTag } from "../../types/nostr-event";
import clientRelaysService from "../../services/client-relays";
import { getEventRelays } from "../../services/event-relays";
import { getZapSplits } from "../../helpers/nostr/zaps";
import { unique } from "../../helpers/array";
import { RelayMode } from "../../classes/relay";
@ -76,7 +75,7 @@ async function getPayRequestForPubkey(
// create zap request
const zapRequest: DraftNostrEvent = {
kind: Kind.ZapRequest,
kind: kinds.ZapRequest,
created_at: dayjs().unix(),
content: comment ?? "",
tags: [

View File

@ -1,6 +1,6 @@
import { useCallback, useState } from "react";
import { Box, Card, CloseButton, Divider, Flex, FlexProps, Spacer, Text } from "@chakra-ui/react";
import { Kind, nip18, nip19, nip25 } from "nostr-tools";
import { kinds, nip18, nip19, nip25 } from "nostr-tools";
import { useNavigate } from "react-router-dom";
import { useInterval } from "react-use";
import dayjs from "dayjs";
@ -19,24 +19,24 @@ import { getSharableEventAddress } from "../../helpers/nip19";
import { safeRelayUrls } from "../../helpers/url";
const kindColors: Record<number, FlexProps["bg"]> = {
[Kind.Text]: "blue.500",
[Kind.RecommendRelay]: "pink",
[Kind.EncryptedDirectMessage]: "orange.500",
[Kind.Repost]: "yellow",
[Kind.Reaction]: "green.500",
[Kind.Article]: "purple.500",
[kinds.ShortTextNote]: "blue.500",
[kinds.RecommendRelay]: "pink",
[kinds.EncryptedDirectMessage]: "orange.500",
[kinds.Repost]: "yellow",
[kinds.Reaction]: "green.500",
[kinds.LongFormArticle]: "purple.500",
};
function EventChunk({ event, ...props }: { event: NostrEvent } & Omit<FlexProps, "children">) {
const navigate = useNavigate();
const handleClick = useCallback(() => {
switch (event.kind) {
case Kind.Reaction: {
case kinds.Reaction: {
const pointer = nip25.getReactedEventPointer(event);
if (pointer) navigate(`/l/${nip19.neventEncode(pointer)}`);
return;
}
case Kind.Repost: {
case kinds.Repost: {
const pointer = nip18.getRepostedEventPointer(event);
if (pointer?.relays) pointer.relays = safeRelayUrls(pointer.relays);
if (pointer) navigate(`/l/${nip19.neventEncode(pointer)}`);
@ -48,11 +48,11 @@ function EventChunk({ event, ...props }: { event: NostrEvent } & Omit<FlexProps,
const getTitle = () => {
switch (event.kind) {
case Kind.Text:
case kinds.ShortTextNote:
return "Note";
case Kind.Reaction:
case kinds.Reaction:
return "Reaction";
case Kind.EncryptedDirectMessage:
case kinds.EncryptedDirectMessage:
return "Direct Message";
}
};

View File

@ -49,6 +49,7 @@ export default function NavItems() {
else if (location.pathname.startsWith("/dm")) active = "dm";
else if (location.pathname.startsWith("/streams")) active = "streams";
else if (location.pathname.startsWith("/relays")) active = "relays";
else if (location.pathname.startsWith("/r/")) active = "relays";
else if (location.pathname.startsWith("/lists")) active = "lists";
else if (location.pathname.startsWith("/communities")) active = "communities";
else if (location.pathname.startsWith("/channels")) active = "channels";

View File

@ -13,7 +13,7 @@ import {
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import dayjs from "dayjs";
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
@ -34,7 +34,7 @@ function buildRepost(event: NostrEvent): DraftNostrEvent {
tags.push(["e", event.id, hint ?? ""]);
return {
kind: Kind.Repost,
kind: kinds.Repost,
tags,
content: JSON.stringify(event),
created_at: dayjs().unix(),

View File

@ -26,7 +26,7 @@ import {
} from "@chakra-ui/react";
import dayjs from "dayjs";
import { useForm } from "react-hook-form";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { ChevronDownIcon, ChevronUpIcon, UploadImageIcon } from "../icons";
import NostrPublishAction from "../../classes/nostr-publish-action";
@ -124,7 +124,7 @@ export default function PostModal({
let updatedDraft = finalizeNote({
content: content,
kind: Kind.Text,
kind: kinds.ShortTextNote,
tags: [],
created_at: dayjs().unix(),
});

View File

@ -1,6 +1,6 @@
import { useRef } from "react";
import { Flex, Heading, Link, SkeletonText, Text } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import { isETag, NostrEvent } from "../../../types/nostr-event";
@ -60,7 +60,7 @@ export default function RepostNote({ event }: { event: NostrEvent }) {
</Flex>
{!note ? (
<SkeletonText />
) : note.kind === Kind.Text ? (
) : note.kind === kinds.ShortTextNote ? (
// NOTE: tell the note not to register itself with the intersection observer. since this is an older note it will break the order of the timeline
<Note event={note} showReplyButton registerIntersectionEntity={false} />
) : (

View File

@ -1,6 +1,6 @@
import { ReactNode, memo, useRef } from "react";
import { Kind } from "nostr-tools";
import { Box, BreadcrumbLink, Text } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { Box, Text } from "@chakra-ui/react";
import { ErrorBoundary } from "../../error-boundary";
import ReplyNote from "./reply-note";
@ -23,22 +23,22 @@ function TimelineItem({ event, visible, minHeight }: { event: NostrEvent; visibl
let content: ReactNode | null = null;
switch (event.kind) {
case Kind.Text:
case kinds.ShortTextNote:
content = isReply(event) ? <ReplyNote event={event} /> : <Note event={event} showReplyButton />;
break;
case Kind.Repost:
case kinds.Repost:
content = <RepostNote event={event} />;
break;
case Kind.Article:
case kinds.LongFormArticle:
content = <ArticleNote article={event} />;
break;
case STREAM_KIND:
content = <StreamNote event={event} />;
break;
case Kind.RecommendRelay:
case kinds.RecommendRelay:
content = <RelayRecommendation event={event} />;
break;
case Kind.BadgeAward:
case kinds.BadgeAward:
content = <BadgeAwardCard award={event} />;
break;
case FLARE_VIDEO_KIND:

View File

@ -1,5 +1,5 @@
import { useMemo, useRef } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { Photo } from "react-photo-album";
import TimelineLoader from "../../../classes/timeline-loader";
@ -45,7 +45,7 @@ export default function MediaTimeline({ timeline }: { timeline: TimelineLoader }
var images: PhotoWithEvent[] = [];
for (const event of events) {
if (event.kind === Kind.Repost) continue;
if (event.kind === kinds.Repost) continue;
const urls = event.content.matchAll(getMatchLink());
let i = 0;

View File

@ -2,7 +2,6 @@ import { getPublicKey, nip19 } from "nostr-tools";
import { NostrEvent, Tag, isATag, isDTag, isETag, isPTag } from "../types/nostr-event";
import { isReplaceable } from "./nostr/events";
import { DecodeResult } from "nostr-tools/lib/types/nip19";
import relayHintService from "../services/event-relay-hint";
export function isHex(str?: string) {
@ -52,7 +51,7 @@ export function getSharableEventAddress(event: NostrEvent) {
}
}
export function encodePointer(pointer: DecodeResult) {
export function encodePointer(pointer: nip19.DecodeResult) {
switch (pointer.type) {
case "naddr":
return nip19.naddrEncode(pointer.data);
@ -71,7 +70,7 @@ export function encodePointer(pointer: DecodeResult) {
}
}
export function getPointerFromTag(tag: Tag): DecodeResult | null {
export function getPointerFromTag(tag: Tag): nip19.DecodeResult | null {
if (isETag(tag)) {
if (!tag[1]) return null;
return {

View File

@ -1,4 +1,4 @@
import { validateEvent } from "nostr-tools";
import { kinds, validateEvent } from "nostr-tools";
import { NostrEvent, isATag, isDTag, isETag, isPTag } from "../../types/nostr-event";
import { getMatchLink, getMatchNostrLink } from "../regexp";
import { ReactionGroup } from "./reactions";
@ -6,8 +6,8 @@ import { parseCoordinate } from "./events";
/** @deprecated */
export const SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER = "communities";
export const COMMUNITY_DEFINITION_KIND = 34550;
export const COMMUNITY_APPROVAL_KIND = 4550;
export const COMMUNITY_DEFINITION_KIND = kinds.CommunityDefinition;
export const COMMUNITY_APPROVAL_KIND = kinds.CommunityPostApproval;
export function getCommunityName(community: NostrEvent) {
const name = community.tags.find(isDTag)?.[1];

View File

@ -1,4 +1,4 @@
import { Kind, nip19, validateEvent } from "nostr-tools";
import { kinds, validateEvent } from "nostr-tools";
import { ATag, DraftNostrEvent, ETag, isATag, isDTag, isETag, NostrEvent, RTag, Tag } from "../../types/nostr-event";
import { RelayConfig, RelayMode } from "../../classes/relay";
@ -12,9 +12,8 @@ export function truncatedId(str: string, keep = 6) {
return str.substring(0, keep) + "..." + str.substring(str.length - keep);
}
// based on replaceable kinds from https://github.com/nostr-protocol/nips/blob/master/01.md#kinds
export function isReplaceable(kind: number) {
return (kind >= 30000 && kind < 40000) || kind === 0 || kind === 3 || kind === 41 || (kind >= 10000 && kind < 20000);
return kinds.isReplaceableKind(kind) || kinds.isParameterizedReplaceableKind(kind);
}
export function pointerMatchEvent(event: NostrEvent, pointer: AddressPointer | EventPointer) {
@ -42,7 +41,7 @@ export function getEventUID(event: NostrEvent) {
}
export function isReply(event: NostrEvent | DraftNostrEvent) {
if (event.kind === Kind.Repost) return false;
if (event.kind === kinds.Repost) return false;
// TODO: update this to only look for a "root" or "reply" tag
return !!getReferences(event).reply;
}
@ -51,7 +50,7 @@ export function isMentionedInContent(event: NostrEvent | DraftNostrEvent, pubkey
}
export function isRepost(event: NostrEvent | DraftNostrEvent) {
if (event.kind === Kind.Repost) return true;
if (event.kind === kinds.Repost) return true;
const match = event.content.match(getMatchNostrLink());
return match && match[0].length === event.content.length;

View File

@ -1,6 +1,6 @@
import stringify from "json-stringify-deterministic";
import { NostrQuery, NostrRequestFilter, RelayQueryMap } from "../../types/nostr-query";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "../../services/local-cache-relay";
import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "../../services/local-cache-relay";
export function addQueryToFilter(filter: NostrRequestFilter, query: NostrQuery) {
if (Array.isArray(filter)) {
@ -9,8 +9,11 @@ export function addQueryToFilter(filter: NostrRequestFilter, query: NostrQuery)
return { ...filter, ...query };
}
export function stringifyFilter(filter: NostrRequestFilter) {
return stringify(filter);
}
export function isFilterEqual(a: NostrRequestFilter, b: NostrRequestFilter) {
return stringify(a) === stringify(b);
return stringifyFilter(a) === stringifyFilter(b);
}
export function mapQueryMap(queryMap: RelayQueryMap, fn: (filter: NostrRequestFilter) => NostrRequestFilter) {
@ -23,7 +26,7 @@ export function createSimpleQueryMap(relays: string[], filter: NostrRequestFilte
const map: RelayQueryMap = {};
// if the local cache relay is enabled, also ask it
if (localCacheRelayService.enabled) {
if (LOCAL_CACHE_RELAY_ENABLED) {
map[LOCAL_CACHE_RELAY] = filter;
}

View File

@ -1,5 +1,5 @@
import dayjs from "dayjs";
import { Kind, nip19 } from "nostr-tools";
import { kinds, nip19 } from "nostr-tools";
import { DraftNostrEvent, NostrEvent, PTag, isATag, isDTag, isETag, isPTag, isRTag } from "../../types/nostr-event";
import { parseCoordinate } from "./events";
@ -15,7 +15,7 @@ export const NOTE_LIST_KIND = 30001;
export const BOOKMARK_LIST_SET_KIND = 30003;
export function getListName(event: NostrEvent) {
if (event.kind === Kind.Contacts) return "Following";
if (event.kind === kinds.Contacts) return "Following";
if (event.kind === MUTE_LIST_KIND) return "Mute";
if (event.kind === PIN_LIST_KIND) return "Pins";
if (event.kind === BOOKMARK_LIST_KIND) return "Bookmarks";
@ -38,7 +38,7 @@ export function isJunkList(event: NostrEvent) {
}
export function isSpecialListKind(kind: number) {
return (
kind === Kind.Contacts ||
kind === kinds.Contacts ||
kind === MUTE_LIST_KIND ||
kind === PIN_LIST_KIND ||
kind === BOOKMARK_LIST_KIND ||
@ -93,7 +93,7 @@ export function createEmptyContactList(): DraftNostrEvent {
created_at: dayjs().unix(),
content: "",
tags: [],
kind: Kind.Contacts,
kind: kinds.Contacts,
};
}

View File

@ -1,4 +1,4 @@
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { DraftNostrEvent, NostrEvent, Tag } from "../../types/nostr-event";
import dayjs from "dayjs";
import { getEventCoordinate, isReplaceable } from "./events";
@ -27,7 +27,7 @@ export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string)
["p", event.pubkey],
];
const draft: DraftNostrEvent = {
kind: Kind.Reaction,
kind: kinds.Reaction,
content: url ? ":" + emoji + ":" : emoji,
tags: isReplaceable(event.kind) ? [...tags, ["a", getEventCoordinate(event)]] : tags,
created_at: dayjs().unix(),

View File

@ -1,6 +1,9 @@
import { SimpleRelay, SimpleSubscription, SimpleSubscriptionOptions } from "nostr-idb";
import { RelayConfig } from "../classes/relay";
import { NostrQuery, NostrRequestFilter } from "../types/nostr-query";
import { safeRelayUrl } from "./url";
import { Filter } from "nostr-tools";
import { NostrEvent } from "../types/nostr-event";
export function normalizeRelayConfigs(relays: RelayConfig[]) {
const seen: string[] = [];
@ -52,3 +55,18 @@ export function splitQueryByPubkeys(query: NostrQuery, relayPubkeyMap: Record<st
return filtersByRelay;
}
export function relayRequest(relay: SimpleRelay, filters: Filter[], opts: SimpleSubscriptionOptions = {}) {
return new Promise<NostrEvent[]>((res) => {
const events: NostrEvent[] = [];
const sub: SimpleSubscription = relay.subscribe(filters, {
...opts,
onevent: (e) => events.push(e),
oneose: () => {
sub.close();
res(events);
},
onclose: () => res(events),
});
});
}

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import useSubject from "./use-subject";
import useSingleEvent from "./use-single-event";
@ -12,7 +12,7 @@ import { unique } from "../helpers/array";
export default function useThreadTimelineLoader(
focusedEvent: NostrEvent | undefined,
relays: string[],
kind: number = Kind.Text,
kind: number = kinds.ShortTextNote,
) {
const refs = focusedEvent && getReferences(focusedEvent);
const rootId = refs?.root?.e?.id || focusedEvent?.id;

View File

@ -1,4 +1,4 @@
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import useReplaceableEvent from "./use-replaceable-event";
import { RequestOptions } from "../services/replaceable-event-requester";
@ -7,5 +7,5 @@ export default function useUserContactList(
additionalRelays: string[] = [],
opts: RequestOptions = {},
) {
return useReplaceableEvent(pubkey && { kind: Kind.Contacts, pubkey }, additionalRelays, opts);
return useReplaceableEvent(pubkey && { kind: kinds.Contacts, pubkey }, additionalRelays, opts);
}

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { getPubkeysFromList } from "../helpers/nostr/lists";
import useUserContactList from "./use-user-contact-list";
@ -34,7 +34,7 @@ export default function useUserNetwork(pubkey: string, additionalRelays: string[
const subjects = useMemo(() => {
return contactsPubkeys.map((person) =>
replaceableEventLoaderService.requestEvent(readRelays, Kind.Contacts, person.pubkey),
replaceableEventLoaderService.requestEvent(readRelays, kinds.Contacts, person.pubkey),
);
}, [contactsPubkeys, readRelays.join("|")]);

View File

@ -1,4 +1,4 @@
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import useReplaceableEvent from "./use-replaceable-event";
import { PROFILE_BADGES_IDENTIFIER, parseProfileBadges } from "../helpers/nostr/badges";
@ -8,14 +8,20 @@ import { getEventCoordinate } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
export default function useUserProfileBadges(pubkey: string, additionalRelays: string[] = []) {
const profileBadgesEvent = useReplaceableEvent({
pubkey,
kind: Kind.ProfileBadge,
identifier: PROFILE_BADGES_IDENTIFIER,
});
const profileBadgesEvent = useReplaceableEvent(
{
pubkey,
kind: kinds.ProfileBadges,
identifier: PROFILE_BADGES_IDENTIFIER,
},
additionalRelays,
);
const parsed = profileBadgesEvent ? parseProfileBadges(profileBadgesEvent) : [];
const badges = useReplaceableEvents(parsed.map((b) => b.badgeCord));
const badges = useReplaceableEvents(
parsed.map((b) => b.badgeCord),
additionalRelays,
);
const awardEvents = useSingleEvents(parsed.map((b) => b.awardEventId));
const final: { badge: NostrEvent; award: NostrEvent }[] = [];

View File

@ -1,5 +1,5 @@
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useCurrentAccount from "../../hooks/use-current-account";
@ -7,7 +7,6 @@ import TimelineLoader from "../../classes/timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { TORRENT_COMMENT_KIND } from "../../helpers/nostr/torrents";
type DMTimelineContextType = {
timeline?: TimelineLoader;
@ -40,8 +39,8 @@ export default function DMTimelineProvider({ children }: PropsWithChildren) {
inbox,
account?.pubkey
? [
{ authors: [account.pubkey], kinds: [Kind.EncryptedDirectMessage] },
{ "#p": [account.pubkey], kinds: [Kind.EncryptedDirectMessage] },
{ authors: [account.pubkey], kinds: [kinds.EncryptedDirectMessage] },
{ "#p": [account.pubkey], kinds: [kinds.EncryptedDirectMessage] },
]
: undefined,
{ eventFilter },

View File

@ -1,5 +1,5 @@
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useCurrentAccount from "../../hooks/use-current-account";
@ -41,7 +41,14 @@ export default function NotificationTimelineProvider({ children }: PropsWithChil
account?.pubkey
? {
"#p": [account.pubkey],
kinds: [Kind.Text, Kind.Repost, Kind.Reaction, Kind.Zap, TORRENT_COMMENT_KIND, Kind.Article],
kinds: [
kinds.ShortTextNote,
kinds.Repost,
kinds.Reaction,
kinds.Zap,
TORRENT_COMMENT_KIND,
kinds.LongFormArticle,
],
}
: undefined,
{ eventFilter },

View File

@ -1,5 +1,5 @@
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import useCurrentAccount from "../../hooks/use-current-account";
import { getPubkeysFromList } from "../../helpers/nostr/lists";
@ -34,7 +34,7 @@ function useListCoordinate(listId: ListId) {
const account = useCurrentAccount();
return useMemo(() => {
if (listId === "following") return account ? `${Kind.Contacts}:${account.pubkey}` : undefined;
if (listId === "following") return account ? `${kinds.Contacts}:${account.pubkey}` : undefined;
if (listId === "global") return undefined;
return listId;
}, [listId, account]);

View File

@ -20,7 +20,7 @@ import {
Text,
useToast,
} from "@chakra-ui/react";
import { Event, Kind } from "nostr-tools";
import { Event, kinds } from "nostr-tools";
import dayjs from "dayjs";
import useCurrentAccount from "../../hooks/use-current-account";
@ -80,7 +80,7 @@ export default function DeleteEventProvider({ children }: PropsWithChildren) {
}
const draft = {
kind: Kind.EventDeletion,
kind: kinds.EventDeletion,
tags,
content: reason,
created_at: dayjs().unix(),

View File

@ -1,4 +1,4 @@
import { getEventHash, nip19, verifySignature } from "nostr-tools";
import { getEventHash, nip19, verifyEvent } from "nostr-tools";
import createDefer, { Deferred } from "../classes/deferred";
import { getPubkeyFromDecodeResult, isHex, isHexKey } from "../helpers/nip19";
@ -78,7 +78,7 @@ async function signEvent(draft: DraftNostrEvent & { pubkey: string }): Promise<N
if (!isHex(sig)) throw new Error("Expected hex signature");
const event: NostrEvent = { ...draftWithId, sig };
if (!verifySignature(event)) throw new Error("Invalid signature");
if (!verifyEvent(event)) throw new Error("Invalid signature");
return event;
}

View File

@ -1,7 +1,7 @@
import dayjs from "dayjs";
import debug, { Debugger } from "debug";
import _throttle from "lodash/throttle";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import NostrSubscription from "../classes/nostr-subscription";
import SuperMap from "../classes/super-map";
@ -12,7 +12,7 @@ import { logger } from "../helpers/debug";
import db from "./db";
import createDefer, { Deferred } from "../classes/deferred";
import { getChannelPointer } from "../helpers/nostr/channel";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "./local-cache-relay";
import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "./local-cache-relay";
type Pubkey = string;
type Relay = string;
@ -105,7 +105,7 @@ class ChannelMetadataRelayLoader {
if (needsUpdate) {
if (this.requested.size > 0) {
const query: NostrQuery = {
kinds: [Kind.ChannelMetadata],
kinds: [kinds.ChannelMetadata],
"#e": Array.from(this.requested.keys()),
};
@ -231,7 +231,7 @@ class ChannelMetadataService {
const sub = this.metadata.get(channelId);
const relayUrls = Array.from(relays);
if (localCacheRelayService.enabled) {
if (LOCAL_CACHE_RELAY_ENABLED) {
relayUrls.unshift(LOCAL_CACHE_RELAY);
}
for (const relay of relayUrls) {

View File

@ -1,6 +1,9 @@
import { openDB, deleteDB, IDBPDatabase, IDBPTransaction } from "idb";
import { clearDB } from "nostr-idb";
import { SchemaV1, SchemaV2, SchemaV3, SchemaV4, SchemaV5, SchemaV6, SchemaV7 } from "./schema";
import { logger } from "../../helpers/debug";
import { localCacheDatabase } from "../local-cache-relay";
const log = logger.extend("Database");
@ -169,6 +172,9 @@ const db = await openDB<SchemaV6>(dbName, version, {
log("Open");
export async function clearCacheData() {
log("Clearing nostr-idb");
await clearDB(localCacheDatabase);
log("Clearing replaceableEvents");
await db.clear("replaceableEvents");
@ -195,6 +201,7 @@ export async function deleteDatabase() {
db.close();
log("Deleting");
await deleteDB(dbName);
await deleteDB("events");
window.location.reload();
}

View File

@ -1,4 +1,4 @@
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import Subject from "../classes/subject";
import { getEventUID } from "../helpers/nostr/events";
@ -7,7 +7,7 @@ import { NostrEvent } from "../types/nostr-event";
const deleteEventStream = new Subject<NostrEvent>();
function handleEvent(deleteEvent: NostrEvent) {
if (deleteEvent.kind !== Kind.EventDeletion) return;
if (deleteEvent.kind !== kinds.EventDeletion) return;
deleteEventStream.next(deleteEvent);
}

View File

@ -8,7 +8,7 @@ import relayScoreboardService from "./relay-scoreboard";
import { logger } from "../helpers/debug";
import { matchFilter, matchFilters } from "nostr-tools";
import { NostrEvent } from "../types/nostr-event";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "./local-cache-relay";
import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED } from "./local-cache-relay";
function hashFilter(filter: NostrRequestFilter) {
// const encoder = new TextEncoder();
@ -43,7 +43,7 @@ class EventExistsService {
if (sub.value !== true) {
const relayUrls = Array.from(relays);
if (localCacheRelayService.enabled) relayUrls.unshift(LOCAL_CACHE_RELAY);
if (LOCAL_CACHE_RELAY_ENABLED) relayUrls.unshift(LOCAL_CACHE_RELAY);
for (const url of relayUrls) {
if (!asked.has(url) && !pending.has(url)) {

View File

@ -1,4 +1,4 @@
import { Kind, nip25 } from "nostr-tools";
import { kinds, nip25 } from "nostr-tools";
import NostrRequest from "../classes/nostr-request";
import Subject from "../classes/subject";
@ -25,7 +25,7 @@ class EventReactionsService {
}
handleEvent(event: NostrEvent) {
if (event.kind !== Kind.Reaction) return;
if (event.kind !== kinds.Reaction) return;
const pointer = nip25.getReactedEventPointer(event);
if (!pointer?.id) return;
@ -51,7 +51,7 @@ class EventReactionsService {
for (const [relay, ids] of Object.entries(idsFromRelays)) {
const request = new NostrRequest([relay]);
request.onEvent.subscribe(this.handleEvent, this);
request.start({ "#e": ids, kinds: [Kind.Reaction] });
request.start({ "#e": ids, kinds: [kinds.Reaction] });
}
this.pending.clear();
}

View File

@ -1,4 +1,4 @@
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import NostrRequest from "../classes/nostr-request";
import Subject from "../classes/subject";
@ -27,7 +27,7 @@ class EventZapsService {
}
handleEvent(event: NostrEvent) {
if (event.kind !== Kind.Zap) return;
if (event.kind !== kinds.Zap) return;
const eventUID = event.tags.find(isETag)?.[1] ?? event.tags.find(isATag)?.[1];
if (!eventUID) return;
@ -58,10 +58,10 @@ class EventZapsService {
const queries: NostrRequestFilter = [];
if (eventIds.length > 0) {
queries.push({ "#e": eventIds, kinds: [Kind.Zap] });
queries.push({ "#e": eventIds, kinds: [kinds.Zap] });
}
if (coordinates.length > 0) {
queries.push({ "#a": coordinates, kinds: [Kind.Zap] });
queries.push({ "#a": coordinates, kinds: [kinds.Zap] });
}
request.start(queries);

View File

@ -1,8 +1,9 @@
import { CacheRelay, openDB } from "nostr-idb";
import { Relay } from "nostr-tools";
import { logger } from "../helpers/debug";
import { NostrEvent } from "../types/nostr-event";
import relayPoolService from "./relay-pool";
import _throttle from "lodash.throttle";
const log = logger.extend(`LocalCacheRelay`);
const params = new URLSearchParams(location.search);
const paramRelay = params.get("cacheRelay");
@ -17,60 +18,34 @@ const storedCacheRelayURL = localStorage.getItem("cacheRelay");
const url = (storedCacheRelayURL && new URL(storedCacheRelayURL)) || new URL("/cache-relay", location.href);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
export const CACHE_RELAY_ENABLED = !!window.CACHE_RELAY_ENABLED || !!localStorage.getItem("cacheRelay");
export const LOCAL_CACHE_RELAY_ENABLED = !!window.CACHE_RELAY_ENABLED || !!localStorage.getItem("cacheRelay");
export const LOCAL_CACHE_RELAY = url.toString();
const wroteEvents = new Set<string>();
const writeQueue: NostrEvent[] = [];
export const localCacheDatabase = await openDB();
const BATCH_WRITE = 100;
const log = logger.extend(`LocalCacheRelay`);
async function flush() {
for (let i = 0; i < BATCH_WRITE; i++) {
const e = writeQueue.pop();
if (!e) continue;
relayPoolService.requestRelay(LOCAL_CACHE_RELAY).send(["EVENT", e]);
}
}
function report() {
if (writeQueue.length) {
log(`${writeQueue.length} events in write queue`);
function createRelay() {
if (LOCAL_CACHE_RELAY_ENABLED) {
log(`Using ${LOCAL_CACHE_RELAY}`);
return new Relay(LOCAL_CACHE_RELAY);
} else {
log(`Using IndexedDB`);
return new CacheRelay(localCacheDatabase);
}
}
function addToQueue(e: NostrEvent) {
if (!CACHE_RELAY_ENABLED) return;
if (!wroteEvents.has(e.id)) {
wroteEvents.add(e.id);
writeQueue.push(e);
}
}
export const localCacheRelay = createRelay();
if (CACHE_RELAY_ENABLED) {
log("Enabled");
relayPoolService.onRelayCreated.subscribe((relay) => {
if (relay.url !== LOCAL_CACHE_RELAY) {
relay.onEvent.subscribe((incomingEvent) => addToQueue(incomingEvent.body));
}
});
}
const localCacheRelayService = {
enabled: CACHE_RELAY_ENABLED,
addToQueue,
};
// connect without waiting
localCacheRelay.connect().then(() => {
log("Connected");
});
// keep the relay connection alive
setInterval(() => {
if (CACHE_RELAY_ENABLED) flush();
}, 1000);
setInterval(() => {
if (CACHE_RELAY_ENABLED) report();
}, 1000 * 10);
if (!localCacheRelay.connected) localCacheRelay.connect().then(() => log("Reconnected"));
}, 1000 * 5);
if (import.meta.env.DEV) {
//@ts-ignore
window.localCacheRelayService = localCacheRelayService;
window.localCacheRelay = localCacheRelay;
}
export default localCacheRelayService;

View File

@ -1,4 +1,4 @@
import { finishEvent, generatePrivateKey, getPublicKey, nip04, nip19 } from "nostr-tools";
import { finalizeEvent, generateSecretKey, getPublicKey, nip04, nip19 } from "nostr-tools";
import dayjs from "dayjs";
import { nanoid } from "nanoid";
@ -10,6 +10,7 @@ import { DraftNostrEvent, NostrEvent, isPTag } from "../types/nostr-event";
import createDefer, { Deferred } from "../classes/deferred";
import { truncatedId } from "../helpers/nostr/events";
import { NostrConnectAccount } from "./account";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
export enum NostrConnectMethod {
Connect = "connect",
@ -60,8 +61,8 @@ export class NostrConnectClient {
this.pubkey = pubkey;
this.relays = relays;
this.secretKey = secretKey || generatePrivateKey();
this.publicKey = getPublicKey(this.secretKey);
this.secretKey = secretKey || bytesToHex(generateSecretKey());
this.publicKey = getPublicKey(hexToBytes(this.secretKey));
this.sub.onEvent.subscribe(this.handleEvent, this);
this.sub.setQueryMap(createSimpleQueryMap(this.relays, { kinds: [24133], "#p": [this.publicKey] }));
@ -99,14 +100,14 @@ export class NostrConnectClient {
}
private createEvent(content: string) {
return finishEvent(
return finalizeEvent(
{
kind: 24133,
created_at: dayjs().unix(),
tags: [["p", this.pubkey]],
content,
},
this.secretKey,
hexToBytes(this.secretKey),
);
}
private async makeRequest<T extends NostrConnectMethod>(

View File

@ -255,7 +255,9 @@ class RelayScoreboardService {
const relayScoreboardService = new RelayScoreboardService();
relayScoreboardService.loadStats();
setTimeout(() => {
relayScoreboardService.loadStats();
}, 0);
setInterval(() => {
relayScoreboardService.saveStats();

View File

@ -12,7 +12,7 @@ import db from "./db";
import { nameOrPubkey } from "./user-metadata";
import { getEventCoordinate } from "../helpers/nostr/events";
import createDefer, { Deferred } from "../classes/deferred";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "./local-cache-relay";
import { LOCAL_CACHE_RELAY, LOCAL_CACHE_RELAY_ENABLED, localCacheRelay } from "./local-cache-relay";
type Pubkey = string;
type Relay = string;
@ -33,7 +33,7 @@ export function createCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
}
const RELAY_REQUEST_BATCH_TIME = 1000;
const RELAY_REQUEST_BATCH_TIME = 500;
/** This class is ued to batch requests to a single relay */
class ReplaceableEventRelayLoader {
@ -167,7 +167,9 @@ class ReplaceableEventLoaderService {
const current = sub.value;
if (!current || event.created_at > current.created_at) {
sub.next(event);
if (saveToCache) this.saveToCache(cord, event);
if (saveToCache) {
this.saveToCache(cord, event);
}
}
}
@ -223,6 +225,8 @@ class ReplaceableEventLoaderService {
this.dbLog(`Writing ${this.writeCacheQueue.size} events to database`);
const transaction = db.transaction("replaceableEvents", "readwrite");
for (const [cord, event] of this.writeCacheQueue) {
localCacheRelay.publish(event);
// TODO: remove this
transaction.objectStore("replaceableEvents").put({ addr: cord, event, created: dayjs().unix() });
}
this.writeCacheQueue.clear();
@ -234,6 +238,7 @@ class ReplaceableEventLoaderService {
this.writeToCacheThrottle();
}
/** @deprecated */
async pruneDatabaseCache() {
const keys = await db.getAllKeysFromIndex(
"replaceableEvents",
@ -255,7 +260,8 @@ class ReplaceableEventLoaderService {
const sub = this.events.get(cord);
const relayUrls = Array.from(relays);
if (localCacheRelayService.enabled) relayUrls.unshift(LOCAL_CACHE_RELAY);
// TODO: use localCacheRelay instead
if (LOCAL_CACHE_RELAY_ENABLED) relayUrls.unshift(LOCAL_CACHE_RELAY);
for (const relay of relayUrls) {
const request = this.loaders.get(relay).requestEvent(kind, pubkey, d);

View File

@ -1,4 +1,4 @@
import { nip04, getPublicKey, finishEvent } from "nostr-tools";
import { nip04, getPublicKey, finalizeEvent } from "nostr-tools";
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
import { Account } from "./account";
@ -6,6 +6,7 @@ import db from "./db";
import serialPortService from "./serial-port";
import amberSignerService from "./amber-signer";
import nostrConnectService from "./nostr-connect";
import { hexToBytes } from "@noble/hashes/utils";
const decryptedKeys = new Map<string, string>();
@ -54,7 +55,7 @@ class SigningService {
const encrypted = await window.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encode.encode(secKey));
// add key to cache
decryptedKeys.set(getPublicKey(secKey), secKey);
decryptedKeys.set(getPublicKey(hexToBytes(secKey)), secKey);
return {
secKey: encrypted,
@ -91,8 +92,8 @@ class SigningService {
switch (account.type) {
case "local": {
const secKey = await this.decryptSecKey(account);
const tmpDraft = { ...draft, pubkey: getPublicKey(secKey) };
const event = finishEvent(tmpDraft, secKey) as NostrEvent;
const tmpDraft = { ...draft, pubkey: getPublicKey(hexToBytes(secKey)) };
const event = finalizeEvent(tmpDraft, hexToBytes(secKey)) as NostrEvent;
return event;
}
case "extension":

View File

@ -5,36 +5,55 @@ import Subject from "../classes/subject";
import SuperMap from "../classes/super-map";
import { safeRelayUrls } from "../helpers/url";
import { NostrEvent } from "../types/nostr-event";
import localCacheRelayService, { LOCAL_CACHE_RELAY } from "./local-cache-relay";
import { localCacheRelay } from "./local-cache-relay";
import { relayRequest } from "../helpers/relay";
import { logger } from "../helpers/debug";
const RELAY_REQUEST_BATCH_TIME = 1000;
const RELAY_REQUEST_BATCH_TIME = 500;
class SingleEventService {
private cache = new SuperMap<string, Subject<NostrEvent>>(() => new Subject());
pending = new Map<string, string[]>();
log = logger.extend("SingleEvent");
requestEvent(id: string, relays: string[]) {
const subject = this.cache.get(id);
if (subject.value) return subject;
const newUrls = safeRelayUrls(relays);
if (localCacheRelayService.enabled) newUrls.push(LOCAL_CACHE_RELAY);
this.pending.set(id, this.pending.get(id)?.concat(newUrls) ?? newUrls);
this.batchRequestsThrottle();
return subject;
}
handleEvent(event: NostrEvent) {
handleEvent(event: NostrEvent, cache = true) {
this.cache.get(event.id).next(event);
if (cache) localCacheRelay.publish(event);
}
private batchRequestsThrottle = _throttle(this.batchRequests, RELAY_REQUEST_BATCH_TIME);
batchRequests() {
async batchRequests() {
if (this.pending.size === 0) return;
const ids = Array.from(this.pending.keys());
const loaded: string[] = [];
// load from cache relay
const fromCache = await relayRequest(localCacheRelay, [{ ids }]);
for (const e of fromCache) {
this.handleEvent(e, false);
loaded.push(e.id);
}
if (loaded.length > 0) this.log(`Loaded ${loaded.length} from cache instead of relays`);
const idsFromRelays: Record<string, string[]> = {};
for (const [id, relays] of this.pending) {
if (loaded.includes(id)) continue;
for (const relay of relays) {
idsFromRelays[relay] = idsFromRelays[relay] ?? [];
idsFromRelays[relay].push(id);

View File

@ -1,3 +1,5 @@
import { kinds } from "nostr-tools";
import { isPTag, NostrEvent } from "../types/nostr-event";
import { safeJson } from "../helpers/parse";
import SuperMap from "../classes/super-map";
@ -5,7 +7,6 @@ import Subject from "../classes/subject";
import { RelayConfig, RelayMode } from "../classes/relay";
import { normalizeRelayConfigs } from "../helpers/relay";
import replaceableEventLoaderService, { RequestOptions } from "./replaceable-event-requester";
import { Kind } from "nostr-tools";
export type UserContacts = {
pubkey: string;
@ -58,7 +59,7 @@ class UserContactsService {
requestContacts(pubkey: string, relays: string[], opts?: RequestOptions) {
const sub = this.subjects.get(pubkey);
const requestSub = replaceableEventLoaderService.requestEvent(relays, Kind.Contacts, pubkey, undefined, opts);
const requestSub = replaceableEventLoaderService.requestEvent(relays, kinds.Contacts, pubkey, undefined, opts);
sub.connectWithHandler(requestSub, (event, next) => next(parseContacts(event)));

View File

@ -1,5 +1,5 @@
import db from "./db";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import _throttle from "lodash.throttle";
import { NostrEvent } from "../types/nostr-event";
@ -26,7 +26,7 @@ class UserMetadataService {
}
requestMetadata(pubkey: string, relays: string[], opts: RequestOptions = {}) {
const sub = this.parsedSubjects.get(pubkey);
const requestSub = replaceableEventLoaderService.requestEvent(relays, Kind.Metadata, pubkey, undefined, opts);
const requestSub = replaceableEventLoaderService.requestEvent(relays, kinds.Metadata, pubkey, undefined, opts);
sub.connectWithHandler(requestSub, (event, next) => next(parseKind0Event(event)));
return sub;
}

View File

@ -1,4 +1,4 @@
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { isRTag, NostrEvent } from "../types/nostr-event";
import { RelayConfig } from "../classes/relay";
@ -30,7 +30,7 @@ class UserRelaysService {
}
requestRelays(pubkey: string, relays: string[], opts: RequestOptions = {}) {
const sub = this.subjects.get(pubkey);
const requestSub = replaceableEventLoaderService.requestEvent(relays, Kind.RelayList, pubkey, undefined, opts);
const requestSub = replaceableEventLoaderService.requestEvent(relays, kinds.RelayList, pubkey, undefined, opts);
sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event)));
// also fetch the relays from the users contacts
@ -49,9 +49,9 @@ class UserRelaysService {
const sub = this.subjects.get(pubkey);
// load from cache
await replaceableEventLoaderService.loadFromCache(createCoordinate(Kind.RelayList, pubkey));
await replaceableEventLoaderService.loadFromCache(createCoordinate(kinds.RelayList, pubkey));
const requestSub = replaceableEventLoaderService.getEvent(Kind.RelayList, pubkey);
const requestSub = replaceableEventLoaderService.getEvent(kinds.RelayList, pubkey);
sub.connectWithHandler(requestSub, (event, next) => next(parseRelaysEvent(event)));
}

View File

@ -1,3 +1,5 @@
import type { NostrEvent as NostrToolsNostrEvent } from "nostr-tools";
export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string];
export type ATag = ["a", string] | ["a", string, string] | ["e", string, string, string];
export type PTag = ["p", string] | ["p", string, string] | ["p", string, string, string];
@ -7,14 +9,8 @@ export type ExpirationTag = ["expiration", string];
export type EmojiTag = ["emoji", string, string];
export type Tag = string[] | ETag | PTag | RTag | DTag | ATag | ExpirationTag;
export type NostrEvent = {
id: string;
pubkey: string;
created_at: number;
kind: number;
export type NostrEvent = Omit<NostrToolsNostrEvent, "tags"> & {
tags: Tag[];
content: string;
sig: string;
};
export type CountResponse = {
count: number;

View File

@ -1,5 +1,5 @@
import { useNavigate } from "react-router-dom";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import {
Button,
Flex,
@ -89,7 +89,7 @@ function BadgeDetailsPage({ badge }: { badge: NostrEvent }) {
const coordinate = getEventCoordinate(badge);
const awardsTimeline = useTimelineLoader(`${coordinate}-awards`, readRelays, {
"#a": [coordinate],
kinds: [Kind.BadgeAward],
kinds: [kinds.BadgeAward],
});
if (!badge) return <Spinner />;

View File

@ -1,5 +1,5 @@
import { Flex, SimpleGrid } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
@ -19,7 +19,7 @@ function BadgesBrowsePage() {
const timeline = useTimelineLoader(
`${listId}-badges`,
readRelays,
filter ? { ...filter, kinds: [Kind.BadgeDefinition] } : undefined,
filter ? { ...filter, kinds: [kinds.BadgeDefinition] } : undefined,
);
const lists = useSubject(timeline.timeline);

View File

@ -1,6 +1,7 @@
import { memo, useRef } from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { ButtonGroup, Card, CardBody, CardHeader, CardProps, Flex, Heading, Image, Link, Text } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import UserAvatarLink from "../../../components/user-avatar-link";
import UserLink from "../../../components/user-link";
@ -12,7 +13,6 @@ import BadgeMenu from "./badge-menu";
import { getBadgeImage, getBadgeName } from "../../../helpers/nostr/badges";
import Timestamp from "../../../components/timestamp";
import useEventCount from "../../../hooks/use-event-count";
import { Kind } from "nostr-tools";
function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: NostrEvent }) {
const naddr = getSharableEventAddress(badge);
@ -23,7 +23,7 @@ function BadgeCard({ badge, ...props }: Omit<CardProps, "children"> & { badge: N
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, getEventUID(badge));
const timesAwarded = useEventCount({ kinds: [Kind.BadgeAward], "#a": [getEventCoordinate(badge)] });
const timesAwarded = useEventCount({ kinds: [kinds.BadgeAward], "#a": [getEventCoordinate(badge)] });
return (
<Card ref={ref} variant="outline" {...props}>

View File

@ -1,7 +1,7 @@
import { useCallback } from "react";
import { Button, Flex, Heading, Image, Link, Spacer } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { ExternalLinkIcon } from "../../components/icons";
import VerticalPageLayout from "../../components/vertical-page-layout";
@ -33,7 +33,7 @@ function BadgesPage() {
readRelays,
{
"#p": filter?.authors,
kinds: [Kind.BadgeAward],
kinds: [kinds.BadgeAward],
},
{ eventFilter },
);

View File

@ -1,7 +1,7 @@
import { memo, useCallback, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import useSingleEvent from "../../hooks/use-single-event";
import { ErrorBoundary } from "../../components/error-boundary";
@ -61,7 +61,7 @@ function ChannelPage({ channel }: { channel: NostrEvent }) {
`${channel.id}-chat-messages`,
relays,
{
kinds: [Kind.ChannelMessage],
kinds: [kinds.ChannelMessage],
"#e": [channel.id],
},
{ eventFilter },

View File

@ -1,7 +1,7 @@
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import dayjs from "dayjs";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { Button, Flex, FlexProps, Heading, useToast } from "@chakra-ui/react";
import { useSigningContext } from "../../../providers/global/signing-provider";
@ -40,7 +40,7 @@ export default function ChannelMessageForm({
if (!values.content) return;
let draft: DraftNostrEvent = {
kind: Kind.ChannelMessage,
kind: kinds.ChannelMessage,
content: values.content,
tags: [["e", channel.id]],
created_at: dayjs().unix(),

View File

@ -1,6 +1,6 @@
import { Kind, nip19 } from "nostr-tools";
import { Box, Card, CardBody, CardHeader, Flex, LinkBox, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { useCallback } from "react";
import { kinds } from "nostr-tools";
import { Flex } from "@chakra-ui/react";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/local/relay-selection-provider";
@ -11,7 +11,6 @@ import IntersectionObserverProvider from "../../providers/local/intersection-obs
import { NostrEvent } from "../../types/nostr-event";
import { ErrorBoundary } from "../../components/error-boundary";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
import { useCallback, useRef } from "react";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
@ -32,7 +31,7 @@ function ChannelsHomePage() {
const timeline = useTimelineLoader(
`${listId}-channels`,
relays,
filter ? { ...filter, kinds: [Kind.ChannelCreation] } : undefined,
filter ? { ...filter, kinds: [kinds.ChannelCreation] } : undefined,
{ eventFilter },
);
const channels = useSubject(timeline.timeline);

View File

@ -1,5 +1,5 @@
import dayjs from "dayjs";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { NostrEvent } from "../../../types/nostr-event";
import useEventCount from "../../../hooks/use-event-count";
@ -9,5 +9,5 @@ export default function useCountCommunityPosts(
community: NostrEvent,
since: number = dayjs().subtract(1, "month").unix(),
) {
return useEventCount({ "#a": [getEventCoordinate(community)], kinds: [Kind.Text], since });
return useEventCount({ "#a": [getEventCoordinate(community)], kinds: [kinds.ShortTextNote], since });
}

View File

@ -1,4 +1,3 @@
import { Kind } from "nostr-tools";
import { useMemo } from "react";
import {
Button,
@ -13,12 +12,12 @@ import {
Flex,
Heading,
Link,
SimpleGrid,
Switch,
Text,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { Navigate } from "react-router-dom";
import dayjs from "dayjs";
@ -110,7 +109,7 @@ function CommunitiesHomePage() {
readRelays,
communityCoordinates.length > 0
? {
kinds: [Kind.Text, Kind.Repost, COMMUNITY_APPROVAL_KIND],
kinds: [kinds.ShortTextNote, kinds.Repost, COMMUNITY_APPROVAL_KIND],
"#a": communityCoordinates.map((p) => createCoordinate(p.kind, p.pubkey, p.identifier)),
}
: undefined,

View File

@ -1,7 +1,7 @@
import { useContext } from "react";
import { Button, ButtonGroup, Divider, Flex, Heading, Text, useDisclosure } from "@chakra-ui/react";
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
import { Kind, nip19 } from "nostr-tools";
import { kinds, nip19 } from "nostr-tools";
import {
getCommunityRelays as getCommunityRelays,
@ -52,7 +52,7 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
const communityRelays = getCommunityRelays(community);
const readRelays = useReadRelayUrls(communityRelays);
const timeline = useTimelineLoader(`${getEventUID(community)}-timeline`, readRelays, {
kinds: [Kind.Text, Kind.Repost, COMMUNITY_APPROVAL_KIND],
kinds: [kinds.ShortTextNote, kinds.Repost, COMMUNITY_APPROVAL_KIND],
"#a": [communityCoordinate],
});

View File

@ -14,7 +14,7 @@ import {
} from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import dayjs from "dayjs";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { NostrEvent, isETag } from "../../../types/nostr-event";
import { getEventCommunityPointer, getPostSubject } from "../../../helpers/nostr/communities";
@ -175,9 +175,9 @@ export function CommunityRepostPost({
export default function CommunityPost({ event, ...props }: Omit<CardProps, "children"> & CommunityPostPropTypes) {
switch (event.kind) {
case Kind.Text:
case kinds.ShortTextNote:
return <CommunityTextPost event={event} {...props} />;
case Kind.Repost:
case kinds.Repost:
return <CommunityRepostPost event={event} {...props} />;
}
return null;

View File

@ -1,12 +1,11 @@
import { memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Button, ButtonGroup, Card, Flex, IconButton } from "@chakra-ui/react";
import { Kind, nip19 } from "nostr-tools";
import { UNSAFE_DataRouterContext, useLocation, useNavigate, useParams } from "react-router-dom";
import { UNSAFE_DataRouterContext, useLocation, useNavigate } from "react-router-dom";
import { kinds } from "nostr-tools";
import { ChevronLeftIcon, ThreadIcon } from "../../components/icons";
import UserAvatar from "../../components/user-avatar";
import UserLink from "../../components/user-link";
import { isHexKey } from "../../helpers/nip19";
import useSubject from "../../hooks/use-subject";
import RequireCurrentAccount from "../../providers/route/require-current-account";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -74,12 +73,12 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
const myInbox = useReadRelayUrls();
const timeline = useTimelineLoader(`${pubkey}-${account.pubkey}-messages`, myInbox, [
{
kinds: [Kind.EncryptedDirectMessage],
kinds: [kinds.EncryptedDirectMessage],
"#p": [account.pubkey],
authors: [pubkey],
},
{
kinds: [Kind.EncryptedDirectMessage],
kinds: [kinds.EncryptedDirectMessage],
"#p": [pubkey],
authors: [account.pubkey],
},

View File

@ -1,7 +1,7 @@
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import dayjs from "dayjs";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { Button, Flex, FlexProps, Heading, useToast } from "@chakra-ui/react";
import { useSigningContext } from "../../../providers/global/signing-provider";
@ -47,7 +47,7 @@ export default function SendMessageForm({
const encrypted = await requestEncrypt(values.content, pubkey);
const event: DraftNostrEvent = {
kind: Kind.EncryptedDirectMessage,
kind: kinds.EncryptedDirectMessage,
content: encrypted,
tags: [["p", pubkey]],
created_at: dayjs().unix(),

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo } from "react";
import { Flex, useDisclosure } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { isReply, isRepost } from "../../helpers/nostr/events";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -15,7 +15,7 @@ import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import NoteFilterTypeButtons from "../../components/note-filter-type-buttons";
import KindSelectionProvider, { useKindSelectionContext } from "../../providers/local/kind-selection-provider";
const defaultKinds = [Kind.Text, Kind.Repost, Kind.Article, Kind.RecommendRelay, Kind.BadgeAward];
const defaultKinds = [kinds.ShortTextNote, kinds.Repost, kinds.LongFormArticle, kinds.RecommendRelay, kinds.BadgeAward];
function HomePage() {
const showReplies = useDisclosure({ defaultIsOpen: localStorage.getItem("show-replies") === "true" });

View File

@ -1,6 +1,6 @@
import { Alert, AlertIcon, AlertTitle } from "@chakra-ui/react";
import { Navigate, useParams } from "react-router-dom";
import { Kind, nip19 } from "nostr-tools";
import { kinds, nip19 } from "nostr-tools";
import { STREAM_KIND } from "../../helpers/nostr/stream";
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
import { NOTE_LIST_KIND, PEOPLE_LIST_KIND } from "../../helpers/nostr/lists";
@ -36,11 +36,11 @@ function NostrLinkPage() {
if (decoded.data.kind === EMOJI_PACK_KIND) return <Navigate to={`/emojis/${cleanLink}`} replace />;
if (decoded.data.kind === NOTE_LIST_KIND) return <Navigate to={`/lists/${cleanLink}`} replace />;
if (decoded.data.kind === PEOPLE_LIST_KIND) return <Navigate to={`/lists/${cleanLink}`} replace />;
if (decoded.data.kind === Kind.BadgeDefinition) return <Navigate to={`/badges/${cleanLink}`} replace />;
if (decoded.data.kind === kinds.BadgeDefinition) return <Navigate to={`/badges/${cleanLink}`} replace />;
if (decoded.data.kind === COMMUNITY_DEFINITION_KIND) return <Navigate to={`/c/${cleanLink}`} replace />;
if (decoded.data.kind === FLARE_VIDEO_KIND) return <Navigate to={`/videos/${cleanLink}`} replace />;
if (decoded.data.kind === Kind.ChannelCreation) return <Navigate to={`/channels/${cleanLink}`} replace />;
if (decoded.data.kind === Kind.Text) return <Navigate to={`/n/${cleanLink}`} replace />;
if (decoded.data.kind === kinds.ChannelCreation) return <Navigate to={`/channels/${cleanLink}`} replace />;
if (decoded.data.kind === kinds.ShortTextNote) return <Navigate to={`/n/${cleanLink}`} replace />;
// if there is no kind redirect to the thread view
return <Navigate to={`/n/${cleanLink}`} replace />;
}

View File

@ -13,7 +13,7 @@ import {
SimpleGrid,
Text,
} from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import UserAvatarLink from "../../../components/user-avatar-link";
import UserLink from "../../../components/user-link";
@ -48,7 +48,7 @@ export function ListCardContent({ list, ...props }: Omit<CardProps, "children">
const notes = getEventPointersFromList(list);
const coordinates = getAddressPointersFromList(list);
const communities = coordinates.filter((cord) => cord.kind === COMMUNITY_DEFINITION_KIND);
const articles = coordinates.filter((cord) => cord.kind === Kind.Article);
const articles = coordinates.filter((cord) => cord.kind === kinds.LongFormArticle);
const references = getReferencesFromList(list);
return (

View File

@ -1,13 +1,13 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { NostrEvent } from "../../../types/nostr-event";
import { getEventCoordinate } from "../../../helpers/nostr/events";
import { PEOPLE_LIST_KIND } from "../../../helpers/nostr/lists";
export default function ListFeedButton({ list, ...props }: { list: NostrEvent } & Omit<ButtonProps, "children">) {
const shouldShowFeedButton = list.kind === PEOPLE_LIST_KIND || list.kind === Kind.Contacts;
const shouldShowFeedButton = list.kind === PEOPLE_LIST_KIND || list.kind === kinds.Contacts;
if (!shouldShowFeedButton) return null;

View File

@ -1,6 +1,6 @@
import { Button, Divider, Flex, Heading, Image, Link, SimpleGrid, Spacer, useDisclosure } from "@chakra-ui/react";
import { useNavigate, Link as RouterLink, Navigate } from "react-router-dom";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import useCurrentAccount from "../../hooks/use-current-account";
import { ExternalLinkIcon, PlusCircleIcon } from "../../components/icons";
@ -55,7 +55,7 @@ function ListsHomePage() {
Special lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="2">
<ListCard cord={`${Kind.Contacts}:${account.pubkey}`} hideCreator />
<ListCard cord={`${kinds.Contacts}:${account.pubkey}`} hideCreator />
<ListCard cord={`${MUTE_LIST_KIND}:${account.pubkey}`} hideCreator />
<ListCard cord={`${PIN_LIST_KIND}:${account.pubkey}`} hideCreator />
<ListCard cord={`${COMMUNITIES_LIST_KIND}:${account.pubkey}`} hideCreator />

View File

@ -1,5 +1,5 @@
import { useNavigate } from "react-router-dom";
import { Kind, nip19 } from "nostr-tools";
import { kinds, nip19 } from "nostr-tools";
import type { DecodeResult } from "nostr-tools/lib/types/nip19";
import { Box, Button, Flex, Heading, SimpleGrid, Spacer, Spinner, Text } from "@chakra-ui/react";
@ -49,7 +49,7 @@ function ListPage({ list }: { list: NostrEvent }) {
const notes = getEventPointersFromList(list);
const coordinates = getAddressPointersFromList(list);
const communities = coordinates.filter((cord) => cord.kind === COMMUNITY_DEFINITION_KIND);
const articles = coordinates.filter((cord) => cord.kind === Kind.Article);
const articles = coordinates.filter((cord) => cord.kind === kinds.LongFormArticle);
const references = getReferencesFromList(list);
return (

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { Box, Button, Flex } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import ngeohash from "ngeohash";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
@ -123,7 +123,7 @@ export default function MapView() {
const timeline = useTimelineLoader(
"geo-events",
readRelays,
cells.length > 0 ? { "#g": cells, kinds: [Kind.Text] } : undefined,
cells.length > 0 ? { "#g": cells, kinds: [kinds.ShortTextNote] } : undefined,
);
const setCellsFromMap = useCallback(() => {

View File

@ -1,5 +1,6 @@
import { Kind } from "nostr-tools";
import React from "react";
import { kinds } from "nostr-tools";
import { ErrorBoundary } from "../../components/error-boundary";
import useSubject from "../../hooks/use-subject";
import StreamNote from "../../components/timeline-page/generic-note-timeline/stream-note";
@ -10,7 +11,7 @@ import { NostrEvent } from "../../types/nostr-event";
const RenderEvent = React.memo(({ event, focused }: { event: NostrEvent; focused?: boolean }) => {
switch (event.kind) {
case Kind.Text:
case kinds.ShortTextNote:
return <Note event={event} variant={focused ? "elevated" : undefined} />;
case STREAM_KIND:
return <StreamNote event={event} />;

View File

@ -1,6 +1,6 @@
import { memo, useEffect, useMemo, useRef } from "react";
import { Button, ButtonGroup, Flex, useDisclosure } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import RequireCurrentAccount from "../../providers/route/require-current-account";
@ -61,15 +61,15 @@ const NotificationsTimeline = memo(
const filteredEvents = useMemo(
() =>
throttledEvents.filter((e) => {
if (peoplePubkeys && e.kind !== Kind.Zap && !peoplePubkeys.includes(e.pubkey)) return false;
if (peoplePubkeys && e.kind !== kinds.Zap && !peoplePubkeys.includes(e.pubkey)) return false;
if (e.kind === Kind.Text) {
if (e.kind === kinds.ShortTextNote) {
if (!showReplies && isReply(e)) return false;
if (!showMentions && !isReply(e)) return false;
}
if (!showReactions && e.kind === Kind.Reaction) return false;
if (!showReposts && e.kind === Kind.Repost) return false;
if (!showZaps && e.kind === Kind.Zap) return false;
if (!showReactions && e.kind === kinds.Reaction) return false;
if (!showReposts && e.kind === kinds.Repost) return false;
if (!showZaps && e.kind === kinds.Zap) return false;
return true;
}),

View File

@ -1,6 +1,6 @@
import { ReactNode, forwardRef, memo, useMemo, useRef } from "react";
import { AvatarGroup, Flex, IconButton, IconButtonProps, Text, useDisclosure } from "@chakra-ui/react";
import { Kind, nip18, nip25 } from "nostr-tools";
import { kinds, nip18, nip25 } from "nostr-tools";
import useCurrentAccount from "../../hooks/use-current-account";
import { NostrEvent, isATag, isETag } from "../../types/nostr-event";
@ -86,7 +86,7 @@ const ReactionNotification = forwardRef<HTMLDivElement, { event: NostrEvent }>((
if (!pointer || (account?.pubkey && pointer.author !== account.pubkey)) return null;
const reactedEvent = useSingleEvent(pointer.id, pointer.relays);
if (reactedEvent?.kind === Kind.EncryptedDirectMessage) return null;
if (reactedEvent?.kind === kinds.EncryptedDirectMessage) return null;
return (
<NotificationIconEntry ref={ref} icon={<Heart boxSize={8} color="red.400" />}>
@ -156,18 +156,18 @@ const NotificationItem = ({ event }: { event: NostrEvent }) => {
let content: ReactNode | null = null;
switch (event.kind) {
case Kind.Text:
case kinds.ShortTextNote:
case TORRENT_COMMENT_KIND:
case Kind.Article:
case kinds.LongFormArticle:
content = <NoteNotification event={event} ref={ref} />;
break;
case Kind.Reaction:
case kinds.Reaction:
content = <ReactionNotification event={event} ref={ref} />;
break;
case Kind.Repost:
case kinds.Repost:
content = <RepostNotification event={event} ref={ref} />;
break;
case Kind.Zap:
case kinds.Zap:
content = <ZapNotification event={event} ref={ref} />;
break;
default:

View File

@ -1,5 +1,5 @@
import { MouseEventHandler, useCallback, useMemo, useRef } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import useCurrentAccount from "../../hooks/use-current-account";
@ -29,7 +29,7 @@ import IntersectionObserverProvider, {
import { getEventUID } from "../../helpers/nostr/events";
import { useNavigateInDrawer } from "../../providers/drawer-sub-view-provider";
const THREAD_KINDS = [Kind.Text, TORRENT_COMMENT_KIND];
const THREAD_KINDS = [kinds.ShortTextNote, TORRENT_COMMENT_KIND];
function ReplyEntry({ event }: { event: NostrEvent }) {
const navigate = useNavigateInDrawer();

View File

@ -1,6 +1,6 @@
import { useCallback, useMemo } from "react";
import { useCallback } from "react";
import { Flex, Spacer, useDisclosure } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { isReply, isRepost } from "../../../helpers/nostr/events";
import { useAppTitle } from "../../../hooks/use-app-title";
@ -10,7 +10,6 @@ import TimelinePage, { useTimelinePageEventFilter } from "../../../components/ti
import TimelineViewTypeButtons from "../../../components/timeline-page/timeline-view-type";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import { usePeopleListContext } from "../../../providers/local/people-list-provider";
import { NostrRequestFilter } from "../../../types/nostr-query";
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
import NoteFilterTypeButtons from "../../../components/note-filter-type-buttons";
@ -20,7 +19,7 @@ export default function RelayNotes({ relay }: { relay: string }) {
const showReposts = useDisclosure({ defaultIsOpen: true });
const { filter } = usePeopleListContext();
const kinds = [Kind.Text];
const k = [kinds.ShortTextNote];
const timelineEventFilter = useTimelinePageEventFilter();
const muteFilter = useClientSideMuteFilter();
@ -33,7 +32,7 @@ export default function RelayNotes({ relay }: { relay: string }) {
},
[timelineEventFilter, showReplies.isOpen, showReposts.isOpen, muteFilter],
);
const timeline = useTimelineLoader(`${relay}-notes`, [relay], filter ? { ...filter, kinds } : undefined, {
const timeline = useTimelineLoader(`${relay}-notes`, [relay], filter ? { ...filter, kinds: k } : undefined, {
eventFilter,
});

View File

@ -1,4 +1,4 @@
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { useRelaySelectionRelays } from "../../providers/local/relay-selection-provider";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -14,7 +14,7 @@ export default function ArticleSearchResults({ search }: { search: string }) {
const timeline = useTimelineLoader(
`${listId ?? "global"}-${search}-article-search`,
searchRelays,
search ? { search: search, kinds: [Kind.Article], ...filter } : undefined,
search ? { search: search, kinds: [kinds.LongFormArticle], ...filter } : undefined,
);
const callback = useTimelineCurserIntersectionCallback(timeline);

View File

@ -1,4 +1,4 @@
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { useRelaySelectionRelays } from "../../providers/local/relay-selection-provider";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -14,7 +14,7 @@ export default function NoteSearchResults({ search }: { search: string }) {
const timeline = useTimelineLoader(
`${listId ?? "global"}-${search}-note-search`,
searchRelays,
search ? { search: search, kinds: [Kind.Text], ...filter } : undefined,
search ? { search: search, kinds: [kinds.ShortTextNote], ...filter } : undefined,
);
const callback = useTimelineCurserIntersectionCallback(timeline);

View File

@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { Box, Text } from "@chakra-ui/react";
import { useAsync } from "react-use";
import { kinds } from "nostr-tools";
import { NostrEvent } from "../../types/nostr-event";
import { parseKind0Event } from "../../helpers/user-metadata";
@ -13,7 +14,6 @@ import UserLink from "../../components/user-link";
import trustedUserStatsService, { NostrBandUserStats } from "../../services/trusted-user-stats";
import { useRelaySelectionRelays } from "../../providers/local/relay-selection-provider";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { Kind } from "nostr-tools";
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
@ -57,7 +57,7 @@ export default function ProfileSearchResults({ search }: { search: string }) {
const timeline = useTimelineLoader(
`${listId ?? "global"}-${search}-profile-search`,
searchRelays,
search ? { search: search, kinds: [Kind.Metadata], ...filter } : undefined,
search ? { search: search, kinds: [kinds.Metadata], ...filter } : undefined,
);
const profiles = useSubject(timeline?.timeline) ?? [];

View File

@ -9,47 +9,25 @@ import {
ButtonGroup,
Text,
} from "@chakra-ui/react";
import db, { clearCacheData, deleteDatabase } from "../../services/db";
import { DatabaseIcon } from "../../components/icons";
import { useAsync } from "react-use";
import { countEvents, countEventsByKind } from "nostr-idb";
// copied from https://stackoverflow.com/a/39906526
const units = ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
function niceBytes(x: number) {
let l = 0,
n = x || 0;
while (n >= 1024 && ++l) {
n = n / 1024;
}
return n.toFixed(n < 10 && l > 0 ? 1 : 0) + " " + units[l];
}
import { clearCacheData, deleteDatabase } from "../../services/db";
import { DatabaseIcon } from "../../components/icons";
import { localCacheDatabase } from "../../services/local-cache-relay";
function DatabaseStats() {
const { value: estimatedStorage } = useAsync(async () => await window.navigator?.storage?.estimate?.(), []);
const { value: replaceableEventCount } = useAsync(async () => {
const keys = await db.getAllKeys("replaceableEvents");
return keys.length;
}, []);
const { value: relayInfoCount } = useAsync(async () => {
const keys = await db.getAllKeys("relayInfo");
return keys.length;
}, []);
const { value: nip05Count } = useAsync(async () => {
const keys = await db.getAllKeys("dnsIdentifiers");
return keys.length;
}, []);
const { value: count } = useAsync(async () => await countEvents(localCacheDatabase), []);
const { value: kinds } = useAsync(async () => await countEventsByKind(localCacheDatabase), []);
return (
<>
<Text>{replaceableEventCount} cached replaceable events</Text>
<Text>{relayInfoCount} cached relay info</Text>
<Text>{nip05Count} cached NIP-05 IDs</Text>
{estimatedStorage ? (
<Text>
{niceBytes(estimatedStorage?.usage ?? 0)} / {niceBytes(estimatedStorage?.quota ?? 0)} Used
</Text>
) : null}
<Text>{count} cached events</Text>
<Text>
{Object.entries(kinds || {})
.map(([kind, count]) => `${kind} (${count})`)
.join(", ")}
</Text>
</>
);
}
@ -82,7 +60,7 @@ export default function DatabaseSettings() {
</h2>
<AccordionPanel>
<DatabaseStats />
<ButtonGroup>
<ButtonGroup mt="2">
<Button onClick={handleClearData} isLoading={clearing} isDisabled={clearing}>
Clear cache data
</Button>

View File

@ -15,13 +15,15 @@ import {
InputRightElement,
Link,
} from "@chakra-ui/react";
import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
import { useNavigate } from "react-router-dom";
import { RelayUrlInput } from "../../components/relay-url-input";
import { isHex, normalizeToHexPubkey, safeDecode } from "../../helpers/nip19";
import { isHex, safeDecode } from "../../helpers/nip19";
import accountService from "../../services/account";
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
import signingService from "../../services/signing";
import { COMMON_CONTACT_RELAY } from "../../const";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
export default function LoginNsecView() {
const navigate = useNavigate();
@ -36,9 +38,9 @@ export default function LoginNsecView() {
const [npub, setNpub] = useState("");
const generateNewKey = useCallback(() => {
const hex = generatePrivateKey();
const hex = generateSecretKey();
const pubkey = getPublicKey(hex);
setHexKey(hex);
setHexKey(bytesToHex(hex));
setInputValue(nip19.nsecEncode(hex));
setNpub(nip19.npubEncode(pubkey));
setShow(true);
@ -53,11 +55,11 @@ export default function LoginNsecView() {
if (isHex(e.target.value)) hex = e.target.value;
else {
const decode = safeDecode(e.target.value);
if (decode && decode.type === "nsec") hex = decode.data;
if (decode && decode.type === "nsec") hex = bytesToHex(decode.data);
}
if (hex) {
const pubkey = getPublicKey(hex);
const pubkey = getPublicKey(hexToBytes(hex));
setHexKey(hex);
setNpub(nip19.npubEncode(pubkey));
setError(false);
@ -75,7 +77,7 @@ export default function LoginNsecView() {
e.preventDefault();
if (!hexKey) return;
const pubkey = getPublicKey(hexKey);
const pubkey = getPublicKey(hexToBytes(hexKey));
const encrypted = await signingService.encryptSecKey(hexKey);
accountService.addAccount({ type: "local", pubkey, relays: [relayUrl], ...encrypted, readonly: false });

View File

@ -15,6 +15,7 @@ import { containerProps } from "./common";
import { CopyIconButton } from "../../components/copy-icon-button";
import styled from "@emotion/styled";
import { useState } from "react";
import { hexToBytes } from "@noble/hashes/utils";
const Blockquote = styled.figure`
padding: var(--chakra-sizes-2) var(--chakra-sizes-4);
@ -48,7 +49,7 @@ const Blockquote = styled.figure`
`;
export default function BackupStep({ secretKey, onConfirm }: { secretKey: string; onConfirm: () => void }) {
const nsec = nip19.nsecEncode(secretKey);
const nsec = nip19.nsecEncode(hexToBytes(secretKey));
const [confirmed, setConfirmed] = useState(false);
const [last4, setLast4] = useState("");

View File

@ -1,10 +1,11 @@
import { useEffect, useState } from "react";
import { generatePrivateKey, finishEvent, Kind, getPublicKey } from "nostr-tools";
import { Avatar, Box, Button, Flex, Heading, Text, useToast } from "@chakra-ui/react";
import { getPublicKey, generateSecretKey, finalizeEvent, kinds } from "nostr-tools";
import { Avatar, Button, Flex, Heading, Text, useToast } from "@chakra-ui/react";
import { bytesToHex } from "@noble/hashes/utils";
import dayjs from "dayjs";
import { Kind0ParsedContent } from "../../helpers/user-metadata";
import { containerProps } from "./common";
import dayjs from "dayjs";
import { nostrBuildUploadImage } from "../../helpers/nostr-build";
import NostrPublishAction from "../../classes/nostr-publish-action";
import accountService from "../../services/account";
@ -41,18 +42,18 @@ export default function CreateStep({
const createProfile = async () => {
setLoading(true);
try {
const hex = generatePrivateKey();
const hex = generateSecretKey();
const uploaded = profileImage
? await nostrBuildUploadImage(profileImage, async (draft) => finishEvent(draft, hex))
? await nostrBuildUploadImage(profileImage, async (draft) => finalizeEvent(draft, hex))
: undefined;
// create profile
const kind0 = finishEvent(
const kind0 = finalizeEvent(
{
content: JSON.stringify({ ...metadata, picture: uploaded?.url }),
created_at: dayjs().unix(),
kind: Kind.Metadata,
kind: kinds.Metadata,
tags: [],
},
hex,
@ -62,14 +63,14 @@ export default function CreateStep({
// login
const pubkey = getPublicKey(hex);
const encrypted = await signingService.encryptSecKey(hex);
const encrypted = await signingService.encryptSecKey(bytesToHex(hex));
accountService.addAccount({ type: "local", pubkey, relays, ...encrypted, readonly: false });
accountService.switchAccount(pubkey);
// set relays
await clientRelaysService.postUpdatedRelays(relays.map((url) => ({ url, mode: RelayMode.ALL })));
onSubmit(hex);
onSubmit(bytesToHex(hex));
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
}

View File

@ -1,10 +1,7 @@
import { useContext } from "react";
import { Button, ButtonProps } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import dayjs from "dayjs";
import { getSharableEventAddress } from "../../../helpers/nip19";
import { DraftNostrEvent } from "../../../types/nostr-event";
import { PostModalContext } from "../../../providers/route/post-modal-provider";
import { RepostIcon } from "../../../components/icons";
import { ParsedStream } from "../../../helpers/nostr/stream";

View File

@ -1,6 +1,6 @@
import { memo } from "react";
import { Flex } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import useSubject from "../../../hooks/use-subject";
import useStreamChatTimeline from "../stream/stream-chat/use-stream-chat-timeline";
@ -15,7 +15,7 @@ function ZapsCard({ stream }: { stream: ParsedStream }) {
const zapMessages = streamChatTimeline.events.getSortedEvents().filter((event) => {
if (stream.starts && event.created_at < stream.starts) return false;
if (stream.ends && event.created_at > stream.ends) return false;
if (event.kind !== Kind.Zap) return false;
if (event.kind !== kinds.Zap) return false;
return true;
});

View File

@ -1,5 +1,5 @@
import { useCallback, useMemo } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { getEventUID } from "../../../../helpers/nostr/events";
import { ParsedStream, STREAM_CHAT_MESSAGE_KIND, getATag } from "../../../../helpers/nostr/stream";
@ -30,14 +30,14 @@ export default function useStreamChatTimeline(stream: ParsedStream) {
const query = useMemo(() => {
const streamQuery: NostrQuery = {
"#a": [getATag(stream)],
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
kinds: [STREAM_CHAT_MESSAGE_KIND, kinds.Zap],
};
if (goal) {
return [
streamQuery,
// also get zaps to goal
{ "#e": [goal.id], kinds: [Kind.Zap] },
{ "#e": [goal.id], kinds: [kinds.Zap] },
];
}
return streamQuery;

View File

@ -2,7 +2,7 @@ import { useCallback, useMemo, useRef, useState } from "react";
import { Box, Button, ButtonGroup, Flex, IconButton, VisuallyHiddenInput, useToast } from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import { useThrottle } from "react-use";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import dayjs from "dayjs";
import { NostrEvent } from "../../../types/nostr-event";
@ -34,7 +34,7 @@ export type ReplyFormProps = {
onSubmitted?: (event: NostrEvent) => void;
};
export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = Kind.Text }: ReplyFormProps) {
export default function ReplyForm({ item, onCancel, onSubmitted, replyKind = kinds.ShortTextNote }: ReplyFormProps) {
const toast = useToast();
const account = useCurrentAccount();
const emojis = useContextEmojis();

View File

@ -1,6 +1,6 @@
import { Button, Flex } from "@chakra-ui/react";
import { memo, useCallback, useRef } from "react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import VerticalPageLayout from "../../components/vertical-page-layout";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
@ -49,11 +49,11 @@ export function DMTimelinePage() {
? [
{
...filter,
kinds: [Kind.EncryptedDirectMessage],
kinds: [kinds.EncryptedDirectMessage],
},
{ "#p": filter.authors, kinds: [Kind.EncryptedDirectMessage] },
{ "#p": filter.authors, kinds: [kinds.EncryptedDirectMessage] },
]
: { kinds: [Kind.EncryptedDirectMessage] },
: { kinds: [kinds.EncryptedDirectMessage] },
{ eventFilter },
);

View File

@ -2,8 +2,9 @@ import { useEffect, useMemo, useState } from "react";
import { Box, Button, Flex, Input, Text } from "@chakra-ui/react";
import AutoSizer from "react-virtualized-auto-sizer";
import ForceGraph, { LinkObject, NodeObject } from "react-force-graph-3d";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import dayjs from "dayjs";
import { useNavigate } from "react-router-dom";
import {
Group,
Mesh,
@ -29,7 +30,6 @@ import RelaySelectionButton from "../../components/relay-selection/relay-selecti
import { useDebounce } from "react-use";
import useSubject from "../../hooks/use-subject";
import { ChevronLeftIcon } from "../../components/icons";
import { useNavigate } from "react-router-dom";
type NodeType = { id: string; image?: string; name?: string };
@ -57,7 +57,7 @@ function NetworkDMGraphPage() {
request.onEvent.subscribe(store.addEvent, store);
request.start({
authors: contactsPubkeys,
kinds: [Kind.EncryptedDirectMessage],
kinds: [kinds.EncryptedDirectMessage],
since,
until,
});

View File

@ -2,6 +2,8 @@
// Also this can be used as a way of discovering apps when NIP-89 is implemented
import { Button, Flex } from "@chakra-ui/react";
import { memo, useCallback, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { kinds } from "nostr-tools";
import VerticalPageLayout from "../../components/vertical-page-layout";
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
@ -15,11 +17,9 @@ import IntersectionObserverProvider, {
import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../../types/nostr-event";
import { ChevronLeftIcon } from "../../components/icons";
import { useNavigate } from "react-router-dom";
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
import { getEventUID } from "../../helpers/nostr/events";
import { EmbedEvent } from "../../components/embed-event";
import { Kind } from "nostr-tools";
import { STREAM_CHAT_MESSAGE_KIND, STREAM_KIND } from "../../helpers/nostr/stream";
import {
BOOKMARK_LIST_KIND,
@ -42,19 +42,20 @@ const UnknownEvent = memo(({ event }: { event: NostrEvent }) => {
});
const commonTimelineKinds = [
Kind.Text,
Kind.Article,
Kind.Repost,
Kind.Reaction,
Kind.BadgeAward,
Kind.BadgeDefinition,
kinds.ShortTextNote,
kinds.LongFormArticle,
kinds.Repost,
kinds.Reaction,
kinds.BadgeAward,
kinds.BadgeDefinition,
STREAM_KIND,
Kind.Contacts,
Kind.Metadata,
Kind.EncryptedDirectMessage,
kinds.Contacts,
kinds.Metadata,
kinds.EncryptedDirectMessage,
MUTE_LIST_KIND,
STREAM_CHAT_MESSAGE_KIND,
Kind.EventDeletion,
kinds.EventDeletion,
kinds.CommunityPostApproval,
BOOKMARK_LIST_KIND,
BOOKMARK_LIST_SET_KIND,
PEOPLE_LIST_KIND,

View File

@ -1,7 +1,8 @@
import { ChangeEventHandler, useCallback, useMemo, useState } from "react";
import { Alert, Button, Flex, Spacer, Table, TableContainer, Tbody, Th, Thead, Tr, useToast } from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { generatePrivateKey, getPublicKey } from "nostr-tools";
import { generateSecretKey, getPublicKey } from "nostr-tools";
import { bytesToHex } from "@noble/hashes/utils";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import VerticalPageLayout from "../../components/vertical-page-layout";
@ -32,8 +33,8 @@ function Warning() {
const createAnonAccount = async () => {
setLoading(true);
try {
const secKey = generatePrivateKey();
const encrypted = await signingService.encryptSecKey(secKey);
const secKey = generateSecretKey();
const encrypted = await signingService.encryptSecKey(bytesToHex(secKey));
const pubkey = getPublicKey(secKey);
accountService.addAccount({ type: "local", ...encrypted, pubkey, readonly: false });
accountService.switchAccount(pubkey);

View File

@ -14,7 +14,7 @@ import {
Text,
} from "@chakra-ui/react";
import { useAsync } from "react-use";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { readablizeSats } from "../../../helpers/bolt11";
import trustedUserStatsService from "../../../services/trusted-user-stats";
@ -29,7 +29,7 @@ export default function UserStatsAccordion({ pubkey }: { pubkey: string }) {
const contacts = useUserContactList(pubkey, contextRelays);
const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(pubkey), [pubkey]);
const followerCount = useEventCount({ "#p": [pubkey], kinds: [Kind.Contacts] });
const followerCount = useEventCount({ "#p": [pubkey], kinds: [kinds.Contacts] });
return (
<Accordion allowMultiple>

View File

@ -1,5 +1,5 @@
import { useOutletContext } from "react-router-dom";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -16,7 +16,7 @@ export default function UserArticlesTab() {
const timeline = useTimelineLoader(pubkey + "-articles", readRelays, {
authors: [pubkey],
kinds: [Kind.Article],
kinds: [kinds.LongFormArticle],
});
const articles = useSubject(timeline.timeline);

View File

@ -1,6 +1,6 @@
import { useRef } from "react";
import { Flex, Text } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { useOutletContext } from "react-router-dom";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -69,9 +69,9 @@ export default function UserDMsTab() {
const timeline = useTimelineLoader(pubkey + "-articles", readRelays, [
{
authors: [pubkey],
kinds: [Kind.EncryptedDirectMessage],
kinds: [kinds.EncryptedDirectMessage],
},
{ "#p": [pubkey], kinds: [Kind.EncryptedDirectMessage] },
{ "#p": [pubkey], kinds: [kinds.EncryptedDirectMessage] },
]);
const dms = useSubject(timeline.timeline);

View File

@ -1,6 +1,6 @@
import { Flex, SimpleGrid } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { Event, Kind } from "nostr-tools";
import { Event, kinds } from "nostr-tools";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -33,7 +33,7 @@ export default function UserFollowersTab() {
const timeline = useTimelineLoader(`${pubkey}-followers`, readRelays, {
"#p": [pubkey],
kinds: [Kind.Contacts],
kinds: [kinds.Contacts],
});
const lists = useSubject(timeline.timeline);

View File

@ -25,12 +25,11 @@ import {
Tabs,
useDisclosure,
} from "@chakra-ui/react";
import { Kind, nip19 } from "nostr-tools";
import { kinds } from "nostr-tools";
import { Outlet, useMatches, useNavigate, useParams } from "react-router-dom";
import { Outlet, useMatches, useNavigate } from "react-router-dom";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { isHexKey } from "../../helpers/nip19";
import { useAppTitle } from "../../hooks/use-app-title";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
@ -98,7 +97,7 @@ const UserView = () => {
"wss://relay.stemstr.app",
...readRelays,
]);
const hasArticles = useEventExists({ kinds: [Kind.Article], authors: [pubkey] }, readRelays);
const hasArticles = useEventExists({ kinds: [kinds.LongFormArticle], authors: [pubkey] }, readRelays);
const hasStreams = useEventExists({ kinds: [STREAM_KIND], authors: [pubkey] }, [
"wss://relay.snort.social",
"wss://nos.lol",

View File

@ -1,6 +1,7 @@
import { useCallback } from "react";
import { useOutletContext } from "react-router-dom";
import { Divider, Heading, SimpleGrid } from "@chakra-ui/react";
import { Heading, SimpleGrid } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
import useTimelineLoader from "../../hooks/use-timeline-loader";
@ -17,7 +18,6 @@ import { getEventUID } from "../../helpers/nostr/events";
import ListCard from "../lists/components/list-card";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { Kind } from "nostr-tools";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { NostrEvent } from "../../types/nostr-event";
import UserName from "../../components/user-name";
@ -59,7 +59,7 @@ export default function UserListsTab() {
Special lists
</Heading>
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2">
<ListCard cord={`${Kind.Contacts}:${pubkey}`} hideCreator />
<ListCard cord={`${kinds.Contacts}:${pubkey}`} hideCreator />
<ListCard cord={`${MUTE_LIST_KIND}:${pubkey}`} hideCreator />
<ListCard cord={`${PIN_LIST_KIND}:${pubkey}`} hideCreator />
<ListCard cord={`${BOOKMARK_LIST_KIND}:${pubkey}`} hideCreator />

View File

@ -1,7 +1,7 @@
import { useCallback } from "react";
import { Flex, Spacer } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { isReply, isRepost, truncatedId } from "../../helpers/nostr/events";
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
@ -35,7 +35,7 @@ export default function UserNotesTab() {
readRelays,
{
authors: [pubkey],
kinds: [Kind.Text, Kind.Repost, Kind.Article, STREAM_KIND, 2],
kinds: [kinds.ShortTextNote, kinds.Repost, kinds.LongFormArticle, STREAM_KIND, 2],
},
{ eventFilter },
);

View File

@ -1,7 +1,7 @@
import { useRef } from "react";
import { Flex, Text } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { Kind } from "nostr-tools";
import { kinds } from "nostr-tools";
import { NoteLink } from "../../components/note-link";
import UserLink from "../../components/user-link";
@ -55,11 +55,11 @@ export default function UserReportsTab() {
const timeline = useTimelineLoader(`${pubkey}-reports`, contextRelays, [
{
authors: [pubkey],
kinds: [Kind.Report],
kinds: [kinds.Report],
},
{
"#p": [pubkey],
kinds: [Kind.Report],
kinds: [kinds.Report],
},
]);

View File

@ -2376,6 +2376,13 @@
dependencies:
"@noble/hashes" "1.3.1"
"@noble/curves@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35"
integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
dependencies:
"@noble/hashes" "1.3.2"
"@noble/curves@^1.0.0", "@noble/curves@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e"
@ -2388,6 +2395,11 @@
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==
"@noble/hashes@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
"@noble/hashes@1.3.3", "@noble/hashes@^1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1", "@noble/hashes@~1.3.2":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699"
@ -5250,6 +5262,14 @@ normalize-package-data@^2.5.0:
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
nostr-idb@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/nostr-idb/-/nostr-idb-0.2.0.tgz#50b74b078333d187be871c3d9086dfaebfa92183"
integrity sha512-BLqLemCzGR88Wa2gVPobmsdWondpDvbMwSgPsCjZlvflQNXpmCXCN1xJbq0j+RyC88guciW3D6yj7+477xg/9g==
dependencies:
idb "^8.0.0"
nostr-tools "^1.17.0"
nostr-tools@^1.17.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.17.0.tgz#b6f62e32fedfd9e68ec0a7ce57f74c44fc768e8c"
@ -5262,6 +5282,25 @@ nostr-tools@^1.17.0:
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
nostr-tools@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.1.3.tgz#424a0dcf329862163ec8914b7c0139f3bc26d70f"
integrity sha512-WqfX4A9aVJhyO2Mu4sL0YqjnGRu9hfSKWPjO3WU4lcdhVkDB2EYoHtwIYQoJEYCjGlyMIlZpVoiTn+eHGeJQSQ==
dependencies:
"@noble/ciphers" "0.2.0"
"@noble/curves" "1.2.0"
"@noble/hashes" "1.3.1"
"@scure/base" "1.1.1"
"@scure/bip32" "1.3.1"
"@scure/bip39" "1.2.1"
optionalDependencies:
nostr-wasm v0.1.0
nostr-wasm@v0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
integrity sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"