diff --git a/src/app.tsx b/src/app.tsx index a61dca935..8b66c69cc 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -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: }, { path: "zaps", element: }, { path: "likes", element: }, + { path: "lists", element: }, { path: "followers", element: }, { path: "following", element: }, { path: "relays", element: }, diff --git a/src/classes/event-store.ts b/src/classes/event-store.ts index 87f3221bb..619ddbab3 100644 --- a/src/classes/event-store.ts +++ b/src/classes/event-store.ts @@ -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"; diff --git a/src/classes/thread-loader.ts b/src/classes/thread-loader.ts index f22e3fe6a..e53b5b733 100644 --- a/src/classes/thread-loader.ts +++ b/src/classes/thread-loader.ts @@ -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"; diff --git a/src/components/debug-modals/note-debug-modal.tsx b/src/components/debug-modals/note-debug-modal.tsx index d18f54472..f9d4b5643 100644 --- a/src/components/debug-modals/note-debug-modal.tsx +++ b/src/components/debug-modals/note-debug-modal.tsx @@ -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) { return ( @@ -16,7 +16,7 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent - + diff --git a/src/components/debug-modals/user-debug-modal.tsx b/src/components/debug-modals/user-debug-modal.tsx index 073850d86..a141240a0 100644 --- a/src/components/debug-modals/user-debug-modal.tsx +++ b/src/components/debug-modals/user-debug-modal.tsx @@ -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) { - 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; diff --git a/src/components/event-verification-icon.tsx b/src/components/event-verification-icon.tsx index 0aec2c194..c870a99be 100644 --- a/src/components/event-verification-icon.tsx +++ b/src/components/event-verification-icon.tsx @@ -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 ; } return ; } +export default memo(EventVerificationIcon); diff --git a/src/components/note-link.tsx b/src/components/note-link.tsx index 68b276031..1e54e4992 100644 --- a/src/components/note-link.tsx +++ b/src/components/note-link.tsx @@ -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"; diff --git a/src/components/note/buttons/quote-repost-button.tsx b/src/components/note/buttons/quote-repost-button.tsx index f6fc1ff8b..f37f1fdd3 100644 --- a/src/components/note/buttons/quote-repost-button.tsx +++ b/src/components/note/buttons/quote-repost-button.tsx @@ -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 }) { diff --git a/src/components/note/buttons/reaction-button.tsx b/src/components/note/buttons/reaction-button.tsx index 2f446e60d..2a6216fe7 100644 --- a/src/components/note/buttons/reaction-button.tsx +++ b/src/components/note/buttons/reaction-button.tsx @@ -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) { +export default function ReactionButton({ + event: note, + ...props +}: { event: NostrEvent } & Omit) { const { requestSignature } = useSigningContext(); const account = useCurrentAccount(); diff --git a/src/components/note/buttons/reply-button.tsx b/src/components/note/buttons/reply-button.tsx index 40b9813d8..389ae20b3 100644 --- a/src/components/note/buttons/reply-button.tsx +++ b/src/components/note/buttons/reply-button.tsx @@ -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 }) { diff --git a/src/components/note/buttons/repost-button.tsx b/src/components/note/buttons/repost-button.tsx index e7ba897c6..4cb39f66b 100644 --- a/src/components/note/buttons/repost-button.tsx +++ b/src/components/note/buttons/repost-button.tsx @@ -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"; diff --git a/src/components/note/embedded-note.tsx b/src/components/note/embedded-note.tsx index a320cef75..ae794b314 100644 --- a/src/components/note/embedded-note.tsx +++ b/src/components/note/embedded-note.tsx @@ -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 ( - + - - - + + + - {showSignatureVerification && } - - {dayjs.unix(note.created_at).fromNow()} + {showSignatureVerification && } + + {dayjs.unix(event.created_at).fromNow()} - {expand.isOpen && } + {expand.isOpen && } ); diff --git a/src/components/note/index.tsx b/src/components/note/index.tsx index 6e1b7bc4c..ebce5da42 100644 --- a/src/components/note/index.tsx +++ b/src/components/note/index.tsx @@ -70,8 +70,8 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => { - - {showReactions && } + + {showReactions && } {externalLink && ( diff --git a/src/components/note/note-menu.tsx b/src/components/note/note-menu.tsx index cf3799e08..179829bfd 100644 --- a/src/components/note/note-menu.tsx +++ b/src/components/note/note-menu.tsx @@ -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) => { @@ -30,7 +25,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit { const missingRelays = clientRelaysService.getWriteUrls(); diff --git a/src/components/note/note-relays.tsx b/src/components/note/note-relays.tsx index daa3fb090..71f275649 100644 --- a/src/components/note/note-relays.tsx +++ b/src/components/note/note-relays.tsx @@ -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 = { diff --git a/src/components/note/note-zap-button.tsx b/src/components/note/note-zap-button.tsx index 3f6691187..aac1afd8b 100644 --- a/src/components/note/note-zap-button.tsx +++ b/src/components/note/note-zap-button.tsx @@ -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) { +}: { event: NostrEvent; allowComment?: boolean; showEventPreview?: boolean } & Omit) { 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({ diff --git a/src/components/note/quote-note.tsx b/src/components/note/quote-note.tsx index 666c62c02..de769d684 100644 --- a/src/components/note/quote-note.tsx +++ b/src/components/note/quote-note.tsx @@ -7,7 +7,7 @@ const QuoteNote = ({ noteId, relays }: { noteId: string; relays?: string[] }) => const readRelays = useReadRelayUrls(relays); const { event, loading } = useSingleEvent(noteId, readRelays); - return event ? : ; + return event ? : ; }; export default QuoteNote; diff --git a/src/components/post-modal/index.tsx b/src/components/post-modal/index.tsx index 31de4c5ee..9063e1c9f 100644 --- a/src/components/post-modal/index.tsx +++ b/src/components/post-modal/index.tsx @@ -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"; diff --git a/src/components/user-avatar-link.tsx b/src/components/user-avatar-link.tsx index 5a8197116..12aa77d24 100644 --- a/src/components/user-avatar-link.tsx +++ b/src/components/user-avatar-link.tsx @@ -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) => ( - + )); diff --git a/src/components/user-avatar.tsx b/src/components/user-avatar.tsx index 0ba042c90..67856c6a1 100644 --- a/src/components/user-avatar.tsx +++ b/src/components/user-avatar.tsx @@ -16,11 +16,12 @@ export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => { export type UserAvatarProps = Omit & { 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); diff --git a/src/components/user-link.tsx b/src/components/user-link.tsx index a5d2e3ea9..40c19e588 100644 --- a/src/components/user-link.tsx +++ b/src/components/user-link.tsx @@ -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 ( - + {showAt && "@"} {getUserDisplayName(metadata, pubkey)} diff --git a/src/components/zap-modal.tsx b/src/components/zap-modal.tsx index 08eee1283..177837570 100644 --- a/src/components/zap-modal.tsx +++ b/src/components/zap-modal.tsx @@ -183,7 +183,7 @@ export default function ZapModal({ {stream.image && } )} - {showEventPreview && event && } + {showEventPreview && event && } {allowComment && (canZap || lnurlMetadata?.commentAllowed) && ( { 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 }); +} diff --git a/src/helpers/nostr-apps.ts b/src/helpers/nostr/apps.ts similarity index 100% rename from src/helpers/nostr-apps.ts rename to src/helpers/nostr/apps.ts diff --git a/src/helpers/nostr/event.ts b/src/helpers/nostr/events.ts similarity index 93% rename from src/helpers/nostr/event.ts rename to src/helpers/nostr/events.ts index add1cbdb8..c04b2a3b4 100644 --- a/src/helpers/nostr/event.ts +++ b/src/helpers/nostr/events.ts @@ -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 & { + 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, }; } diff --git a/src/helpers/nostr/lists.ts b/src/helpers/nostr/lists.ts new file mode 100644 index 000000000..4fa01df20 --- /dev/null +++ b/src/helpers/nostr/lists.ts @@ -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; +} diff --git a/src/helpers/nostr/post.ts b/src/helpers/nostr/post.ts index 92b8bf256..c1502551e 100644 --- a/src/helpers/nostr/post.ts +++ b/src/helpers/nostr/post.ts @@ -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"; diff --git a/src/helpers/nostr/stream.ts b/src/helpers/nostr/stream.ts index 8cc5cde33..44cad6775 100644 --- a/src/helpers/nostr/stream.ts +++ b/src/helpers/nostr/stream.ts @@ -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) { diff --git a/src/helpers/thread.ts b/src/helpers/thread.ts index f69c5e9e9..24bbcaad2 100644 --- a/src/helpers/thread.ts +++ b/src/helpers/thread.ts @@ -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; diff --git a/src/helpers/user-metadata.ts b/src/helpers/user-metadata.ts index e5a04d76a..26687aca5 100644 --- a/src/helpers/user-metadata.ts +++ b/src/helpers/user-metadata.ts @@ -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)) ); } diff --git a/src/hooks/use-replaceable-event.ts b/src/hooks/use-replaceable-event.ts new file mode 100644 index 000000000..3a7605a0d --- /dev/null +++ b/src/hooks/use-replaceable-event.ts @@ -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); +} diff --git a/src/providers/delete-event-provider.tsx b/src/providers/delete-event-provider.tsx index 8b0b87913..ae1c0bebb 100644 --- a/src/providers/delete-event-provider.tsx +++ b/src/providers/delete-event-provider.tsx @@ -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 ( diff --git a/src/providers/notification-timeline.tsx b/src/providers/notification-timeline.tsx index 8c315352e..3f3462e1f 100644 --- a/src/providers/notification-timeline.tsx +++ b/src/providers/notification-timeline.tsx @@ -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"; diff --git a/src/services/event-reactions.ts b/src/services/event-reactions.ts index e3b9d60c1..d654b23d5 100644 --- a/src/services/event-reactions.ts +++ b/src/services/event-reactions.ts @@ -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; diff --git a/src/services/event-relays.ts b/src/services/event-relays.ts index 21b6a9e2c..1dbe842cb 100644 --- a/src/services/event-relays.ts +++ b/src/services/event-relays.ts @@ -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"; diff --git a/src/services/event-zaps.ts b/src/services/event-zaps.ts index 21850bbe3..35a3f8bfe 100644 --- a/src/services/event-zaps.ts +++ b/src/services/event-zaps.ts @@ -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; diff --git a/src/services/lists.ts b/src/services/lists.ts index 6a71ed2e8..05ad54d0a 100644 --- a/src/services/lists.ts +++ b/src/services/lists.ts @@ -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) { diff --git a/src/services/replaceable-event-requester.ts b/src/services/replaceable-event-requester.ts index 459799bd6..aa2c991b6 100644 --- a/src/services/replaceable-event-requester.ts +++ b/src/services/replaceable-event-requester.ts @@ -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 = {}; - 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>(); - 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); } diff --git a/src/services/user-relays.ts b/src/services/user-relays.ts index 5e76d0ada..a8fec1145 100644 --- a/src/services/user-relays.ts +++ b/src/services/user-relays.ts @@ -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"; diff --git a/src/views/hashtag/index.tsx b/src/views/hashtag/index.tsx index 32334ea20..85ee915fa 100644 --- a/src/views/hashtag/index.tsx +++ b/src/views/hashtag/index.tsx @@ -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"; diff --git a/src/views/home/following-tab.tsx b/src/views/home/following-tab.tsx index ca45a0aaa..710bdbb32 100644 --- a/src/views/home/following-tab.tsx +++ b/src/views/home/following-tab.tsx @@ -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"; diff --git a/src/views/home/global-tab.tsx b/src/views/home/global-tab.tsx index 32d0d2579..2cf873be0 100644 --- a/src/views/home/global-tab.tsx +++ b/src/views/home/global-tab.tsx @@ -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"; diff --git a/src/views/lists/components/list-card.tsx b/src/views/lists/components/list-card.tsx new file mode 100644 index 000000000..98f6507d2 --- /dev/null +++ b/src/views/lists/components/list-card.tsx @@ -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 ( + + + + + {getListName(event)} + + + + Created by: + + + + + + {people.length > 0 && ( + <> + {people.length} people + + {people.map(({ pubkey, relay }) => ( + + ))} + + + )} + + + ); +} diff --git a/src/views/lists/index.tsx b/src/views/lists/index.tsx index d00bb14b9..35f469bf9 100644 --- a/src/views/lists/index.tsx +++ b/src/views/lists/index.tsx @@ -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 ( <> + + {Array.from(Object.entries(lists)).map(([name, list]) => ( - + ))} ); } 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 ( @@ -40,7 +54,11 @@ function ListsPage() { - + + + {events.map((event) => ( + + ))} ); } diff --git a/src/views/lists/list.tsx b/src/views/lists/list.tsx index e32da8668..20c6a6df4 100644 --- a/src/views/lists/list.tsx +++ b/src/views/lists/list.tsx @@ -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 + Looking for list "{coordinate.identifier}" created by ); - const isAuthor = account?.pubkey === list.author; + const isAuthor = account?.pubkey === event.pubkey; + const people = getPubkeysFromList(event); return ( @@ -53,17 +62,14 @@ export default function ListView() { - {list.name} + {getListName(event)} {isAuthor && ( - )} - {people.map(({ pubkey, relay }) => ( diff --git a/src/views/login/nsec.tsx b/src/views/login/nsec.tsx index 89a4006cb..661930c6b 100644 --- a/src/views/login/nsec.tsx +++ b/src/views/login/nsec.tsx @@ -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); diff --git a/src/views/messages/chat.tsx b/src/views/messages/chat.tsx index 61ce6d674..a8bccbcdd 100644 --- a/src/views/messages/chat.tsx +++ b/src/views/messages/chat.tsx @@ -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"; diff --git a/src/views/messages/index.tsx b/src/views/messages/index.tsx index e9d2d0a2d..9c75e2487 100644 --- a/src/views/messages/index.tsx +++ b/src/views/messages/index.tsx @@ -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 ( @@ -40,7 +39,7 @@ function ContactCard({ pubkey }: { pubkey: string }) { {messages[0] && {dayjs.unix(messages[0].created_at).fromNow()}} - + ); } diff --git a/src/views/notifications/index.tsx b/src/views/notifications/index.tsx index e80a6de88..fd1fa87d0 100644 --- a/src/views/notifications/index.tsx +++ b/src/views/notifications/index.tsx @@ -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 }) => ( diff --git a/src/views/relays/relay/relay-notes.tsx b/src/views/relays/relay/relay-notes.tsx index 9b90fbfd5..78b8fdd70 100644 --- a/src/views/relays/relay/relay-notes.tsx +++ b/src/views/relays/relay/relay-notes.tsx @@ -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"; diff --git a/src/views/streams/components/streamer-cards.tsx b/src/views/streams/components/streamer-cards.tsx index ab7622b43..aee0a67d3 100644 --- a/src/views/streams/components/streamer-cards.tsx +++ b/src/views/streams/components/streamer-cards.tsx @@ -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]; diff --git a/src/views/streams/stream/stream-chat/chat-message.tsx b/src/views/streams/stream/stream-chat/chat-message.tsx index 3b580bd36..5e6e7c1c9 100644 --- a/src/views/streams/stream/stream-chat/chat-message.tsx +++ b/src/views/streams/stream/stream-chat/chat-message.tsx @@ -16,7 +16,7 @@ function ChatMessage({ event, stream }: { event: NostrEvent; stream: ParsedStrea return ( - + diff --git a/src/views/streams/stream/stream-chat/index.tsx b/src/views/streams/stream/stream-chat/index.tsx index 2d76aebd7..8da5e564e 100644 --- a/src/views/streams/stream/stream-chat/index.tsx +++ b/src/views/streams/stream/stream-chat/index.tsx @@ -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"; diff --git a/src/views/tools/index.tsx b/src/views/tools/index.tsx index c1e482676..cc13c7104 100644 --- a/src/views/tools/index.tsx +++ b/src/views/tools/index.tsx @@ -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 ( diff --git a/src/views/user/about.tsx b/src/views/user/about.tsx index 553b4219f..f47e7bf35 100644 --- a/src/views/user/about.tsx +++ b/src/views/user/about.tsx @@ -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]); diff --git a/src/views/user/components/header.tsx b/src/views/user/components/header.tsx index 09a35ca23..f0f9b536a 100644 --- a/src/views/user/components/header.tsx +++ b/src/views/user/components/header.tsx @@ -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"; diff --git a/src/views/user/components/share-qr-button.tsx b/src/views/user/components/share-qr-button.tsx index af3f9a3c1..0841b15f1 100644 --- a/src/views/user/components/share-qr-button.tsx +++ b/src/views/user/components/share-qr-button.tsx @@ -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) => { 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; diff --git a/src/views/user/components/user-card.tsx b/src/views/user/components/user-card.tsx index 8aca42d45..bfd796483 100644 --- a/src/views/user/components/user-card.tsx +++ b/src/views/user/components/user-card.tsx @@ -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) => { > - + {getUserDisplayName(metadata, pubkey)} diff --git a/src/views/user/components/user-profile-menu.tsx b/src/views/user/components/user-profile-menu.tsx index f4f045fa3..ce76b0e34 100644 --- a/src/views/user/components/user-profile-menu.tsx +++ b/src/views/user/components/user-profile-menu.tsx @@ -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, diff --git a/src/views/user/followers.tsx b/src/views/user/followers.tsx index fd1b49f9b..6f0bc79db 100644 --- a/src/views/user/followers.tsx +++ b/src/views/user/followers.tsx @@ -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"; diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index db804b2c8..4b759f5e1 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -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 = () => { {tabs.map(({ label }) => ( - }> - - + + }> + + + ))} diff --git a/src/views/user/likes.tsx b/src/views/user/likes.tsx index 4e2a36316..938f2749a 100644 --- a/src/views/user/likes.tsx +++ b/src/views/user/likes.tsx @@ -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"; diff --git a/src/views/user/lists.tsx b/src/views/user/lists.tsx new file mode 100644 index 000000000..735da4f9a --- /dev/null +++ b/src/views/user/lists.tsx @@ -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 ( + + + + {events.map((event) => ( + + ))} + + ); +} diff --git a/src/views/user/notes.tsx b/src/views/user/notes.tsx index f9ddc1842..c4de5f9cf 100644 --- a/src/views/user/notes.tsx +++ b/src/views/user/notes.tsx @@ -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"; diff --git a/src/views/user/relays.tsx b/src/views/user/relays.tsx index 725081eae..08b2f54e7 100644 --- a/src/views/user/relays.tsx +++ b/src/views/user/relays.tsx @@ -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"; diff --git a/src/views/user/reports.tsx b/src/views/user/reports.tsx index de0785dda..c52640d57 100644 --- a/src/views/user/reports.tsx +++ b/src/views/user/reports.tsx @@ -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"; diff --git a/src/views/user/streams.tsx b/src/views/user/streams.tsx index c878a2a34..6e7d424bc 100644 --- a/src/views/user/streams.tsx +++ b/src/views/user/streams.tsx @@ -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"; diff --git a/src/views/user/zaps.tsx b/src/views/user/zaps.tsx index b66b6a639..4eec31707 100644 --- a/src/views/user/zaps.tsx +++ b/src/views/user/zaps.tsx @@ -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";