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 (
-
+
-
-
-
+
+
+
: }>
Expand
- {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() {
}>New List
-
+
+
+ {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";