mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-01 15:06:19 +02:00
cleanup helps and components
This commit is contained in:
@@ -38,6 +38,7 @@ import RelayView from "./views/relays/relay";
|
||||
import RelayReviewsView from "./views/relays/reviews";
|
||||
import ListsView from "./views/lists";
|
||||
import ListView from "./views/lists/list";
|
||||
import UserListsTab from "./views/user/lists";
|
||||
|
||||
const StreamsView = React.lazy(() => import("./views/streams"));
|
||||
const StreamView = React.lazy(() => import("./views/streams/stream"));
|
||||
@@ -96,6 +97,7 @@ const router = createHashRouter([
|
||||
{ path: "streams", element: <UserStreamsTab /> },
|
||||
{ path: "zaps", element: <UserZapsTab /> },
|
||||
{ path: "likes", element: <UserLikesTab /> },
|
||||
{ path: "lists", element: <UserListsTab /> },
|
||||
{ path: "followers", element: <UserFollowersTab /> },
|
||||
{ path: "following", element: <UserFollowingTab /> },
|
||||
{ path: "relays", element: <UserRelaysTab /> },
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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";
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Flex } from "@chakra-ui/react";
|
||||
import { ModalProps } from "@chakra-ui/react";
|
||||
import { Bech32Prefix, hexToBech32 } from "../../helpers/nip19";
|
||||
import { getReferences } from "../../helpers/nostr/event";
|
||||
import { getReferences } from "../../helpers/nostr/events";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import RawJson from "./raw-json";
|
||||
import RawValue from "./raw-value";
|
||||
import RawPre from "./raw-pre";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
export default function NoteDebugModal({ event, ...props }: { event: NostrEvent } & Omit<ModalProps, "children">) {
|
||||
return (
|
||||
@@ -16,7 +16,7 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent
|
||||
<ModalBody p="4">
|
||||
<Flex gap="2" direction="column">
|
||||
<RawValue heading="Event Id" value={event.id} />
|
||||
<RawValue heading="Encoded id (NIP-19)" value={hexToBech32(event.id, Bech32Prefix.Note) ?? "failed"} />
|
||||
<RawValue heading="Encoded id (NIP-19)" value={nip19.noteEncode(event.id)} />
|
||||
<RawPre heading="Content" value={event.content} />
|
||||
<RawJson heading="JSON" json={event} />
|
||||
<RawJson heading="References" json={getReferences(event)} />
|
||||
|
@@ -1,17 +1,15 @@
|
||||
import { useMemo } from "react";
|
||||
import { Flex, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay } from "@chakra-ui/react";
|
||||
import { ModalProps } from "@chakra-ui/react";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import RawValue from "./raw-value";
|
||||
import RawJson from "./raw-json";
|
||||
import { useSharableProfileId } from "../../hooks/use-shareable-profile-id";
|
||||
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
|
||||
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
|
||||
export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit<ModalProps, "children">) {
|
||||
const npub = useMemo(() => normalizeToBech32(pubkey, Bech32Prefix.Pubkey), [pubkey]);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const nprofile = useSharableProfileId(pubkey);
|
||||
const relays = replaceableEventLoaderService.getEvent(Kind.RelayList, pubkey).value;
|
||||
|
@@ -1,13 +1,17 @@
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { memo } from "react";
|
||||
import { verifySignature } from "nostr-tools";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { CheckIcon, VerificationFailed } from "./icons";
|
||||
import useAppSettings from "../hooks/use-app-settings";
|
||||
|
||||
export default function EventVerificationIcon({ event }: { event: NostrEvent }) {
|
||||
const valid = useMemo(() => verifySignature(event), [event]);
|
||||
function EventVerificationIcon({ event }: { event: NostrEvent }) {
|
||||
const { showSignatureVerification } = useAppSettings();
|
||||
if (!showSignatureVerification) return null;
|
||||
|
||||
if (!valid) {
|
||||
if (!verifySignature(event)) {
|
||||
return <VerificationFailed color="red.500" />;
|
||||
}
|
||||
return <CheckIcon color="green.500" />;
|
||||
}
|
||||
export default memo(EventVerificationIcon);
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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 }) {
|
||||
|
@@ -13,7 +13,10 @@ import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import { LikeIcon } from "../../icons";
|
||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||
|
||||
export default function ReactionButton({ note, ...props }: { note: NostrEvent } & Omit<ButtonProps, "children">) {
|
||||
export default function ReactionButton({
|
||||
event: note,
|
||||
...props
|
||||
}: { event: NostrEvent } & Omit<ButtonProps, "children">) {
|
||||
const { requestSignature } = useSigningContext();
|
||||
const account = useCurrentAccount();
|
||||
|
||||
|
@@ -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 }) {
|
||||
|
@@ -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";
|
||||
|
@@ -13,27 +13,27 @@ import { TrustProvider } from "../../providers/trust";
|
||||
import { NoteLink } from "../note-link";
|
||||
import { ArrowDownSIcon, ArrowUpSIcon } from "../icons";
|
||||
|
||||
export default function EmbeddedNote({ note }: { note: NostrEvent }) {
|
||||
export default function EmbeddedNote({ event }: { event: NostrEvent }) {
|
||||
const { showSignatureVerification } = useSubject(appSettings);
|
||||
const expand = useDisclosure();
|
||||
|
||||
return (
|
||||
<TrustProvider event={note}>
|
||||
<TrustProvider event={event}>
|
||||
<Card variant="outline">
|
||||
<CardHeader padding="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={note.pubkey} size="sm" />
|
||||
<UserLink pubkey={note.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
|
||||
<UserDnsIdentityIcon pubkey={note.pubkey} onlyIcon />
|
||||
<UserAvatarLink pubkey={event.pubkey} size="sm" />
|
||||
<UserLink pubkey={event.pubkey} fontWeight="bold" isTruncated fontSize="lg" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<Button size="sm" onClick={expand.onToggle} leftIcon={expand.isOpen ? <ArrowUpSIcon /> : <ArrowDownSIcon />}>
|
||||
Expand
|
||||
</Button>
|
||||
<Spacer />
|
||||
{showSignatureVerification && <EventVerificationIcon event={note} />}
|
||||
<NoteLink noteId={note.id} color="current" whiteSpace="nowrap">
|
||||
{dayjs.unix(note.created_at).fromNow()}
|
||||
{showSignatureVerification && <EventVerificationIcon event={event} />}
|
||||
<NoteLink noteId={event.id} color="current" whiteSpace="nowrap">
|
||||
{dayjs.unix(event.created_at).fromNow()}
|
||||
</NoteLink>
|
||||
</CardHeader>
|
||||
<CardBody p="0">{expand.isOpen && <NoteContents px="2" event={note} />}</CardBody>
|
||||
<CardBody p="0">{expand.isOpen && <NoteContents px="2" event={event} />}</CardBody>
|
||||
</Card>
|
||||
</TrustProvider>
|
||||
);
|
||||
|
@@ -70,8 +70,8 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
|
||||
<ButtonGroup size="sm" variant="link">
|
||||
<RepostButton event={event} />
|
||||
<QuoteRepostButton event={event} />
|
||||
<NoteZapButton note={event} size="sm" />
|
||||
{showReactions && <ReactionButton note={event} size="sm" />}
|
||||
<NoteZapButton event={event} size="sm" />
|
||||
{showReactions && <ReactionButton event={event} size="sm" />}
|
||||
</ButtonGroup>
|
||||
<Box flexGrow={1} />
|
||||
{externalLink && (
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { useCallback } from "react";
|
||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { Bech32Prefix, getSharableNoteId, normalizeToBech32 } from "../../helpers/nip19";
|
||||
import { getSharableNoteId } from "../../helpers/nip19";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
|
||||
|
||||
@@ -10,16 +11,10 @@ import { ClipboardIcon, CodeIcon, ExternalLinkIcon, LikeIcon, RelayIcon, RepostI
|
||||
import NoteReactionsModal from "./note-zaps-modal";
|
||||
import NoteDebugModal from "../debug-modals/note-debug-modal";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useCallback, useState } from "react";
|
||||
import QuoteNote from "./quote-note";
|
||||
import { buildDeleteEvent } from "../../helpers/nostr/event";
|
||||
import signingService from "../../services/signing";
|
||||
import { buildAppSelectUrl } from "../../helpers/nostr-apps";
|
||||
import { buildAppSelectUrl } from "../../helpers/nostr/apps";
|
||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||
import { nostrPostAction } from "../../classes/nostr-post-action";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { handleEventFromRelay } from "../../services/event-relays";
|
||||
import relayPoolService from "../../services/relay-pool";
|
||||
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||
|
||||
export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) => {
|
||||
@@ -30,7 +25,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
|
||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||
const noteId = normalizeToBech32(event.id, Bech32Prefix.Note);
|
||||
const noteId = nip19.noteEncode(event.id);
|
||||
|
||||
const broadcast = useCallback(() => {
|
||||
const missingRelays = clientRelaysService.getWriteUrls();
|
||||
|
@@ -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 = {
|
||||
|
@@ -12,15 +12,15 @@ import { useInvoiceModalContext } from "../../providers/invoice-modal";
|
||||
import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
|
||||
|
||||
export default function NoteZapButton({
|
||||
note,
|
||||
event,
|
||||
allowComment,
|
||||
showEventPreview,
|
||||
...props
|
||||
}: { note: NostrEvent; allowComment?: boolean; showEventPreview?: boolean } & Omit<ButtonProps, "children">) {
|
||||
}: { event: NostrEvent; allowComment?: boolean; showEventPreview?: boolean } & Omit<ButtonProps, "children">) {
|
||||
const account = useCurrentAccount();
|
||||
const { metadata } = useUserLNURLMetadata(note.pubkey);
|
||||
const { metadata } = useUserLNURLMetadata(event.pubkey);
|
||||
const { requestPay } = useInvoiceModalContext();
|
||||
const zaps = useEventZaps(note.id);
|
||||
const zaps = useEventZaps(event.id);
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const hasZapped = !!account && zaps.some((zap) => zap.request.pubkey === account.pubkey);
|
||||
@@ -28,7 +28,7 @@ export default function NoteZapButton({
|
||||
const handleInvoice = async (invoice: string) => {
|
||||
onClose();
|
||||
await requestPay(invoice);
|
||||
eventZapsService.requestZaps(note.id, clientRelaysService.getReadUrls(), true);
|
||||
eventZapsService.requestZaps(event.id, clientRelaysService.getReadUrls(), true);
|
||||
};
|
||||
|
||||
const total = totalZaps(zaps);
|
||||
@@ -62,9 +62,9 @@ export default function NoteZapButton({
|
||||
<ZapModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
event={note}
|
||||
event={event}
|
||||
onInvoice={handleInvoice}
|
||||
pubkey={note.pubkey}
|
||||
pubkey={event.pubkey}
|
||||
allowComment={allowComment}
|
||||
showEventPreview={showEventPreview}
|
||||
/>
|
||||
|
@@ -7,7 +7,7 @@ const QuoteNote = ({ noteId, relays }: { noteId: string; relays?: string[] }) =>
|
||||
const readRelays = useReadRelayUrls(relays);
|
||||
const { event, loading } = useSingleEvent(noteId, readRelays);
|
||||
|
||||
return event ? <EmbeddedNote note={event} /> : <NoteLink noteId={noteId} />;
|
||||
return event ? <EmbeddedNote event={event} /> : <NoteLink noteId={noteId} />;
|
||||
};
|
||||
|
||||
export default QuoteNote;
|
||||
|
@@ -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";
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { UserAvatar, UserAvatarProps } from "./user-avatar";
|
||||
|
||||
export const UserAvatarLink = React.memo(({ pubkey, ...props }: UserAvatarProps) => (
|
||||
<Link to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
||||
<Link to={`/u/${nip19.npubEncode(pubkey)}`}>
|
||||
<UserAvatar pubkey={pubkey} {...props} />
|
||||
</Link>
|
||||
));
|
||||
|
@@ -16,11 +16,12 @@ export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => {
|
||||
|
||||
export type UserAvatarProps = Omit<AvatarProps, "src"> & {
|
||||
pubkey: string;
|
||||
relay?: string;
|
||||
noProxy?: boolean;
|
||||
};
|
||||
export const UserAvatar = React.memo(({ pubkey, noProxy, ...props }: UserAvatarProps) => {
|
||||
export const UserAvatar = React.memo(({ pubkey, noProxy, relay, ...props }: UserAvatarProps) => {
|
||||
const { imageProxy, proxyUserMedia } = useSubject(appSettings);
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const metadata = useUserMetadata(pubkey, relay ? [relay] : undefined);
|
||||
const picture = useMemo(() => {
|
||||
if (metadata?.picture) {
|
||||
const src = safeUrl(metadata?.picture);
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { Link, LinkProps } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
|
||||
@@ -11,10 +12,9 @@ export type UserLinkProps = LinkProps & {
|
||||
|
||||
export const UserLink = ({ pubkey, showAt, ...props }: UserLinkProps) => {
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||
|
||||
return (
|
||||
<Link as={RouterLink} to={`/u/${npub}`} whiteSpace="nowrap" {...props}>
|
||||
<Link as={RouterLink} to={`/u/${nip19.npubEncode(pubkey)}`} whiteSpace="nowrap" {...props}>
|
||||
{showAt && "@"}
|
||||
{getUserDisplayName(metadata, pubkey)}
|
||||
</Link>
|
||||
|
@@ -183,7 +183,7 @@ export default function ZapModal({
|
||||
{stream.image && <Image src={stream.image} />}
|
||||
</Box>
|
||||
)}
|
||||
{showEventPreview && event && <EmbeddedNote note={event} />}
|
||||
{showEventPreview && event && <EmbeddedNote event={event} />}
|
||||
|
||||
{allowComment && (canZap || lnurlMetadata?.commentAllowed) && (
|
||||
<Input
|
||||
|
@@ -2,19 +2,15 @@ import { bech32 } from "bech32";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { getEventRelays } from "../services/event-relays";
|
||||
import relayScoreboardService from "../services/relay-scoreboard";
|
||||
import { NostrEvent, isDTag } from "../types/nostr-event";
|
||||
import { getEventUID } from "./nostr/events";
|
||||
|
||||
export function isHex(key?: string) {
|
||||
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export enum Bech32Prefix {
|
||||
Pubkey = "npub",
|
||||
SecKey = "nsec",
|
||||
Note = "note",
|
||||
Profile = "nprofile",
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function isBech32Key(bech32String: string) {
|
||||
try {
|
||||
const { prefix } = bech32.decode(bech32String.toLowerCase());
|
||||
@@ -26,6 +22,7 @@ export function isBech32Key(bech32String: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function bech32ToHex(bech32String: string) {
|
||||
try {
|
||||
const { words } = bech32.decode(bech32String);
|
||||
@@ -34,16 +31,7 @@ export function bech32ToHex(bech32String: string) {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function hexToBech32(hex: string, prefix: Bech32Prefix) {
|
||||
try {
|
||||
const hexArray = hexStringToUint8(hex);
|
||||
return hexArray && bech32.encode(prefix, bech32.toWords(hexArray));
|
||||
} catch (error) {
|
||||
// continue
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
export function toHexString(buffer: Uint8Array) {
|
||||
return buffer.reduce((s, byte) => {
|
||||
let hex = byte.toString(16);
|
||||
@@ -52,28 +40,13 @@ export function toHexString(buffer: Uint8Array) {
|
||||
}, "");
|
||||
}
|
||||
|
||||
export function hexStringToUint8(str: string) {
|
||||
if (str.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(str)) {
|
||||
return null;
|
||||
}
|
||||
let buffer = new Uint8Array(str.length / 2);
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
buffer[i] = parseInt(str.substr(2 * i, 2), 16);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function safeDecode(str: string) {
|
||||
try {
|
||||
return nip19.decode(str);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function normalizeToBech32(key: string, prefix: Bech32Prefix = Bech32Prefix.Pubkey) {
|
||||
if (isHex(key)) return hexToBech32(key, prefix);
|
||||
if (isBech32Key(key)) return key;
|
||||
return null;
|
||||
}
|
||||
/** @deprecated */
|
||||
export function normalizeToHex(hex: string) {
|
||||
if (isHex(hex)) return hex;
|
||||
if (isBech32Key(hex)) return bech32ToHex(hex);
|
||||
@@ -89,3 +62,15 @@ export function getSharableNoteId(eventId: string) {
|
||||
return nip19.neventEncode({ id: eventId, relays: onlyTwo });
|
||||
} else return nip19.noteEncode(eventId);
|
||||
}
|
||||
|
||||
export function getSharableEventNaddr(event: NostrEvent) {
|
||||
const relays = getEventRelays(getEventUID(event)).value;
|
||||
const ranked = relayScoreboardService.getRankedRelays(relays);
|
||||
const onlyTwo = ranked.slice(0, 2);
|
||||
|
||||
const d = event.tags.find(isDTag)?.[1];
|
||||
|
||||
if (!d) return null;
|
||||
|
||||
return nip19.naddrEncode({ kind: event.kind, identifier: d, pubkey: event.pubkey, relays: onlyTwo });
|
||||
}
|
||||
|
@@ -1,22 +1,14 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
|
||||
import { getEventRelays } from "../../services/event-relays";
|
||||
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag, Tag } from "../../types/nostr-event";
|
||||
import { RelayConfig, RelayMode } from "../../classes/relay";
|
||||
import accountService from "../../services/account";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
import { getMatchNostrLink } from "../regexp";
|
||||
import { getSharableNoteId } from "../nip19";
|
||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||
import { getAddr } from "../../services/replaceable-event-requester";
|
||||
|
||||
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
||||
return event.kind === 1 && !!getReferences(event).replyId;
|
||||
}
|
||||
|
||||
export function isRepost(event: NostrEvent | DraftNostrEvent) {
|
||||
const match = event.content.match(getMatchNostrLink());
|
||||
return event.kind === 6 || (match && match[0].length === event.content.length);
|
||||
}
|
||||
import { AddressPointer } from "nostr-tools/lib/nip19";
|
||||
|
||||
export function truncatedId(str: string, keep = 6) {
|
||||
if (str.length < keep * 2 + 3) return str;
|
||||
@@ -26,11 +18,20 @@ export function truncatedId(str: string, keep = 6) {
|
||||
// used to get a unique Id for each event, should take into account replaceable events
|
||||
export function getEventUID(event: NostrEvent) {
|
||||
if (event.kind >= 30000 && event.kind < 40000) {
|
||||
return getAddr(event.kind, event.pubkey, event.tags.find((t) => t[0] === "d" && t[1])?.[1]);
|
||||
return getEventCoordinate(event);
|
||||
}
|
||||
return event.id;
|
||||
}
|
||||
|
||||
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
||||
return event.kind === 1 && !!getReferences(event).replyId;
|
||||
}
|
||||
|
||||
export function isRepost(event: NostrEvent | DraftNostrEvent) {
|
||||
const match = event.content.match(getMatchNostrLink());
|
||||
return event.kind === 6 || (match && match[0].length === event.content.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns an array of tag indexes that are referenced in the content
|
||||
* either with the legacy #[0] syntax or nostr:xxxxx links
|
||||
@@ -211,7 +212,15 @@ export function parseRTag(tag: RTag): RelayConfig {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCoordinate(a: string) {
|
||||
export function getEventCoordinate(event: NostrEvent) {
|
||||
const d = event.tags.find((t) => t[0] === "d")?.[1];
|
||||
return d ? `${event.kind}:${event.pubkey}:${d}` : `${event.kind}:${event.pubkey}`;
|
||||
}
|
||||
|
||||
export type ParsedCoordinate = Omit<AddressPointer, "identifier"> & {
|
||||
identifier?: string;
|
||||
};
|
||||
export function parseCoordinate(a: string): ParsedCoordinate | null {
|
||||
const parts = a.split(":") as (string | undefined)[];
|
||||
const kind = parts[0] && parseInt(parts[0]);
|
||||
const pubkey = parts[1];
|
||||
@@ -223,6 +232,6 @@ export function parseCoordinate(a: string) {
|
||||
return {
|
||||
kind,
|
||||
pubkey,
|
||||
d,
|
||||
identifier: d,
|
||||
};
|
||||
}
|
41
src/helpers/nostr/lists.ts
Normal file
41
src/helpers/nostr/lists.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import dayjs from "dayjs";
|
||||
import { DraftNostrEvent, NostrEvent, isDTag, isPTag } from "../../types/nostr-event";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
export const PEOPLE_LIST = 30000;
|
||||
export const NOTE_LIST = 30001;
|
||||
export const MUTE_LIST = 10000;
|
||||
export const FOLLOW_LIST = Kind.Contacts;
|
||||
|
||||
export function getListName(event: NostrEvent) {
|
||||
if (event.kind === 3) return "Following";
|
||||
return event.tags.find(isDTag)?.[1];
|
||||
}
|
||||
|
||||
export function getPubkeysFromList(event: NostrEvent) {
|
||||
return event.tags.filter(isPTag).map((t) => ({ pubkey: t[1], relay: t[2] }));
|
||||
}
|
||||
|
||||
export function draftAddPerson(event: NostrEvent, pubkey: string, relay?: string) {
|
||||
if (event.tags.some((t) => t[0] === "p" && t[1] === pubkey)) throw new Error("person already in list");
|
||||
|
||||
const draft: DraftNostrEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
kind: event.kind,
|
||||
content: event.content,
|
||||
tags: [...event.tags, relay ? ["p", pubkey, relay] : ["p", pubkey]],
|
||||
};
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
export function draftRemovePerson(event: NostrEvent, pubkey: string) {
|
||||
const draft: DraftNostrEvent = {
|
||||
created_at: dayjs().unix(),
|
||||
kind: event.kind,
|
||||
content: event.content,
|
||||
tags: event.tags.filter((t) => t[0] !== "p" || t[1] !== pubkey),
|
||||
};
|
||||
|
||||
return draft;
|
||||
}
|
@@ -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";
|
||||
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
@@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
|
21
src/hooks/use-replaceable-event.ts
Normal file
21
src/hooks/use-replaceable-event.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useReadRelayUrls } from "./use-client-relays";
|
||||
import { useMemo } from "react";
|
||||
import replaceableEventLoaderService from "../services/replaceable-event-requester";
|
||||
import { ParsedCoordinate, parseCoordinate } from "../helpers/nostr/events";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export default function useReplaceableEvent(cord: string | ParsedCoordinate, additionalRelays: string[] = []) {
|
||||
const readRelays = useReadRelayUrls(additionalRelays);
|
||||
const sub = useMemo(() => {
|
||||
const parsed = typeof cord === "string" ? parseCoordinate(cord) : cord;
|
||||
if (!parsed) return;
|
||||
return replaceableEventLoaderService.requestEvent(
|
||||
parsed.relays ? [...readRelays, ...parsed.relays] : readRelays,
|
||||
parsed.kind,
|
||||
parsed.pubkey,
|
||||
parsed.identifier,
|
||||
);
|
||||
}, [cord, readRelays.join("|")]);
|
||||
|
||||
return useSubject(sub);
|
||||
}
|
@@ -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 (
|
||||
|
@@ -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";
|
||||
|
@@ -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;
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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) {
|
||||
|
@@ -8,14 +8,15 @@ import { NostrQuery } from "../types/nostr-query";
|
||||
import { logger } from "../helpers/debug";
|
||||
import db from "./db";
|
||||
import { nameOrPubkey } from "./user-metadata";
|
||||
import { getEventCoordinate } from "../helpers/nostr/events";
|
||||
|
||||
type Pubkey = string;
|
||||
type Relay = string;
|
||||
|
||||
export function getReadableAddr(kind: number, pubkey: string, d?: string) {
|
||||
export function getReadableCoordinate(kind: number, pubkey: string, d?: string) {
|
||||
return `${kind}:${nameOrPubkey(pubkey)}${d ? ":" + d : ""}`;
|
||||
}
|
||||
export function getAddr(kind: number, pubkey: string, d?: string) {
|
||||
export function createCoordinate(kind: number, pubkey: string, d?: string) {
|
||||
return `${kind}:${pubkey}${d ? ":" + d : ""}`;
|
||||
}
|
||||
|
||||
@@ -38,13 +39,12 @@ class ReplaceableEventRelayLoader {
|
||||
}
|
||||
|
||||
private handleEvent(event: NostrEvent) {
|
||||
const d = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
||||
const addr = getAddr(event.kind, event.pubkey, d);
|
||||
const cord = getEventCoordinate(event);
|
||||
|
||||
// remove the pubkey from the waiting list
|
||||
this.requested.delete(addr);
|
||||
this.requested.delete(cord);
|
||||
|
||||
const sub = this.events.get(addr);
|
||||
const sub = this.events.get(cord);
|
||||
|
||||
const current = sub.value;
|
||||
if (!current || event.created_at > current.created_at) {
|
||||
@@ -57,15 +57,15 @@ class ReplaceableEventRelayLoader {
|
||||
}
|
||||
|
||||
getEvent(kind: number, pubkey: string, d?: string) {
|
||||
return this.events.get(getAddr(kind, pubkey, d));
|
||||
return this.events.get(createCoordinate(kind, pubkey, d));
|
||||
}
|
||||
|
||||
requestEvent(kind: number, pubkey: string, d?: string) {
|
||||
const addr = getAddr(kind, pubkey, d);
|
||||
const event = this.events.get(addr);
|
||||
const cord = createCoordinate(kind, pubkey, d);
|
||||
const event = this.events.get(cord);
|
||||
|
||||
if (!event.value) {
|
||||
this.requestNext.add(addr);
|
||||
this.requestNext.add(cord);
|
||||
}
|
||||
|
||||
return event;
|
||||
@@ -73,9 +73,9 @@ class ReplaceableEventRelayLoader {
|
||||
|
||||
update() {
|
||||
let needsUpdate = false;
|
||||
for (const addr of this.requestNext) {
|
||||
if (!this.requested.has(addr)) {
|
||||
this.requested.set(addr, new Date());
|
||||
for (const cord of this.requestNext) {
|
||||
if (!this.requested.has(cord)) {
|
||||
this.requested.set(cord, new Date());
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
@@ -83,9 +83,9 @@ class ReplaceableEventRelayLoader {
|
||||
|
||||
// prune requests
|
||||
const timeout = dayjs().subtract(1, "minute");
|
||||
for (const [addr, date] of this.requested) {
|
||||
for (const [cord, date] of this.requested) {
|
||||
if (dayjs(date).isBefore(timeout)) {
|
||||
this.requested.delete(addr);
|
||||
this.requested.delete(cord);
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
@@ -95,8 +95,8 @@ class ReplaceableEventRelayLoader {
|
||||
if (this.requested.size > 0) {
|
||||
const filters: Record<number, NostrQuery> = {};
|
||||
|
||||
for (const [addr] of this.requested) {
|
||||
const [kindStr, pubkey, d] = addr.split(":") as [string, string] | [string, string, string];
|
||||
for (const [cord] of this.requested) {
|
||||
const [kindStr, pubkey, d] = cord.split(":") as [string, string] | [string, string, string];
|
||||
const kind = parseInt(kindStr);
|
||||
filters[kind] = filters[kind] || { kinds: [kind] };
|
||||
|
||||
@@ -139,28 +139,27 @@ class ReplaceableEventLoaderService {
|
||||
log = logger.extend("ReplaceableEventLoader");
|
||||
|
||||
handleEvent(event: NostrEvent) {
|
||||
const d = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
||||
const addr = getAddr(event.kind, event.pubkey, d);
|
||||
const cord = getEventCoordinate(event);
|
||||
|
||||
const sub = this.events.get(addr);
|
||||
const sub = this.events.get(cord);
|
||||
const current = sub.value;
|
||||
if (!current || event.created_at > current.created_at) {
|
||||
sub.next(event);
|
||||
this.saveToCache(addr, event);
|
||||
this.saveToCache(cord, event);
|
||||
}
|
||||
}
|
||||
|
||||
getEvent(kind: number, pubkey: string, d?: string) {
|
||||
return this.events.get(getAddr(kind, pubkey, d));
|
||||
return this.events.get(createCoordinate(kind, pubkey, d));
|
||||
}
|
||||
|
||||
private loadCacheDedupe = new Map<string, Promise<boolean>>();
|
||||
private loadFromCache(addr: string) {
|
||||
const dedupe = this.loadCacheDedupe.get(addr);
|
||||
private loadFromCache(cord: string) {
|
||||
const dedupe = this.loadCacheDedupe.get(cord);
|
||||
if (dedupe) return dedupe;
|
||||
|
||||
const promise = db.get("replaceableEvents", addr).then((cached) => {
|
||||
this.loadCacheDedupe.delete(addr);
|
||||
const promise = db.get("replaceableEvents", cord).then((cached) => {
|
||||
this.loadCacheDedupe.delete(cord);
|
||||
if (cached?.event) {
|
||||
this.handleEvent(cached.event);
|
||||
return true;
|
||||
@@ -168,12 +167,12 @@ class ReplaceableEventLoaderService {
|
||||
return false;
|
||||
});
|
||||
|
||||
this.loadCacheDedupe.set(addr, promise);
|
||||
this.loadCacheDedupe.set(cord, promise);
|
||||
|
||||
return promise;
|
||||
}
|
||||
private async saveToCache(addr: string, event: NostrEvent) {
|
||||
await db.put("replaceableEvents", { addr, event, created: dayjs().unix() });
|
||||
private async saveToCache(cord: string, event: NostrEvent) {
|
||||
await db.put("replaceableEvents", { addr: cord, event, created: dayjs().unix() });
|
||||
}
|
||||
|
||||
async pruneCache() {
|
||||
@@ -193,8 +192,8 @@ class ReplaceableEventLoaderService {
|
||||
}
|
||||
|
||||
private requestEventFromRelays(relays: string[], kind: number, pubkey: string, d?: string) {
|
||||
const addr = getAddr(kind, pubkey, d);
|
||||
const sub = this.events.get(addr);
|
||||
const cord = createCoordinate(kind, pubkey, d);
|
||||
const sub = this.events.get(cord);
|
||||
|
||||
for (const relay of relays) {
|
||||
const request = this.loaders.get(relay).requestEvent(kind, pubkey, d);
|
||||
@@ -202,7 +201,7 @@ class ReplaceableEventLoaderService {
|
||||
sub.connectWithHandler(request, (event, next, current) => {
|
||||
if (!current || event.created_at > current.created_at) {
|
||||
next(event);
|
||||
this.saveToCache(addr, event);
|
||||
this.saveToCache(cord, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -211,11 +210,11 @@ class ReplaceableEventLoaderService {
|
||||
}
|
||||
|
||||
requestEvent(relays: string[], kind: number, pubkey: string, d?: string, alwaysRequest = false) {
|
||||
const addr = getAddr(kind, pubkey, d);
|
||||
const sub = this.events.get(addr);
|
||||
const cord = createCoordinate(kind, pubkey, d);
|
||||
const sub = this.events.get(cord);
|
||||
|
||||
if (!sub.value) {
|
||||
this.loadFromCache(addr).then((loaded) => {
|
||||
this.loadFromCache(cord).then((loaded) => {
|
||||
if (!loaded) {
|
||||
this.requestEventFromRelays(relays, kind, pubkey, d);
|
||||
}
|
||||
|
@@ -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";
|
||||
|
@@ -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";
|
||||
|
@@ -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";
|
||||
|
@@ -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";
|
||||
|
50
src/views/lists/components/list-card.tsx
Normal file
50
src/views/lists/components/list-card.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { AvatarGroup, Card, CardBody, CardHeader, Heading, Link, Spacer, Text } from "@chakra-ui/react";
|
||||
|
||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import EventVerificationIcon from "../../../components/event-verification-icon";
|
||||
import { getListName, getPubkeysFromList } from "../../../helpers/nostr/lists";
|
||||
import { getSharableEventNaddr } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { createCoordinate } from "../../../services/replaceable-event-requester";
|
||||
|
||||
export default function ListCard({ cord, event: maybeEvent }: { cord?: string; event?: NostrEvent }) {
|
||||
const event = maybeEvent ?? (cord ? useReplaceableEvent(cord as string) : undefined);
|
||||
if (!event) return null;
|
||||
|
||||
const people = getPubkeysFromList(event);
|
||||
const link =
|
||||
event.kind === Kind.Contacts ? createCoordinate(Kind.Contacts, event.pubkey) : getSharableEventNaddr(event);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader display="flex" p="2" flex="1" gap="2" alignItems="center">
|
||||
<Heading size="md">
|
||||
<Link as={RouterLink} to={`/lists/${link}`}>
|
||||
{getListName(event)}
|
||||
</Link>
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Text>Created by:</Text>
|
||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||
<EventVerificationIcon event={event} />
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
{people.length > 0 && (
|
||||
<>
|
||||
<Text>{people.length} people</Text>
|
||||
<AvatarGroup overflow="hidden">
|
||||
{people.map(({ pubkey, relay }) => (
|
||||
<UserAvatarLink pubkey={pubkey} relay={relay} />
|
||||
))}
|
||||
</AvatarGroup>
|
||||
</>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -1,10 +1,14 @@
|
||||
import { Button, Divider, Flex, Heading, Image, Link, Spacer } from "@chakra-ui/react";
|
||||
import { Button, Flex, Image, Link, Spacer } from "@chakra-ui/react";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useUserLists from "../../hooks/use-user-lists";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { ExternalLinkIcon, PlusCircleIcon } from "../../components/icons";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import ListCard from "./components/list-card";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { NOTE_LIST, PEOPLE_LIST } from "../../helpers/nostr/lists";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events";
|
||||
|
||||
function UsersLists() {
|
||||
const account = useCurrentAccount()!;
|
||||
@@ -14,16 +18,26 @@ function UsersLists() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListCard cord={`3:${account.pubkey}`} />
|
||||
<ListCard cord={`10000:${account.pubkey}`} />
|
||||
{Array.from(Object.entries(lists)).map(([name, list]) => (
|
||||
<Button key={name} as={RouterLink} to={`./${list.getAddress()}`} isTruncated>
|
||||
{name}
|
||||
</Button>
|
||||
<ListCard key={name} cord={list.cord} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ListsPage() {
|
||||
const account = useCurrentAccount()!;
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const timeline = useTimelineLoader("lists", readRelays, {
|
||||
authors: [account.pubkey],
|
||||
kinds: [PEOPLE_LIST, NOTE_LIST],
|
||||
});
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
|
||||
return (
|
||||
<Flex direction="column" p="2" gap="2">
|
||||
<Flex gap="2">
|
||||
@@ -40,7 +54,11 @@ function ListsPage() {
|
||||
<Button leftIcon={<PlusCircleIcon />}>New List</Button>
|
||||
</Flex>
|
||||
|
||||
<UsersLists />
|
||||
<ListCard cord={`3:${account.pubkey}`} />
|
||||
<ListCard cord={`10000:${account.pubkey}`} />
|
||||
{events.map((event) => (
|
||||
<ListCard key={getEventUID(event)} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@@ -1,49 +1,58 @@
|
||||
import { Link as RouterList, useNavigate, useParams } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useUserLists from "../../hooks/use-user-lists";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { Button, Flex, Heading, Link } from "@chakra-ui/react";
|
||||
import { Button, Flex, Heading } from "@chakra-ui/react";
|
||||
import { UserCard } from "../user/components/user-card";
|
||||
import { ArrowLeftSIcon, ExternalLinkIcon } from "../../components/icons";
|
||||
import { ArrowLeftSIcon } from "../../components/icons";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import { buildAppSelectUrl } from "../../helpers/nostr-apps";
|
||||
import { useDeleteEventContext } from "../../providers/delete-event-provider";
|
||||
import { parseCoordinate } from "../../helpers/nostr/events";
|
||||
import accountService from "../../services/account";
|
||||
import { MUTE_LIST, getListName, getPubkeysFromList } from "../../helpers/nostr/lists";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
|
||||
function useListPointer() {
|
||||
function useListCoordinate() {
|
||||
const { addr } = useParams() as { addr: string };
|
||||
const pointer = nip19.decode(addr);
|
||||
|
||||
switch (pointer.type) {
|
||||
case "naddr":
|
||||
if (pointer.data.kind !== 30000) throw new Error("Unknown event kind");
|
||||
return pointer.data;
|
||||
default:
|
||||
throw new Error(`Unknown type ${pointer.type}`);
|
||||
const current = accountService.current.value;
|
||||
|
||||
if (addr === "following") {
|
||||
if (!current) throw new Error("No account");
|
||||
return { kind: Kind.Contacts, pubkey: current.pubkey };
|
||||
}
|
||||
if (addr === "mute") {
|
||||
if (!current) throw new Error("No account");
|
||||
return { kind: MUTE_LIST, pubkey: current.pubkey };
|
||||
}
|
||||
|
||||
if (addr.includes(":")) {
|
||||
const parsed = parseCoordinate(addr);
|
||||
if (!parsed) throw new Error("Bad coordinate");
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const parsed = nip19.decode(addr);
|
||||
if (parsed.type !== "naddr") throw new Error(`Unknown type ${parsed.type}`);
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
export default function ListView() {
|
||||
const pointer = useListPointer();
|
||||
const account = useCurrentAccount();
|
||||
const navigate = useNavigate();
|
||||
const coordinate = useListCoordinate();
|
||||
const { deleteEvent } = useDeleteEventContext();
|
||||
const account = useCurrentAccount();
|
||||
|
||||
const readRelays = useReadRelayUrls(pointer.relays);
|
||||
const lists = useUserLists(pointer.pubkey, readRelays, true);
|
||||
const event = useReplaceableEvent(coordinate);
|
||||
|
||||
const list = lists[pointer.identifier];
|
||||
const people = useSubject(list?.people) ?? [];
|
||||
|
||||
if (!list)
|
||||
if (!event)
|
||||
return (
|
||||
<>
|
||||
Looking for list "{pointer.identifier}" created by <UserLink pubkey={pointer.pubkey} />
|
||||
Looking for list "{coordinate.identifier}" created by <UserLink pubkey={coordinate.pubkey} />
|
||||
</>
|
||||
);
|
||||
|
||||
const isAuthor = account?.pubkey === list.author;
|
||||
const isAuthor = account?.pubkey === event.pubkey;
|
||||
const people = getPubkeysFromList(event);
|
||||
|
||||
return (
|
||||
<Flex direction="column" px="2" pt="2" pb="8" overflowY="auto" overflowX="hidden" h="full" gap="2">
|
||||
@@ -53,17 +62,14 @@ export default function ListView() {
|
||||
</Button>
|
||||
|
||||
<Heading size="md" flex={1} isTruncated>
|
||||
{list.name}
|
||||
{getListName(event)}
|
||||
</Heading>
|
||||
|
||||
{isAuthor && (
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(list.event).then(() => navigate("/lists"))}>
|
||||
<Button colorScheme="red" onClick={() => deleteEvent(event).then(() => navigate("/lists"))}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button as={Link} href={buildAppSelectUrl(list.getAddress())} target="_blank" leftIcon={<ExternalLinkIcon />}>
|
||||
Open in app
|
||||
</Button>
|
||||
</Flex>
|
||||
{people.map(({ pubkey, relay }) => (
|
||||
<UserCard pubkey={pubkey} relay={relay} />
|
||||
|
@@ -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);
|
||||
|
@@ -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";
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ChatIcon } from "@chakra-ui/icons";
|
||||
import {
|
||||
Alert,
|
||||
@@ -14,22 +15,20 @@ import {
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
|
||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import directMessagesService from "../../services/direct-messages";
|
||||
import { ExternalLinkIcon } from "../../components/icons";
|
||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
function ContactCard({ pubkey }: { pubkey: string }) {
|
||||
const subject = useMemo(() => directMessagesService.getUserMessages(pubkey), [pubkey]);
|
||||
const messages = useSubject(subject);
|
||||
const metadata = useUserMetadata(pubkey);
|
||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||
|
||||
return (
|
||||
<LinkBox as={Card} size="sm">
|
||||
@@ -40,7 +39,7 @@ function ContactCard({ pubkey }: { pubkey: string }) {
|
||||
{messages[0] && <Text flexShrink={0}>{dayjs.unix(messages[0].created_at).fromNow()}</Text>}
|
||||
</Flex>
|
||||
</CardBody>
|
||||
<LinkOverlay as={RouterLink} to={`/dm/${npub ?? pubkey}`} />
|
||||
<LinkOverlay as={RouterLink} to={`/dm/${nip19.npubEncode(pubkey)}`} />
|
||||
</LinkBox>
|
||||
);
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@ import { useNotificationTimeline } from "../../providers/notification-timeline";
|
||||
import { Kind, getEventHash } from "nostr-tools";
|
||||
import { parseZapEvent } from "../../helpers/zaps";
|
||||
import { readablizeSats } from "../../helpers/bolt11";
|
||||
import { getReferences } from "../../helpers/nostr/event";
|
||||
import { getReferences } from "../../helpers/nostr/events";
|
||||
|
||||
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
||||
<Card size="sm" variant="outline">
|
||||
|
@@ -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";
|
||||
|
@@ -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];
|
||||
|
@@ -16,7 +16,7 @@ function ChatMessage({ event, stream }: { event: NostrEvent; stream: ParsedStrea
|
||||
return (
|
||||
<TrustProvider event={event}>
|
||||
<Box ref={ref}>
|
||||
<NoteZapButton note={event} size="xs" variant="ghost" float="right" ml="2" allowComment={false} />
|
||||
<NoteZapButton event={event} size="xs" variant="ghost" float="right" ml="2" allowComment={false} />
|
||||
<Box overflow="hidden" maxH="lg">
|
||||
<UserAvatar pubkey={event.pubkey} size="xs" display="inline-block" mr="2" />
|
||||
<Text as="span" fontWeight="bold" color={event.pubkey === stream.host ? "rgb(248, 56, 217)" : "cyan"}>
|
||||
|
@@ -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";
|
||||
|
@@ -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 (
|
||||
|
@@ -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]);
|
||||
|
@@ -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";
|
||||
|
@@ -15,16 +15,17 @@ import {
|
||||
Input,
|
||||
Flex,
|
||||
} from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { QrCodeIcon } from "../../../components/icons";
|
||||
import QrCodeSvg from "../../../components/qr-code-svg";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
|
||||
import { CopyIconButton } from "../../../components/copy-icon-button";
|
||||
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
|
||||
|
||||
export const QrIconButton = ({ pubkey, ...props }: { pubkey: string } & Omit<IconButtonProps, "icon">) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey) || pubkey;
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const npubLink = "nostr:" + npub;
|
||||
const nprofile = useSharableProfileId(pubkey);
|
||||
const nprofileLink = "nostr:" + nprofile;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { Flex, FlexProps, Heading, Link } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||
import { UserAvatar } from "../../../components/user-avatar";
|
||||
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19";
|
||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||
import { UserFollowButton } from "../../../components/user-follow-button";
|
||||
|
||||
@@ -28,7 +28,7 @@ export const UserCard = ({ pubkey, relay, ...props }: UserCardProps) => {
|
||||
>
|
||||
<UserAvatar pubkey={pubkey} />
|
||||
<Flex direction="column" flex={1} overflow="hidden">
|
||||
<Link as={RouterLink} to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
||||
<Link as={RouterLink} to={`/u/${nip19.npubEncode(pubkey)}`}>
|
||||
<Heading size="sm" whiteSpace="nowrap" isTruncated>
|
||||
{getUserDisplayName(metadata, pubkey)}
|
||||
</Heading>
|
||||
|
@@ -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,
|
||||
|
@@ -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";
|
||||
|
@@ -27,7 +27,7 @@ import {
|
||||
import { Outlet, useMatches, useNavigate, useParams } from "react-router-dom";
|
||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||
import { Bech32Prefix, isHex, normalizeToBech32 } from "../../helpers/nip19";
|
||||
import { isHex } from "../../helpers/nip19";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import { Suspense, useState } from "react";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
@@ -39,12 +39,14 @@ import { unique } from "../../helpers/array";
|
||||
import { RelayFavicon } from "../../components/relay-favicon";
|
||||
import { useUserRelays } from "../../hooks/use-user-relays";
|
||||
import Header from "./components/header";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
|
||||
const tabs = [
|
||||
{ label: "About", path: "about" },
|
||||
{ label: "Notes", path: "notes" },
|
||||
{ label: "Streams", path: "streams" },
|
||||
{ label: "Zaps", path: "zaps" },
|
||||
{ label: "Lists", path: "lists" },
|
||||
{ label: "Following", path: "following" },
|
||||
{ label: "Likes", path: "likes" },
|
||||
{ label: "Relays", path: "relays" },
|
||||
@@ -94,9 +96,8 @@ const UserView = () => {
|
||||
const activeTab = tabs.indexOf(tabs.find((t) => lastMatch.pathname.endsWith(t.path)) ?? tabs[0]);
|
||||
|
||||
const metadata = useUserMetadata(pubkey, userTopRelays, true);
|
||||
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
|
||||
|
||||
useAppTitle(getUserDisplayName(metadata, npub ?? pubkey));
|
||||
useAppTitle(getUserDisplayName(metadata, pubkey));
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -121,9 +122,11 @@ const UserView = () => {
|
||||
<TabPanels>
|
||||
{tabs.map(({ label }) => (
|
||||
<TabPanel key={label} p={0}>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Outlet context={{ pubkey, setRelayCount }} />
|
||||
</Suspense>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Outlet context={{ pubkey, setRelayCount }} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
|
@@ -2,7 +2,7 @@ import { useRef } from "react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Box, Flex, SkeletonText, Spacer, Text } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
import { getReferences, truncatedId } from "../../helpers/nostr/event";
|
||||
import { getReferences, truncatedId } from "../../helpers/nostr/events";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
|
31
src/views/user/lists.tsx
Normal file
31
src/views/user/lists.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { NOTE_LIST, PEOPLE_LIST } from "../../helpers/nostr/lists";
|
||||
import { getEventUID, truncatedId } from "../../helpers/nostr/events";
|
||||
import ListCard from "../lists/components/list-card";
|
||||
|
||||
export default function UserListsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const readRelays = useAdditionalRelayContext();
|
||||
|
||||
const timeline = useTimelineLoader(truncatedId(pubkey) + "-lists", readRelays, {
|
||||
authors: [pubkey],
|
||||
kinds: [PEOPLE_LIST, NOTE_LIST],
|
||||
});
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
|
||||
return (
|
||||
<Flex direction="column" p="2" gap="2">
|
||||
<ListCard cord={`3:${pubkey}`} />
|
||||
<ListCard cord={`10000:${pubkey}`} />
|
||||
{events.map((event) => (
|
||||
<ListCard key={getEventUID(event)} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
@@ -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";
|
||||
|
@@ -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";
|
||||
|
@@ -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";
|
||||
|
@@ -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";
|
||||
|
@@ -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";
|
||||
|
Reference in New Issue
Block a user