cleanup helps and components

This commit is contained in:
hzrd149
2023-08-23 16:42:09 -05:00
parent 0e702e4e97
commit e26b7e0db1
68 changed files with 422 additions and 295 deletions

View File

@@ -38,6 +38,7 @@ import RelayView from "./views/relays/relay";
import RelayReviewsView from "./views/relays/reviews";
import ListsView from "./views/lists";
import ListView from "./views/lists/list";
import UserListsTab from "./views/user/lists";
const StreamsView = React.lazy(() => import("./views/streams"));
const StreamView = React.lazy(() => import("./views/streams/stream"));
@@ -96,6 +97,7 @@ const router = createHashRouter([
{ path: "streams", element: <UserStreamsTab /> },
{ path: "zaps", element: <UserZapsTab /> },
{ path: "likes", element: <UserLikesTab /> },
{ path: "lists", element: <UserListsTab /> },
{ path: "followers", element: <UserFollowersTab /> },
{ path: "following", element: <UserFollowingTab /> },
{ path: "relays", element: <UserRelaysTab /> },

View File

@@ -1,4 +1,4 @@
import { getEventUID } from "../helpers/nostr/event";
import { getEventUID } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
import Subject from "./subject";

View File

@@ -1,4 +1,4 @@
import { getReferences } from "../helpers/nostr/event";
import { getReferences } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
import { NostrRequest } from "./nostr-request";
import { NostrMultiSubscription } from "./nostr-multi-subscription";

View File

@@ -1,11 +1,11 @@
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Flex } from "@chakra-ui/react";
import { ModalProps } from "@chakra-ui/react";
import { Bech32Prefix, hexToBech32 } from "../../helpers/nip19";
import { getReferences } from "../../helpers/nostr/event";
import { getReferences } from "../../helpers/nostr/events";
import { NostrEvent } from "../../types/nostr-event";
import RawJson from "./raw-json";
import RawValue from "./raw-value";
import RawPre from "./raw-pre";
import { nip19 } from "nostr-tools";
export default function NoteDebugModal({ event, ...props }: { event: NostrEvent } & Omit<ModalProps, "children">) {
return (
@@ -16,7 +16,7 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent
<ModalBody p="4">
<Flex gap="2" direction="column">
<RawValue heading="Event Id" value={event.id} />
<RawValue heading="Encoded id (NIP-19)" value={hexToBech32(event.id, Bech32Prefix.Note) ?? "failed"} />
<RawValue heading="Encoded id (NIP-19)" value={nip19.noteEncode(event.id)} />
<RawPre heading="Content" value={event.content} />
<RawJson heading="JSON" json={event} />
<RawJson heading="References" json={getReferences(event)} />

View File

@@ -1,17 +1,15 @@
import { useMemo } from "react";
import { Flex, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay } from "@chakra-ui/react";
import { ModalProps } from "@chakra-ui/react";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import RawValue from "./raw-value";
import RawJson from "./raw-json";
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import { Kind } from "nostr-tools";
import { Kind, nip19 } from "nostr-tools";
export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit<ModalProps, "children">) {
const npub = useMemo(() => normalizeToBech32(pubkey, Bech32Prefix.Pubkey), [pubkey]);
const npub = nip19.npubEncode(pubkey);
const metadata = useUserMetadata(pubkey);
const nprofile = useSharableProfileId(pubkey);
const relays = replaceableEventLoaderService.getEvent(Kind.RelayList, pubkey).value;

View File

@@ -1,13 +1,17 @@
import { NostrEvent } from "../types/nostr-event";
import { memo } from "react";
import { verifySignature } from "nostr-tools";
import { useMemo } from "react";
import { NostrEvent } from "../types/nostr-event";
import { CheckIcon, VerificationFailed } from "./icons";
import useAppSettings from "../hooks/use-app-settings";
export default function EventVerificationIcon({ event }: { event: NostrEvent }) {
const valid = useMemo(() => verifySignature(event), [event]);
function EventVerificationIcon({ event }: { event: NostrEvent }) {
const { showSignatureVerification } = useAppSettings();
if (!showSignatureVerification) return null;
if (!valid) {
if (!verifySignature(event)) {
return <VerificationFailed color="red.500" />;
}
return <CheckIcon color="green.500" />;
}
export default memo(EventVerificationIcon);

View File

@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { Link, LinkProps } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { truncatedId } from "../helpers/nostr/event";
import { truncatedId } from "../helpers/nostr/events";
import { nip19 } from "nostr-tools";
import { getSharableNoteId } from "../helpers/nip19";

View File

@@ -3,7 +3,7 @@ import { IconButton } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import { QuoteRepostIcon } from "../../icons";
import { PostModalContext } from "../../../providers/post-modal-provider";
import { buildQuoteRepost } from "../../../helpers/nostr/event";
import { buildQuoteRepost } from "../../../helpers/nostr/events";
import { useCurrentAccount } from "../../../hooks/use-current-account";
export function QuoteRepostButton({ event }: { event: NostrEvent }) {

View File

@@ -13,7 +13,10 @@ import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import { LikeIcon } from "../../icons";
import NostrPublishAction from "../../../classes/nostr-publish-action";
export default function ReactionButton({ note, ...props }: { note: NostrEvent } & Omit<ButtonProps, "children">) {
export default function ReactionButton({
event: note,
...props
}: { event: NostrEvent } & Omit<ButtonProps, "children">) {
const { requestSignature } = useSigningContext();
const account = useCurrentAccount();

View File

@@ -3,7 +3,7 @@ import { IconButton } from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import { ReplyIcon } from "../../icons";
import { PostModalContext } from "../../../providers/post-modal-provider";
import { buildReply } from "../../../helpers/nostr/event";
import { buildReply } from "../../../helpers/nostr/events";
import { useCurrentAccount } from "../../../hooks/use-current-account";
export function ReplyButton({ event }: { event: NostrEvent }) {

View File

@@ -14,7 +14,7 @@ import {
} from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import { RepostIcon } from "../../icons";
import { buildRepost } from "../../../helpers/nostr/event";
import { buildRepost } from "../../../helpers/nostr/events";
import { useCurrentAccount } from "../../../hooks/use-current-account";
import clientRelaysService from "../../../services/client-relays";
import signingService from "../../../services/signing";

View File

@@ -13,27 +13,27 @@ import { TrustProvider } from "../../providers/trust";
import { NoteLink } from "../note-link";
import { ArrowDownSIcon, ArrowUpSIcon } from "../icons";
export default function EmbeddedNote({ note }: { note: NostrEvent }) {
export default function EmbeddedNote({ event }: { event: NostrEvent }) {
const { showSignatureVerification } = useSubject(appSettings);
const expand = useDisclosure();
return (
<TrustProvider event={note}>
<TrustProvider event={event}>
<Card variant="outline">
<CardHeader padding="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
<UserAvatarLink pubkey={note.pubkey} size="sm" />
<UserLink pubkey={note.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
<UserDnsIdentityIcon pubkey={note.pubkey} onlyIcon />
<UserAvatarLink pubkey={event.pubkey} size="sm" />
<UserLink pubkey={event.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Button size="sm" onClick={expand.onToggle} leftIcon={expand.isOpen ? <ArrowUpSIcon /> : <ArrowDownSIcon />}>
Expand
</Button>
<Spacer />
{showSignatureVerification && <EventVerificationIcon event={note} />}
<NoteLink noteId={note.id} color="current" whiteSpace="nowrap">
{dayjs.unix(note.created_at).fromNow()}
{showSignatureVerification && <EventVerificationIcon event={event} />}
<NoteLink noteId={event.id} color="current" whiteSpace="nowrap">
{dayjs.unix(event.created_at).fromNow()}
</NoteLink>
</CardHeader>
<CardBody p="0">{expand.isOpen && <NoteContents px="2" event={note} />}</CardBody>
<CardBody p="0">{expand.isOpen && <NoteContents px="2" event={event} />}</CardBody>
</Card>
</TrustProvider>
);

View File

@@ -70,8 +70,8 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
<ButtonGroup size="sm" variant="link">
<RepostButton event={event} />
<QuoteRepostButton event={event} />
<NoteZapButton note={event} size="sm" />
{showReactions && <ReactionButton note={event} size="sm" />}
<NoteZapButton event={event} size="sm" />
{showReactions && <ReactionButton event={event} size="sm" />}
</ButtonGroup>
<Box flexGrow={1} />
{externalLink && (

View File

@@ -1,8 +1,9 @@
import { useCallback } from "react";
import { MenuItem, useDisclosure } from "@chakra-ui/react";
import { useCopyToClipboard } from "react-use";
import { nip19 } from "nostr-tools";
import { Bech32Prefix, getSharableNoteId, normalizeToBech32 } from "../../helpers/nip19";
import { getSharableNoteId } from "../../helpers/nip19";
import { NostrEvent } from "../../types/nostr-event";
import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
@@ -10,16 +11,10 @@ import { ClipboardIcon, CodeIcon, ExternalLinkIcon, LikeIcon, RelayIcon, RepostI
import NoteReactionsModal from "./note-zaps-modal";
import NoteDebugModal from "../debug-modals/note-debug-modal";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { useCallback, useState } from "react";
import QuoteNote from "./quote-note";
import { buildDeleteEvent } from "../../helpers/nostr/event";
import signingService from "../../services/signing";
import { buildAppSelectUrl } from "../../helpers/nostr-apps";
import { buildAppSelectUrl } from "../../helpers/nostr/apps";
import { useDeleteEventContext } from "../../providers/delete-event-provider";
import { nostrPostAction } from "../../classes/nostr-post-action";
import clientRelaysService from "../../services/client-relays";
import { handleEventFromRelay } from "../../services/event-relays";
import relayPoolService from "../../services/relay-pool";
import NostrPublishAction from "../../classes/nostr-publish-action";
export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) => {
@@ -30,7 +25,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
const { deleteEvent } = useDeleteEventContext();
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
const noteId = normalizeToBech32(event.id, Bech32Prefix.Note);
const noteId = nip19.noteEncode(event.id);
const broadcast = useCallback(() => {
const missingRelays = clientRelaysService.getWriteUrls();

View File

@@ -3,7 +3,7 @@ import { getEventRelays } from "../../services/event-relays";
import { NostrEvent } from "../../types/nostr-event";
import useSubject from "../../hooks/use-subject";
import { RelayIconStack } from "../relay-icon-stack";
import { getEventUID } from "../../helpers/nostr/event";
import { getEventUID } from "../../helpers/nostr/events";
import { useBreakpointValue } from "@chakra-ui/react";
export type NoteRelaysProps = {

View File

@@ -12,15 +12,15 @@ import { useInvoiceModalContext } from "../../providers/invoice-modal";
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
export default function NoteZapButton({
note,
event,
allowComment,
showEventPreview,
...props
}: { note: NostrEvent; allowComment?: boolean; showEventPreview?: boolean } & Omit<ButtonProps, "children">) {
}: { event: NostrEvent; allowComment?: boolean; showEventPreview?: boolean } & Omit<ButtonProps, "children">) {
const account = useCurrentAccount();
const { metadata } = useUserLNURLMetadata(note.pubkey);
const { metadata } = useUserLNURLMetadata(event.pubkey);
const { requestPay } = useInvoiceModalContext();
const zaps = useEventZaps(note.id);
const zaps = useEventZaps(event.id);
const { isOpen, onOpen, onClose } = useDisclosure();
const hasZapped = !!account && zaps.some((zap) => zap.request.pubkey === account.pubkey);
@@ -28,7 +28,7 @@ export default function NoteZapButton({
const handleInvoice = async (invoice: string) => {
onClose();
await requestPay(invoice);
eventZapsService.requestZaps(note.id, clientRelaysService.getReadUrls(), true);
eventZapsService.requestZaps(event.id, clientRelaysService.getReadUrls(), true);
};
const total = totalZaps(zaps);
@@ -62,9 +62,9 @@ export default function NoteZapButton({
<ZapModal
isOpen={isOpen}
onClose={onClose}
event={note}
event={event}
onInvoice={handleInvoice}
pubkey={note.pubkey}
pubkey={event.pubkey}
allowComment={allowComment}
showEventPreview={showEventPreview}
/>

View File

@@ -7,7 +7,7 @@ const QuoteNote = ({ noteId, relays }: { noteId: string; relays?: string[] }) =>
const readRelays = useReadRelayUrls(relays);
const { event, loading } = useSingleEvent(noteId, readRelays);
return event ? <EmbeddedNote note={event} /> : <NoteLink noteId={noteId} />;
return event ? <EmbeddedNote event={event} /> : <NoteLink noteId={noteId} />;
};
export default QuoteNote;

View File

@@ -15,7 +15,7 @@ import {
} from "@chakra-ui/react";
import dayjs from "dayjs";
import NostrPublishAction from "../../classes/nostr-publish-action";
import { getReferences } from "../../helpers/nostr/event";
import { getReferences } from "../../helpers/nostr/events";
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
import { useSigningContext } from "../../providers/signing-provider";
import { DraftNostrEvent } from "../../types/nostr-event";

View File

@@ -1,10 +1,11 @@
import React from "react";
import { Link } from "react-router-dom";
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
import { nip19 } from "nostr-tools";
import { UserAvatar, UserAvatarProps } from "./user-avatar";
export const UserAvatarLink = React.memo(({ pubkey, ...props }: UserAvatarProps) => (
<Link to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
<Link to={`/u/${nip19.npubEncode(pubkey)}`}>
<UserAvatar pubkey={pubkey} {...props} />
</Link>
));

View File

@@ -16,11 +16,12 @@ export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => {
export type UserAvatarProps = Omit<AvatarProps, "src"> & {
pubkey: string;
relay?: string;
noProxy?: boolean;
};
export const UserAvatar = React.memo(({ pubkey, noProxy, ...props }: UserAvatarProps) => {
export const UserAvatar = React.memo(({ pubkey, noProxy, relay, ...props }: UserAvatarProps) => {
const { imageProxy, proxyUserMedia } = useSubject(appSettings);
const metadata = useUserMetadata(pubkey);
const metadata = useUserMetadata(pubkey, relay ? [relay] : undefined);
const picture = useMemo(() => {
if (metadata?.picture) {
const src = safeUrl(metadata?.picture);

View File

@@ -1,6 +1,7 @@
import { Link, LinkProps } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
import { nip19 } from "nostr-tools";
import { getUserDisplayName } from "../helpers/user-metadata";
import { useUserMetadata } from "../hooks/use-user-metadata";
@@ -11,10 +12,9 @@ export type UserLinkProps = LinkProps & {
export const UserLink = ({ pubkey, showAt, ...props }: UserLinkProps) => {
const metadata = useUserMetadata(pubkey);
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
return (
<Link as={RouterLink} to={`/u/${npub}`} whiteSpace="nowrap" {...props}>
<Link as={RouterLink} to={`/u/${nip19.npubEncode(pubkey)}`} whiteSpace="nowrap" {...props}>
{showAt && "@"}
{getUserDisplayName(metadata, pubkey)}
</Link>

View File

@@ -183,7 +183,7 @@ export default function ZapModal({
{stream.image && <Image src={stream.image} />}
</Box>
)}
{showEventPreview && event && <EmbeddedNote note={event} />}
{showEventPreview && event && <EmbeddedNote event={event} />}
{allowComment && (canZap || lnurlMetadata?.commentAllowed) && (
<Input

View File

@@ -2,19 +2,15 @@ import { bech32 } from "bech32";
import { nip19 } from "nostr-tools";
import { getEventRelays } from "../services/event-relays";
import relayScoreboardService from "../services/relay-scoreboard";
import { NostrEvent, isDTag } from "../types/nostr-event";
import { getEventUID } from "./nostr/events";
export function isHex(key?: string) {
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
return false;
}
export enum Bech32Prefix {
Pubkey = "npub",
SecKey = "nsec",
Note = "note",
Profile = "nprofile",
}
/** @deprecated */
export function isBech32Key(bech32String: string) {
try {
const { prefix } = bech32.decode(bech32String.toLowerCase());
@@ -26,6 +22,7 @@ export function isBech32Key(bech32String: string) {
return true;
}
/** @deprecated */
export function bech32ToHex(bech32String: string) {
try {
const { words } = bech32.decode(bech32String);
@@ -34,16 +31,7 @@ export function bech32ToHex(bech32String: string) {
return "";
}
export function hexToBech32(hex: string, prefix: Bech32Prefix) {
try {
const hexArray = hexStringToUint8(hex);
return hexArray && bech32.encode(prefix, bech32.toWords(hexArray));
} catch (error) {
// continue
}
return null;
}
/** @deprecated */
export function toHexString(buffer: Uint8Array) {
return buffer.reduce((s, byte) => {
let hex = byte.toString(16);
@@ -52,28 +40,13 @@ export function toHexString(buffer: Uint8Array) {
}, "");
}
export function hexStringToUint8(str: string) {
if (str.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(str)) {
return null;
}
let buffer = new Uint8Array(str.length / 2);
for (let i = 0; i < buffer.length; i++) {
buffer[i] = parseInt(str.substr(2 * i, 2), 16);
}
return buffer;
}
export function safeDecode(str: string) {
try {
return nip19.decode(str);
} catch (e) {}
}
export function normalizeToBech32(key: string, prefix: Bech32Prefix = Bech32Prefix.Pubkey) {
if (isHex(key)) return hexToBech32(key, prefix);
if (isBech32Key(key)) return key;
return null;
}
/** @deprecated */
export function normalizeToHex(hex: string) {
if (isHex(hex)) return hex;
if (isBech32Key(hex)) return bech32ToHex(hex);
@@ -89,3 +62,15 @@ export function getSharableNoteId(eventId: string) {
return nip19.neventEncode({ id: eventId, relays: onlyTwo });
} else return nip19.noteEncode(eventId);
}
export function getSharableEventNaddr(event: NostrEvent) {
const relays = getEventRelays(getEventUID(event)).value;
const ranked = relayScoreboardService.getRankedRelays(relays);
const onlyTwo = ranked.slice(0, 2);
const d = event.tags.find(isDTag)?.[1];
if (!d) return null;
return nip19.naddrEncode({ kind: event.kind, identifier: d, pubkey: event.pubkey, relays: onlyTwo });
}

View File

@@ -1,22 +1,14 @@
import dayjs from "dayjs";
import { Kind, nip19 } from "nostr-tools";
import { getEventRelays } from "../../services/event-relays";
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag, Tag } from "../../types/nostr-event";
import { RelayConfig, RelayMode } from "../../classes/relay";
import accountService from "../../services/account";
import { Kind, nip19 } from "nostr-tools";
import { getMatchNostrLink } from "../regexp";
import { getSharableNoteId } from "../nip19";
import relayScoreboardService from "../../services/relay-scoreboard";
import { getAddr } from "../../services/replaceable-event-requester";
export function isReply(event: NostrEvent | DraftNostrEvent) {
return event.kind === 1 && !!getReferences(event).replyId;
}
export function isRepost(event: NostrEvent | DraftNostrEvent) {
const match = event.content.match(getMatchNostrLink());
return event.kind === 6 || (match && match[0].length === event.content.length);
}
import { AddressPointer } from "nostr-tools/lib/nip19";
export function truncatedId(str: string, keep = 6) {
if (str.length < keep * 2 + 3) return str;
@@ -26,11 +18,20 @@ export function truncatedId(str: string, keep = 6) {
// used to get a unique Id for each event, should take into account replaceable events
export function getEventUID(event: NostrEvent) {
if (event.kind >= 30000 && event.kind < 40000) {
return getAddr(event.kind, event.pubkey, event.tags.find((t) => t[0] === "d" && t[1])?.[1]);
return getEventCoordinate(event);
}
return event.id;
}
export function isReply(event: NostrEvent | DraftNostrEvent) {
return event.kind === 1 && !!getReferences(event).replyId;
}
export function isRepost(event: NostrEvent | DraftNostrEvent) {
const match = event.content.match(getMatchNostrLink());
return event.kind === 6 || (match && match[0].length === event.content.length);
}
/**
* returns an array of tag indexes that are referenced in the content
* either with the legacy #[0] syntax or nostr:xxxxx links
@@ -211,7 +212,15 @@ export function parseRTag(tag: RTag): RelayConfig {
}
}
export function parseCoordinate(a: string) {
export function getEventCoordinate(event: NostrEvent) {
const d = event.tags.find((t) => t[0] === "d")?.[1];
return d ? `${event.kind}:${event.pubkey}:${d}` : `${event.kind}:${event.pubkey}`;
}
export type ParsedCoordinate = Omit<AddressPointer, "identifier"> & {
identifier?: string;
};
export function parseCoordinate(a: string): ParsedCoordinate | null {
const parts = a.split(":") as (string | undefined)[];
const kind = parts[0] && parseInt(parts[0]);
const pubkey = parts[1];
@@ -223,6 +232,6 @@ export function parseCoordinate(a: string) {
return {
kind,
pubkey,
d,
identifier: d,
};
}

View File

@@ -0,0 +1,41 @@
import dayjs from "dayjs";
import { DraftNostrEvent, NostrEvent, isDTag, isPTag } from "../../types/nostr-event";
import { Kind } from "nostr-tools";
export const PEOPLE_LIST = 30000;
export const NOTE_LIST = 30001;
export const MUTE_LIST = 10000;
export const FOLLOW_LIST = Kind.Contacts;
export function getListName(event: NostrEvent) {
if (event.kind === 3) return "Following";
return event.tags.find(isDTag)?.[1];
}
export function getPubkeysFromList(event: NostrEvent) {
return event.tags.filter(isPTag).map((t) => ({ pubkey: t[1], relay: t[2] }));
}
export function draftAddPerson(event: NostrEvent, pubkey: string, relay?: string) {
if (event.tags.some((t) => t[0] === "p" && t[1] === pubkey)) throw new Error("person already in list");
const draft: DraftNostrEvent = {
created_at: dayjs().unix(),
kind: event.kind,
content: event.content,
tags: [...event.tags, relay ? ["p", pubkey, relay] : ["p", pubkey]],
};
return draft;
}
export function draftRemovePerson(event: NostrEvent, pubkey: string) {
const draft: DraftNostrEvent = {
created_at: dayjs().unix(),
kind: event.kind,
content: event.content,
tags: event.tags.filter((t) => t[0] !== "p" || t[1] !== pubkey),
};
return draft;
}

View File

@@ -1,7 +1,7 @@
import { DraftNostrEvent, NostrEvent, PTag, Tag } from "../../types/nostr-event";
import { getMatchHashtag, getMentionNpubOrNote } from "../regexp";
import { normalizeToHex } from "../nip19";
import { getReferences } from "./event";
import { getReferences } from "./events";
import { getEventRelays } from "../../services/event-relays";
import relayScoreboardService from "../../services/relay-scoreboard";

View File

@@ -1,7 +1,7 @@
import dayjs from "dayjs";
import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
import { unique } from "../array";
import { getAddr } from "../../services/replaceable-event-requester";
import { createCoordinate } from "../../services/replaceable-event-requester";
export const STREAM_KIND = 30311;
export const STREAM_CHAT_MESSAGE_KIND = 1311;
@@ -79,7 +79,7 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
}
export function getATag(stream: ParsedStream) {
return getAddr(stream.event.kind, stream.author, stream.identifier);
return createCoordinate(stream.event.kind, stream.author, stream.identifier);
}
export function buildChatMessage(stream: ParsedStream, content: string) {

View File

@@ -1,5 +1,5 @@
import { NostrEvent } from "../types/nostr-event";
import { EventReferences, getReferences } from "./nostr/event";
import { EventReferences, getReferences } from "./nostr/events";
export function countReplies(thread: ThreadItem): number {
return thread.replies.reduce((c, item) => c + countReplies(item), 0) + thread.replies.length;

View File

@@ -1,6 +1,6 @@
import { nip19 } from "nostr-tools";
import { NostrEvent } from "../types/nostr-event";
import { Bech32Prefix, normalizeToBech32 } from "./nip19";
import { truncatedId } from "./nostr/event";
import { truncatedId } from "./nostr/events";
export type Kind0ParsedContent = {
name?: string;
@@ -32,7 +32,7 @@ export function parseKind0Event(event: NostrEvent): Kind0ParsedContent {
export function getUserDisplayName(metadata: Kind0ParsedContent | undefined, pubkey: string) {
return (
metadata?.display_name || metadata?.name || truncatedId(normalizeToBech32(pubkey, Bech32Prefix.Pubkey) ?? pubkey)
metadata?.display_name || metadata?.name || truncatedId(nip19.npubEncode(pubkey))
);
}

View File

@@ -0,0 +1,21 @@
import { useReadRelayUrls } from "./use-client-relays";
import { useMemo } from "react";
import replaceableEventLoaderService from "../services/replaceable-event-requester";
import { ParsedCoordinate, parseCoordinate } from "../helpers/nostr/events";
import useSubject from "./use-subject";
export default function useReplaceableEvent(cord: string | ParsedCoordinate, additionalRelays: string[] = []) {
const readRelays = useReadRelayUrls(additionalRelays);
const sub = useMemo(() => {
const parsed = typeof cord === "string" ? parseCoordinate(cord) : cord;
if (!parsed) return;
return replaceableEventLoaderService.requestEvent(
parsed.relays ? [...readRelays, ...parsed.relays] : readRelays,
parsed.kind,
parsed.pubkey,
parsed.identifier,
);
}, [cord, readRelays.join("|")]);
return useSubject(sub);
}

View File

@@ -25,14 +25,14 @@ import { Event, Kind, nip19 } from "nostr-tools";
import { useCurrentAccount } from "../hooks/use-current-account";
import signingService from "../services/signing";
import { nostrPostAction } from "../classes/nostr-post-action";
import QuoteNote from "../components/note/quote-note";
import createDefer, { Deferred } from "../classes/deferred";
import useEventRelays from "../hooks/use-event-relays";
import { useWriteRelayUrls } from "../hooks/use-client-relays";
import { RelayFavicon } from "../components/relay-favicon";
import { ExternalLinkIcon } from "../components/icons";
import { buildDeleteEvent } from "../helpers/nostr/event";
import { buildDeleteEvent } from "../helpers/nostr/events";
import NostrPublishAction from "../classes/nostr-publish-action";
type DeleteEventContextType = {
isLoading: boolean;
@@ -82,8 +82,8 @@ export default function DeleteEventProvider({ children }: PropsWithChildren) {
const deleteEvent = buildDeleteEvent([event.id], reason);
const signed = await signingService.requestSignature(deleteEvent, account);
const results = nostrPostAction(writeRelays, signed);
await results.onComplete;
const pub = new NostrPublishAction("Delete", writeRelays, signed);
await pub.onComplete;
defer?.resolve();
} catch (e) {
if (e instanceof Error) {
@@ -106,7 +106,7 @@ export default function DeleteEventProvider({ children }: PropsWithChildren) {
isLoading,
deleteEvent,
}),
[isLoading, deleteEvent]
[isLoading, deleteEvent],
);
return (

View File

@@ -1,5 +1,5 @@
import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from "react";
import { truncatedId } from "../helpers/nostr/event";
import { truncatedId } from "../helpers/nostr/events";
import { useReadRelayUrls } from "../hooks/use-client-relays";
import { useCurrentAccount } from "../hooks/use-current-account";
import { TimelineLoader } from "../classes/timeline-loader";

View File

@@ -2,7 +2,7 @@ import { Kind } from "nostr-tools";
import { NostrRequest } from "../classes/nostr-request";
import Subject from "../classes/subject";
import { SuperMap } from "../classes/super-map";
import { getReferences } from "../helpers/nostr/event";
import { getReferences } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
type eventId = string;

View File

@@ -1,6 +1,6 @@
import { Relay } from "../classes/relay";
import { PersistentSubject } from "../classes/subject";
import { getEventUID } from "../helpers/nostr/event";
import { getEventUID } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
import relayPoolService from "./relay-pool";

View File

@@ -2,7 +2,7 @@ import { Kind } from "nostr-tools";
import { NostrRequest } from "../classes/nostr-request";
import Subject from "../classes/subject";
import { SuperMap } from "../classes/super-map";
import { getReferences } from "../helpers/nostr/event";
import { getReferences } from "../helpers/nostr/events";
import { NostrEvent } from "../types/nostr-event";
type eventId = string;

View File

@@ -1,18 +1,18 @@
import dayjs from "dayjs";
import { nip19 } from "nostr-tools";
import { NostrRequest } from "../classes/nostr-request";
import { PersistentSubject } from "../classes/subject";
import { DraftNostrEvent, NostrEvent, isPTag } from "../types/nostr-event";
import { NostrEvent, isPTag } from "../types/nostr-event";
import { getEventRelays } from "./event-relays";
import relayScoreboardService from "./relay-scoreboard";
import { getEventCoordinate } from "../helpers/nostr/events";
import { draftAddPerson, draftRemovePerson, getListName } from "../helpers/nostr/lists";
import replaceableEventLoaderService from "./replaceable-event-requester";
function getListName(event: NostrEvent) {
return event.tags.find((t) => t[0] === "d")?.[1];
}
/** @deprecated */
export class List {
event: NostrEvent;
cord: string;
people = new PersistentSubject<{ pubkey: string; relay?: string }[]>([]);
get author() {
@@ -36,6 +36,7 @@ export class List {
constructor(event: NostrEvent) {
this.event = event;
this.cord = getEventCoordinate(event);
this.updatePeople();
}
@@ -51,27 +52,11 @@ export class List {
}
draftAddPerson(pubkey: string, relay?: string) {
if (this.event.tags.some((t) => t[0] === "p" && t[1] === pubkey)) throw new Error("person already in list");
const draft: DraftNostrEvent = {
created_at: dayjs().unix(),
kind: this.event.kind,
content: this.event.content,
tags: [...this.event.tags, relay ? ["p", pubkey, relay] : ["p", pubkey]],
};
return draft;
return draftAddPerson(this.event, pubkey, relay);
}
draftRemovePerson(pubkey: string) {
const draft: DraftNostrEvent = {
created_at: dayjs().unix(),
kind: this.event.kind,
content: this.event.content,
tags: this.event.tags.filter((t) => t[0] !== "p" || t[1] !== pubkey),
};
return draft;
return draftRemovePerson(this.event, pubkey);
}
}
@@ -91,6 +76,8 @@ class ListsService {
const request = new NostrRequest(relays);
request.onEvent.subscribe((event) => {
replaceableEventLoaderService.handleEvent(event);
const listName = getListName(event);
if (listName && event.kind === 30000) {

View File

@@ -8,14 +8,15 @@ import { NostrQuery } from "../types/nostr-query";
import { logger } from "../helpers/debug";
import db from "./db";
import { nameOrPubkey } from "./user-metadata";
import { getEventCoordinate } from "../helpers/nostr/events";
type Pubkey = string;
type Relay = string;
export function getReadableAddr(kind: number, pubkey: string, d?: string) {
export function getReadableCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${nameOrPubkey(pubkey)}${d ? ":" + d : ""}`;
}
export function getAddr(kind: number, pubkey: string, d?: string) {
export function createCoordinate(kind: number, pubkey: string, d?: string) {
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
}
@@ -38,13 +39,12 @@ class ReplaceableEventRelayLoader {
}
private handleEvent(event: NostrEvent) {
const d = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
const addr = getAddr(event.kind, event.pubkey, d);
const cord = getEventCoordinate(event);
// remove the pubkey from the waiting list
this.requested.delete(addr);
this.requested.delete(cord);
const sub = this.events.get(addr);
const sub = this.events.get(cord);
const current = sub.value;
if (!current || event.created_at > current.created_at) {
@@ -57,15 +57,15 @@ class ReplaceableEventRelayLoader {
}
getEvent(kind: number, pubkey: string, d?: string) {
return this.events.get(getAddr(kind, pubkey, d));
return this.events.get(createCoordinate(kind, pubkey, d));
}
requestEvent(kind: number, pubkey: string, d?: string) {
const addr = getAddr(kind, pubkey, d);
const event = this.events.get(addr);
const cord = createCoordinate(kind, pubkey, d);
const event = this.events.get(cord);
if (!event.value) {
this.requestNext.add(addr);
this.requestNext.add(cord);
}
return event;
@@ -73,9 +73,9 @@ class ReplaceableEventRelayLoader {
update() {
let needsUpdate = false;
for (const addr of this.requestNext) {
if (!this.requested.has(addr)) {
this.requested.set(addr, new Date());
for (const cord of this.requestNext) {
if (!this.requested.has(cord)) {
this.requested.set(cord, new Date());
needsUpdate = true;
}
}
@@ -83,9 +83,9 @@ class ReplaceableEventRelayLoader {
// prune requests
const timeout = dayjs().subtract(1, "minute");
for (const [addr, date] of this.requested) {
for (const [cord, date] of this.requested) {
if (dayjs(date).isBefore(timeout)) {
this.requested.delete(addr);
this.requested.delete(cord);
needsUpdate = true;
}
}
@@ -95,8 +95,8 @@ class ReplaceableEventRelayLoader {
if (this.requested.size > 0) {
const filters: Record<number, NostrQuery> = {};
for (const [addr] of this.requested) {
const [kindStr, pubkey, d] = addr.split(":") as [string, string] | [string, string, string];
for (const [cord] of this.requested) {
const [kindStr, pubkey, d] = cord.split(":") as [string, string] | [string, string, string];
const kind = parseInt(kindStr);
filters[kind] = filters[kind] || { kinds: [kind] };
@@ -139,28 +139,27 @@ class ReplaceableEventLoaderService {
log = logger.extend("ReplaceableEventLoader");
handleEvent(event: NostrEvent) {
const d = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
const addr = getAddr(event.kind, event.pubkey, d);
const cord = getEventCoordinate(event);
const sub = this.events.get(addr);
const sub = this.events.get(cord);
const current = sub.value;
if (!current || event.created_at > current.created_at) {
sub.next(event);
this.saveToCache(addr, event);
this.saveToCache(cord, event);
}
}
getEvent(kind: number, pubkey: string, d?: string) {
return this.events.get(getAddr(kind, pubkey, d));
return this.events.get(createCoordinate(kind, pubkey, d));
}
private loadCacheDedupe = new Map<string, Promise<boolean>>();
private loadFromCache(addr: string) {
const dedupe = this.loadCacheDedupe.get(addr);
private loadFromCache(cord: string) {
const dedupe = this.loadCacheDedupe.get(cord);
if (dedupe) return dedupe;
const promise = db.get("replaceableEvents", addr).then((cached) => {
this.loadCacheDedupe.delete(addr);
const promise = db.get("replaceableEvents", cord).then((cached) => {
this.loadCacheDedupe.delete(cord);
if (cached?.event) {
this.handleEvent(cached.event);
return true;
@@ -168,12 +167,12 @@ class ReplaceableEventLoaderService {
return false;
});
this.loadCacheDedupe.set(addr, promise);
this.loadCacheDedupe.set(cord, promise);
return promise;
}
private async saveToCache(addr: string, event: NostrEvent) {
await db.put("replaceableEvents", { addr, event, created: dayjs().unix() });
private async saveToCache(cord: string, event: NostrEvent) {
await db.put("replaceableEvents", { addr: cord, event, created: dayjs().unix() });
}
async pruneCache() {
@@ -193,8 +192,8 @@ class ReplaceableEventLoaderService {
}
private requestEventFromRelays(relays: string[], kind: number, pubkey: string, d?: string) {
const addr = getAddr(kind, pubkey, d);
const sub = this.events.get(addr);
const cord = createCoordinate(kind, pubkey, d);
const sub = this.events.get(cord);
for (const relay of relays) {
const request = this.loaders.get(relay).requestEvent(kind, pubkey, d);
@@ -202,7 +201,7 @@ class ReplaceableEventLoaderService {
sub.connectWithHandler(request, (event, next, current) => {
if (!current || event.created_at > current.created_at) {
next(event);
this.saveToCache(addr, event);
this.saveToCache(cord, event);
}
});
}
@@ -211,11 +210,11 @@ class ReplaceableEventLoaderService {
}
requestEvent(relays: string[], kind: number, pubkey: string, d?: string, alwaysRequest = false) {
const addr = getAddr(kind, pubkey, d);
const sub = this.events.get(addr);
const cord = createCoordinate(kind, pubkey, d);
const sub = this.events.get(cord);
if (!sub.value) {
this.loadFromCache(addr).then((loaded) => {
this.loadFromCache(cord).then((loaded) => {
if (!loaded) {
this.requestEventFromRelays(relays, kind, pubkey, d);
}

View File

@@ -1,6 +1,6 @@
import { isRTag, NostrEvent } from "../types/nostr-event";
import { RelayConfig } from "../classes/relay";
import { parseRTag } from "../helpers/nostr/event";
import { parseRTag } from "../helpers/nostr/events";
import { SuperMap } from "../classes/super-map";
import Subject from "../classes/subject";
import { normalizeRelayConfigs } from "../helpers/relay";

View File

@@ -18,7 +18,7 @@ import { CloseIcon } from "@chakra-ui/icons";
import { useNavigate, useParams } from "react-router-dom";
import { useAppTitle } from "../../hooks/use-app-title";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { isReply } from "../../helpers/nostr/event";
import { isReply } from "../../helpers/nostr/events";
import { CheckIcon, EditIcon } from "../../components/icons";
import { NostrEvent } from "../../types/nostr-event";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";

View File

@@ -3,7 +3,7 @@ import { Flex, FormControl, FormLabel, Switch } from "@chakra-ui/react";
import { useSearchParams } from "react-router-dom";
import { Kind } from "nostr-tools";
import { isReply, truncatedId } from "../../helpers/nostr/event";
import { isReply, truncatedId } from "../../helpers/nostr/events";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useUserContacts } from "../../hooks/use-user-contacts";
import { useReadRelayUrls } from "../../hooks/use-client-relays";

View File

@@ -1,6 +1,6 @@
import { useCallback } from "react";
import { Flex, FormControl, FormLabel, Switch, useDisclosure } from "@chakra-ui/react";
import { isReply } from "../../helpers/nostr/event";
import { isReply } from "../../helpers/nostr/events";
import { useAppTitle } from "../../hooks/use-app-title";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";

View File

@@ -0,0 +1,50 @@
import { Link as RouterLink } from "react-router-dom";
import { AvatarGroup, Card, CardBody, CardHeader, Heading, Link, Spacer, Text } from "@chakra-ui/react";
import { UserAvatarLink } from "../../../components/user-avatar-link";
import { UserLink } from "../../../components/user-link";
import EventVerificationIcon from "../../../components/event-verification-icon";
import { getListName, getPubkeysFromList } from "../../../helpers/nostr/lists";
import { getSharableEventNaddr } from "../../../helpers/nip19";
import { NostrEvent } from "../../../types/nostr-event";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { Kind } from "nostr-tools";
import { createCoordinate } from "../../../services/replaceable-event-requester";
export default function ListCard({ cord, event: maybeEvent }: { cord?: string; event?: NostrEvent }) {
const event = maybeEvent ?? (cord ? useReplaceableEvent(cord as string) : undefined);
if (!event) return null;
const people = getPubkeysFromList(event);
const link =
event.kind === Kind.Contacts ? createCoordinate(Kind.Contacts, event.pubkey) : getSharableEventNaddr(event);
return (
<Card>
<CardHeader display="flex" p="2" flex="1" gap="2" alignItems="center">
<Heading size="md">
<Link as={RouterLink} to={`/lists/${link}`}>
{getListName(event)}
</Link>
</Heading>
<Spacer />
<Text>Created by:</Text>
<UserAvatarLink pubkey={event.pubkey} size="xs" />
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
<EventVerificationIcon event={event} />
</CardHeader>
<CardBody p="2">
{people.length > 0 && (
<>
<Text>{people.length} people</Text>
<AvatarGroup overflow="hidden">
{people.map(({ pubkey, relay }) => (
<UserAvatarLink pubkey={pubkey} relay={relay} />
))}
</AvatarGroup>
</>
)}
</CardBody>
</Card>
);
}

View File

@@ -1,10 +1,14 @@
import { Button, Divider, Flex, Heading, Image, Link, Spacer } from "@chakra-ui/react";
import { Button, Flex, Image, Link, Spacer } from "@chakra-ui/react";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useUserLists from "../../hooks/use-user-lists";
import { Link as RouterLink } from "react-router-dom";
import { ExternalLinkIcon, PlusCircleIcon } from "../../components/icons";
import RequireCurrentAccount from "../../providers/require-current-account";
import ListCard from "./components/list-card";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NOTE_LIST, PEOPLE_LIST } from "../../helpers/nostr/lists";
import useSubject from "../../hooks/use-subject";
import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events";
function UsersLists() {
const account = useCurrentAccount()!;
@@ -14,16 +18,26 @@ function UsersLists() {
return (
<>
<ListCard cord={`3:${account.pubkey}`} />
<ListCard cord={`10000:${account.pubkey}`} />
{Array.from(Object.entries(lists)).map(([name, list]) => (
<Button key={name} as={RouterLink} to={`./${list.getAddress()}`} isTruncated>
{name}
</Button>
<ListCard key={name} cord={list.cord} />
))}
</>
);
}
function ListsPage() {
const account = useCurrentAccount()!;
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader("lists", readRelays, {
authors: [account.pubkey],
kinds: [PEOPLE_LIST, NOTE_LIST],
});
const events = useSubject(timeline.timeline);
return (
<Flex direction="column" p="2" gap="2">
<Flex gap="2">
@@ -40,7 +54,11 @@ function ListsPage() {
<Button leftIcon={<PlusCircleIcon />}>New List</Button>
</Flex>
<UsersLists />
<ListCard cord={`3:${account.pubkey}`} />
<ListCard cord={`10000:${account.pubkey}`} />
{events.map((event) => (
<ListCard key={getEventUID(event)} event={event} />
))}
</Flex>
);
}

View File

@@ -1,49 +1,58 @@
import { Link as RouterList, useNavigate, useParams } from "react-router-dom";
import { nip19 } from "nostr-tools";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useUserLists from "../../hooks/use-user-lists";
import { Kind, nip19 } from "nostr-tools";
import { UserLink } from "../../components/user-link";
import useSubject from "../../hooks/use-subject";
import { Button, Flex, Heading, Link } from "@chakra-ui/react";
import { Button, Flex, Heading } from "@chakra-ui/react";
import { UserCard } from "../user/components/user-card";
import { ArrowLeftSIcon, ExternalLinkIcon } from "../../components/icons";
import { ArrowLeftSIcon } from "../../components/icons";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { buildAppSelectUrl } from "../../helpers/nostr-apps";
import { useDeleteEventContext } from "../../providers/delete-event-provider";
import { parseCoordinate } from "../../helpers/nostr/events";
import accountService from "../../services/account";
import { MUTE_LIST, getListName, getPubkeysFromList } from "../../helpers/nostr/lists";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
function useListPointer() {
function useListCoordinate() {
const { addr } = useParams() as { addr: string };
const pointer = nip19.decode(addr);
switch (pointer.type) {
case "naddr":
if (pointer.data.kind !== 30000) throw new Error("Unknown event kind");
return pointer.data;
default:
throw new Error(`Unknown type ${pointer.type}`);
const current = accountService.current.value;
if (addr === "following") {
if (!current) throw new Error("No account");
return { kind: Kind.Contacts, pubkey: current.pubkey };
}
if (addr === "mute") {
if (!current) throw new Error("No account");
return { kind: MUTE_LIST, pubkey: current.pubkey };
}
if (addr.includes(":")) {
const parsed = parseCoordinate(addr);
if (!parsed) throw new Error("Bad coordinate");
return parsed;
}
const parsed = nip19.decode(addr);
if (parsed.type !== "naddr") throw new Error(`Unknown type ${parsed.type}`);
return parsed.data;
}
export default function ListView() {
const pointer = useListPointer();
const account = useCurrentAccount();
const navigate = useNavigate();
const coordinate = useListCoordinate();
const { deleteEvent } = useDeleteEventContext();
const account = useCurrentAccount();
const readRelays = useReadRelayUrls(pointer.relays);
const lists = useUserLists(pointer.pubkey, readRelays, true);
const event = useReplaceableEvent(coordinate);
const list = lists[pointer.identifier];
const people = useSubject(list?.people) ?? [];
if (!list)
if (!event)
return (
<>
Looking for list "{pointer.identifier}" created by <UserLink pubkey={pointer.pubkey} />
Looking for list "{coordinate.identifier}" created by <UserLink pubkey={coordinate.pubkey} />
</>
);
const isAuthor = account?.pubkey === list.author;
const isAuthor = account?.pubkey === event.pubkey;
const people = getPubkeysFromList(event);
return (
<Flex direction="column" px="2" pt="2" pb="8" overflowY="auto" overflowX="hidden" h="full" gap="2">
@@ -53,17 +62,14 @@ export default function ListView() {
</Button>
<Heading size="md" flex={1} isTruncated>
{list.name}
{getListName(event)}
</Heading>
{isAuthor && (
<Button colorScheme="red" onClick={() => deleteEvent(list.event).then(() => navigate("/lists"))}>
<Button colorScheme="red" onClick={() => deleteEvent(event).then(() => navigate("/lists"))}>
Delete
</Button>
)}
<Button as={Link} href={buildAppSelectUrl(list.getAddress())} target="_blank" leftIcon={<ExternalLinkIcon />}>
Open in app
</Button>
</Flex>
{people.map(({ pubkey, relay }) => (
<UserCard pubkey={pubkey} relay={relay} />

View File

@@ -17,10 +17,10 @@ import {
} from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { RelayUrlInput } from "../../components/relay-url-input";
import { Bech32Prefix, normalizeToBech32, normalizeToHex } from "../../helpers/nip19";
import { normalizeToHex } from "../../helpers/nip19";
import accountService from "../../services/account";
import clientRelaysService from "../../services/client-relays";
import { generatePrivateKey, getPublicKey } from "nostr-tools";
import { generatePrivateKey, getPublicKey, nip19 } from "nostr-tools";
import signingService from "../../services/signing";
export default function LoginNsecView() {
@@ -39,8 +39,8 @@ export default function LoginNsecView() {
const hex = generatePrivateKey();
const pubkey = getPublicKey(hex);
setHexKey(hex);
setInputValue(normalizeToBech32(hex, Bech32Prefix.SecKey) ?? "");
setNpub(normalizeToBech32(pubkey, Bech32Prefix.Pubkey) ?? "");
setInputValue(nip19.nsecEncode(hex));
setNpub(nip19.npubEncode(pubkey));
setShow(true);
}, [setHexKey, setInputValue, setShow]);
@@ -53,7 +53,7 @@ export default function LoginNsecView() {
if (hex) {
const pubkey = getPublicKey(hex);
setHexKey(hex);
setNpub(normalizeToBech32(pubkey, Bech32Prefix.Pubkey) ?? "");
setNpub(nip19.npubEncode(pubkey));
setError(false);
} else {
setError(true);

View File

@@ -15,7 +15,7 @@ import { DraftNostrEvent } from "../../types/nostr-event";
import RequireCurrentAccount from "../../providers/require-current-account";
import { Message } from "./message";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { truncatedId } from "../../helpers/nostr/event";
import { truncatedId } from "../../helpers/nostr/events";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import IntersectionObserverProvider from "../../providers/intersection-observer";

View File

@@ -1,3 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { ChatIcon } from "@chakra-ui/icons";
import {
Alert,
@@ -14,22 +15,20 @@ import {
Text,
} from "@chakra-ui/react";
import dayjs from "dayjs";
import { useEffect, useMemo, useRef, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import { UserAvatar } from "../../components/user-avatar";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { getUserDisplayName } from "../../helpers/user-metadata";
import useSubject from "../../hooks/use-subject";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import directMessagesService from "../../services/direct-messages";
import { ExternalLinkIcon } from "../../components/icons";
import RequireCurrentAccount from "../../providers/require-current-account";
import { nip19 } from "nostr-tools";
function ContactCard({ pubkey }: { pubkey: string }) {
const subject = useMemo(() => directMessagesService.getUserMessages(pubkey), [pubkey]);
const messages = useSubject(subject);
const metadata = useUserMetadata(pubkey);
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
return (
<LinkBox as={Card} size="sm">
@@ -40,7 +39,7 @@ function ContactCard({ pubkey }: { pubkey: string }) {
{messages[0] && <Text flexShrink={0}>{dayjs.unix(messages[0].created_at).fromNow()}</Text>}
</Flex>
</CardBody>
<LinkOverlay as={RouterLink} to={`/dm/${npub ?? pubkey}`} />
<LinkOverlay as={RouterLink} to={`/dm/${nip19.npubEncode(pubkey)}`} />
</LinkBox>
);
}

View File

@@ -15,7 +15,7 @@ import { useNotificationTimeline } from "../../providers/notification-timeline";
import { Kind, getEventHash } from "nostr-tools";
import { parseZapEvent } from "../../helpers/zaps";
import { readablizeSats } from "../../helpers/bolt11";
import { getReferences } from "../../helpers/nostr/event";
import { getReferences } from "../../helpers/nostr/events";
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
<Card size="sm" variant="outline">

View File

@@ -1,7 +1,7 @@
import { useCallback } from "react";
import { Flex, Switch, useDisclosure } from "@chakra-ui/react";
import { isReply } from "../../../helpers/nostr/event";
import { isReply } from "../../../helpers/nostr/events";
import { useAppTitle } from "../../../hooks/use-app-title";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import { NostrEvent } from "../../../types/nostr-event";

View File

@@ -1,25 +1,13 @@
import { useMemo } from "react";
import { Card, CardBody, CardHeader, CardProps, Flex, Heading, Image, LinkBox, LinkOverlay } from "@chakra-ui/react";
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
import { useRelaySelectionRelays } from "../../../providers/relay-selection-provider";
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
import useSubject from "../../../hooks/use-subject";
import {
Card,
CardBody,
CardHeader,
CardProps,
Code,
Flex,
Heading,
Image,
Link,
LinkBox,
LinkOverlay,
} from "@chakra-ui/react";
import { NoteContents } from "../../../components/note/note-contents";
import { isATag } from "../../../types/nostr-event";
import {} from "nostr-tools";
import { parseCoordinate } from "../../../helpers/nostr/event";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
export const STREAMER_CARDS_TYPE = 17777;
export const STREAMER_CARD_TYPE = 37777;
@@ -33,22 +21,13 @@ function useStreamerCardsCords(pubkey: string, relays: string[]) {
return streamerCards?.tags.filter(isATag) ?? [];
}
function useStreamerCard(cord: string, relays: string[]) {
const sub = useMemo(() => {
const parsed = parseCoordinate(cord);
if (!parsed || !parsed.d || parsed.kind !== STREAMER_CARD_TYPE) return;
return replaceableEventLoaderService.requestEvent(relays, STREAMER_CARD_TYPE, parsed.pubkey, parsed.d);
}, [cord, relays.join("|")]);
return useSubject(sub);
}
function StreamerCard({ cord, relay, ...props }: { cord: string; relay?: string } & CardProps) {
const contextRelays = useRelaySelectionRelays();
const readRelays = useReadRelayUrls(relay ? [...contextRelays, relay] : contextRelays);
const card = useStreamerCard(cord, readRelays);
if (!card) return null;
const card = useReplaceableEvent(cord, readRelays);
if (!card || card.kind !== STREAMER_CARD_TYPE) return null;
const title = card.tags.find((t) => t[0] === "title")?.[1];
const image = card.tags.find((t) => t[0] === "image")?.[1];

View File

@@ -16,7 +16,7 @@ function ChatMessage({ event, stream }: { event: NostrEvent; stream: ParsedStrea
return (
<TrustProvider event={event}>
<Box ref={ref}>
<NoteZapButton note={event} size="xs" variant="ghost" float="right" ml="2" allowComment={false} />
<NoteZapButton event={event} size="xs" variant="ghost" float="right" ml="2" allowComment={false} />
<Box overflow="hidden" maxH="lg">
<UserAvatar pubkey={event.pubkey} size="xs" display="inline-block" mr="2" />
<Text as="span" fontWeight="bold" color={event.pubkey === stream.host ? "rgb(248, 56, 217)" : "cyan"}>

View File

@@ -31,7 +31,7 @@ import { useSigningContext } from "../../../../providers/signing-provider";
import { useTimelineCurserIntersectionCallback } from "../../../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../../../hooks/use-subject";
import useTimelineLoader from "../../../../hooks/use-timeline-loader";
import { truncatedId } from "../../../../helpers/nostr/event";
import { truncatedId } from "../../../../helpers/nostr/events";
import { css } from "@emotion/react";
import TopZappers from "./top-zappers";
import { parseZapEvent } from "../../../../helpers/zaps";

View File

@@ -1,8 +1,5 @@
import { Avatar, Button, Flex, Heading, Image, Link } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { Button, Flex, Heading, Image, Link } from "@chakra-ui/react";
import { ExternalLinkIcon, ToolsIcon } from "../../components/icons";
import { ToolsIcon } from "../../components/icons";
import OpenGraphCard from "../../components/open-graph-card";
export default function ToolsHomeView() {
return (

View File

@@ -23,29 +23,29 @@ import {
useDisclosure,
} from "@chakra-ui/react";
import { useAsync } from "react-use";
import { readablizeSats } from "../../helpers/bolt11";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { getLudEndpoint } from "../../helpers/lnurl";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import { truncatedId } from "../../helpers/nostr/events";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { embedNostrLinks, renderGenericUrl } from "../../components/embed-types";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import { ArrowDownSIcon, ArrowUpSIcon, AtIcon, ExternalLinkIcon, KeyIcon, LightningIcon } from "../../components/icons";
import { normalizeToBech32 } from "../../helpers/nip19";
import { Bech32Prefix } from "../../helpers/nip19";
import { truncatedId } from "../../helpers/nostr/event";
import { CopyIconButton } from "../../components/copy-icon-button";
import { QrIconButton } from "./components/share-qr-button";
import { UserDnsIdentityIcon } from "../../components/user-dns-identity-icon";
import { useUserContacts } from "../../hooks/use-user-contacts";
import userTrustedStatsService from "../../services/user-trusted-stats";
import { readablizeSats } from "../../helpers/bolt11";
import { UserAvatar } from "../../components/user-avatar";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { ChatIcon } from "@chakra-ui/icons";
import { UserFollowButton } from "../../components/user-follow-button";
import { UserTipButton } from "../../components/user-tip-button";
import { UserProfileMenu } from "./components/user-profile-menu";
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
import { parseAddress } from "../../services/dns-identity";
import { getLudEndpoint } from "../../helpers/lnurl";
import { nip19 } from "nostr-tools";
function buildDescriptionContent(description: string) {
let content: EmbedableContent = [description.trim()];
@@ -63,7 +63,7 @@ export default function UserAboutTab() {
const metadata = useUserMetadata(pubkey, contextRelays);
const contacts = useUserContacts(pubkey, contextRelays);
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
const npub = nip19.npubEncode(pubkey);
const nprofile = useSharableProfileId(pubkey);
const { value: stats } = useAsync(() => userTrustedStatsService.getUserStats(pubkey), [pubkey]);

View File

@@ -1,11 +1,8 @@
import { Flex, Heading, IconButton, Spacer, useBreakpointValue } from "@chakra-ui/react";
import { useNavigate, Link as RouterLink } from "react-router-dom";
import { ChatIcon, EditIcon } from "../../../components/icons";
import { useNavigate } from "react-router-dom";
import { EditIcon } from "../../../components/icons";
import { UserAvatar } from "../../../components/user-avatar";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
import { UserFollowButton } from "../../../components/user-follow-button";
import { UserTipButton } from "../../../components/user-tip-button";
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { useCurrentAccount } from "../../../hooks/use-current-account";
import { useUserMetadata } from "../../../hooks/use-user-metadata";

View File

@@ -15,16 +15,17 @@ import {
Input,
Flex,
} from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { QrCodeIcon } from "../../../components/icons";
import QrCodeSvg from "../../../components/qr-code-svg";
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
import { CopyIconButton } from "../../../components/copy-icon-button";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "icon">) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey) || pubkey;
const npub = nip19.npubEncode(pubkey);
const npubLink = "nostr:" + npub;
const nprofile = useSharableProfileId(pubkey);
const nprofileLink = "nostr:" + nprofile;

View File

@@ -1,10 +1,10 @@
import { Flex, FlexProps, Heading, Link } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { nip19 } from "nostr-tools";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../../helpers/user-metadata";
import { UserAvatar } from "../../../components/user-avatar";
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
import { UserFollowButton } from "../../../components/user-follow-button";
@@ -28,7 +28,7 @@ export const UserCard = ({ pubkey, relay, ...props }: UserCardProps) => {
>
<UserAvatar pubkey={pubkey} />
<Flex direction="column" flex={1} overflow="hidden">
<Link as={RouterLink} to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
<Link as={RouterLink} to={`/u/${nip19.npubEncode(pubkey)}`}>
<Heading size="sm" whiteSpace="nowrap" isTruncated>
{getUserDisplayName(metadata, pubkey)}
</Heading>

View File

@@ -12,8 +12,8 @@ import { RelayMode } from "../../../classes/relay";
import UserDebugModal from "../../../components/debug-modals/user-debug-modal";
import { useCopyToClipboard } from "react-use";
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
import { buildAppSelectUrl } from "../../../helpers/nostr-apps";
import { truncatedId } from "../../../helpers/nostr/event";
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
import { truncatedId } from "../../../helpers/nostr/events";
export const UserProfileMenu = ({
pubkey,

View File

@@ -6,7 +6,7 @@ import { UserCard, UserCardProps } from "./components/user-card";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { truncatedId } from "../../helpers/nostr/event";
import { truncatedId } from "../../helpers/nostr/events";
import useSubject from "../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";

View File

@@ -27,7 +27,7 @@ import {
import { Outlet, useMatches, useNavigate, useParams } from "react-router-dom";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { Bech32Prefix, isHex, normalizeToBech32 } from "../../helpers/nip19";
import { isHex } from "../../helpers/nip19";
import { useAppTitle } from "../../hooks/use-app-title";
import { Suspense, useState } from "react";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
@@ -39,12 +39,14 @@ import { unique } from "../../helpers/array";
import { RelayFavicon } from "../../components/relay-favicon";
import { useUserRelays } from "../../hooks/use-user-relays";
import Header from "./components/header";
import { ErrorBoundary } from "../../components/error-boundary";
const tabs = [
{ label: "About", path: "about" },
{ label: "Notes", path: "notes" },
{ label: "Streams", path: "streams" },
{ label: "Zaps", path: "zaps" },
{ label: "Lists", path: "lists" },
{ label: "Following", path: "following" },
{ label: "Likes", path: "likes" },
{ label: "Relays", path: "relays" },
@@ -94,9 +96,8 @@ const UserView = () => {
const activeTab = tabs.indexOf(tabs.find((t) => lastMatch.pathname.endsWith(t.path)) ?? tabs[0]);
const metadata = useUserMetadata(pubkey, userTopRelays, true);
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
useAppTitle(getUserDisplayName(metadata, npub ?? pubkey));
useAppTitle(getUserDisplayName(metadata, pubkey));
return (
<>
@@ -121,9 +122,11 @@ const UserView = () => {
<TabPanels>
{tabs.map(({ label }) => (
<TabPanel key={label} p={0}>
<Suspense fallback={<Spinner />}>
<Outlet context={{ pubkey, setRelayCount }} />
</Suspense>
<ErrorBoundary>
<Suspense fallback={<Spinner />}>
<Outlet context={{ pubkey, setRelayCount }} />
</Suspense>
</ErrorBoundary>
</TabPanel>
))}
</TabPanels>

View File

@@ -2,7 +2,7 @@ import { useRef } from "react";
import { useOutletContext } from "react-router-dom";
import { Box, Flex, SkeletonText, Spacer, Text } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { getReferences, truncatedId } from "../../helpers/nostr/event";
import { getReferences, truncatedId } from "../../helpers/nostr/events";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";

31
src/views/user/lists.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { useOutletContext } from "react-router-dom";
import { Flex } from "@chakra-ui/react";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import { NOTE_LIST, PEOPLE_LIST } from "../../helpers/nostr/lists";
import { getEventUID, truncatedId } from "../../helpers/nostr/events";
import ListCard from "../lists/components/list-card";
export default function UserListsTab() {
const { pubkey } = useOutletContext() as { pubkey: string };
const readRelays = useAdditionalRelayContext();
const timeline = useTimelineLoader(truncatedId(pubkey) + "-lists", readRelays, {
authors: [pubkey],
kinds: [PEOPLE_LIST, NOTE_LIST],
});
const events = useSubject(timeline.timeline);
return (
<Flex direction="column" p="2" gap="2">
<ListCard cord={`3:${pubkey}`} />
<ListCard cord={`10000:${pubkey}`} />
{events.map((event) => (
<ListCard key={getEventUID(event)} event={event} />
))}
</Flex>
);
}

View File

@@ -2,7 +2,7 @@ import { useCallback } from "react";
import { Flex, FormControl, FormLabel, Spacer, Switch, useDisclosure } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { Kind } from "nostr-tools";
import { isReply, isRepost, truncatedId } from "../../helpers/nostr/event";
import { isReply, isRepost, truncatedId } from "../../helpers/nostr/events";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
import { RelayIconStack } from "../../components/relay-icon-stack";
import { NostrEvent } from "../../types/nostr-event";

View File

@@ -3,7 +3,7 @@ import { Button, Flex, Heading, Spacer, StackDivider, Tag, VStack } from "@chakr
import { useUserRelays } from "../../hooks/use-user-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { truncatedId } from "../../helpers/nostr/event";
import { truncatedId } from "../../helpers/nostr/events";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../../types/nostr-event";

View File

@@ -2,7 +2,7 @@ import { Flex, Text } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { NoteLink } from "../../components/note-link";
import { UserLink } from "../../components/user-link";
import { filterTagsByContentRefs, truncatedId } from "../../helpers/nostr/event";
import { filterTagsByContentRefs, truncatedId } from "../../helpers/nostr/events";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { isETag, isPTag, NostrEvent } from "../../types/nostr-event";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";

View File

@@ -1,6 +1,6 @@
import { Flex, SimpleGrid } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import { truncatedId } from "../../helpers/nostr/event";
import { truncatedId } from "../../helpers/nostr/events";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
import IntersectionObserverProvider from "../../providers/intersection-observer";

View File

@@ -8,7 +8,7 @@ import { NoteLink } from "../../components/note-link";
import { UserAvatarLink } from "../../components/user-avatar-link";
import { UserLink } from "../../components/user-link";
import { readablizeSats } from "../../helpers/bolt11";
import { truncatedId } from "../../helpers/nostr/event";
import { truncatedId } from "../../helpers/nostr/events";
import { isProfileZap, isNoteZap, parseZapEvent, totalZaps } from "../../helpers/zaps";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";