Merge branch 'next'

This commit is contained in:
hzrd149 2023-05-25 11:37:46 -05:00
commit 32cc4fc108
34 changed files with 796 additions and 675 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Dont proxy main user profile image

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Dont blur images on shared notes

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Make all note links nevent

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Trim note content

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Update nostr-tools dependency

View File

@ -0,0 +1,5 @@
---
"nostrudel": patch
---
Fix link regexp

View File

@ -5,41 +5,41 @@
"license": "MIT",
"scripts": {
"start": "vite serve",
"build": "tsc && vite build",
"build": "tsc --project tsconfig.json && vite build",
"format": "prettier --ignore-path .prettierignore -w ."
},
"dependencies": {
"@chakra-ui/icons": "^2.0.14",
"@chakra-ui/react": "^2.4.4",
"@changesets/cli": "^2.26.1",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@chakra-ui/icons": "^2.0.19",
"@chakra-ui/react": "^2.6.1",
"@emotion/react": "^11.11.0",
"@emotion/styled": "^11.11.0",
"bech32": "^2.0.0",
"framer-motion": "^7.10.3",
"idb": "^7.1.1",
"identicon.js": "^2.3.3",
"light-bolt11-decoder": "^2.1.0",
"light-bolt11-decoder": "^3.0.0",
"moment": "^2.29.4",
"noble-secp256k1": "^1.2.14",
"nostr-tools": "^1.8.3",
"nostr-tools": "^1.11.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.43.1",
"react-error-boundary": "^4.0.4",
"react-hook-form": "^7.43.9",
"react-qr-barcode-scanner": "^1.0.6",
"react-router-dom": "^6.5.0",
"react-router-dom": "^6.11.2",
"react-singleton-hook": "^4.0.1",
"react-use": "^17.4.0",
"webln": "^0.3.2"
},
"devDependencies": {
"@changesets/cli": "^2.26.1",
"@types/identicon.js": "^2.3.1",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "^3.0.0",
"prettier": "^2.8.1",
"typescript": "^4.9.4",
"vite": "^4.0.2",
"vite-plugin-pwa": "^0.14.1"
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@vitejs/plugin-react": "^4.0.0",
"prettier": "^2.8.8",
"typescript": "^5.0.4",
"vite": "^4.3.8",
"vite-plugin-pwa": "^0.15.1"
}
}

View File

@ -2,6 +2,7 @@ import { Box, Image, ImageProps, Link, useDisclosure } from "@chakra-ui/react";
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
import appSettings from "../../services/app-settings";
import { ImageGalleryLink } from "../image-gallery";
import { useIsMobile } from "../../hooks/use-is-mobile";
const BlurredImage = (props: ImageProps) => {
const { isOpen, onOpen } = useDisclosure();
@ -12,24 +13,26 @@ const BlurredImage = (props: ImageProps) => {
);
};
const EmbeddedImage = ({ src, blue }: { src: string; blue: boolean }) => {
const isMobile = useIsMobile();
const ImageComponent = blue || !appSettings.value.blurImages ? Image : BlurredImage;
const thumbnail = appSettings.value.imageProxy
? new URL(`/256,fit/${src}`, appSettings.value.imageProxy).toString()
: src;
return (
<ImageGalleryLink href={src} target="_blank" display="block" mx="-2">
<ImageComponent src={thumbnail} cursor="pointer" maxH={isMobile ? "80vh" : "25vh"} mx={isMobile ? "auto" : "0"} />
</ImageGalleryLink>
);
};
// note1n06jceulg3gukw836ghd94p0ppwaz6u3mksnnz960d8vlcp2fnqsgx3fu9
export function embedImages(content: EmbedableContent, trusted = false) {
return embedJSX(content, {
regexp:
/https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,6})((?:\/[\+~%\/\.\w\-_]*)?\.(?:svg|gif|png|jpg|jpeg|webp|avif))(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/i,
render: (match) => {
const ImageComponent = trusted || !appSettings.value.blurImages ? Image : BlurredImage;
const thumbnail = appSettings.value.imageProxy
? new URL(`/256,fit/${match[0]}`, appSettings.value.imageProxy).toString()
: match[0];
const src = match[0];
return (
<ImageGalleryLink href={src} target="_blank" display="block" mx="-2">
<ImageComponent src={thumbnail} cursor="pointer" maxW="30rem" w="full" />
</ImageGalleryLink>
);
},
render: (match) => <EmbeddedImage blue={trusted} src={match[0]} />,
name: "Image",
});
}
@ -44,12 +47,12 @@ export function embedVideos(content: EmbedableContent) {
}
// based on http://urlregex.com/
// note1c34vht0lu2qzrgr4az3u8jn5xl3fycr2gfpahkepthg7hzlqg26sr59amt
// nostr:nevent1qqsvg6kt4hl79qpp5p673g7ref6r0c5jvp4yys7mmvs4m50t30sy9dgpp4mhxue69uhkummn9ekx7mqpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet59dl66z
// nostr:nevent1qqsymds0vlpp4f5s0dckjf4qz283pdsen0rmx8lu7ct6hpnxag2hpacpremhxue69uhkummnw3ez6un9d3shjtnwda4k7arpwfhjucm0d5q3qamnwvaz7tmwdaehgu3wwa5kueghxyq76
export function embedLinks(content: EmbedableContent) {
return embedJSX(content, {
name: "Link",
regexp:
/https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,6})((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/i,
regexp: /https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,6})(\/[\+~%\/\.\w\-_]*)?([\?#][^\s]+)?/i,
render: (match) => (
<Link color="blue.500" href={match[0]} target="_blank" isExternal>
{match[0]}

View File

@ -1,51 +0,0 @@
import { Link as RouterLink } from "react-router-dom";
import moment from "moment";
import { Card, CardBody, CardHeader, Flex, Heading, Link } from "@chakra-ui/react";
import { useIsMobile } from "../hooks/use-is-mobile";
import { NoteContents } from "./note/note-contents";
import { useUserContacts } from "../hooks/use-user-contacts";
import { useCurrentAccount } from "../hooks/use-current-account";
import { NostrEvent } from "../types/nostr-event";
import { UserAvatarLink } from "./user-avatar-link";
import { UserLink } from "./user-link";
import { UserDnsIdentityIcon } from "./user-dns-identity";
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
import { convertTimestampToDate } from "../helpers/date";
import useSubject from "../hooks/use-subject";
import appSettings from "../services/app-settings";
import EventVerificationIcon from "./event-verification-icon";
import { useReadRelayUrls } from "../hooks/use-client-relays";
const EmbeddedNote = ({ note }: { note: NostrEvent }) => {
const account = useCurrentAccount();
const { showSignatureVerification } = useSubject(appSettings);
const readRelays = useReadRelayUrls();
const contacts = useUserContacts(account.pubkey, readRelays);
const following = contacts?.contacts || [];
return (
<Card variant="outline">
<CardHeader padding="2">
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
<UserAvatarLink pubkey={note.pubkey} size="xs" />
<Heading size="sm" display="inline">
<UserLink pubkey={note.pubkey} />
</Heading>
<UserDnsIdentityIcon pubkey={note.pubkey} onlyIcon />
<Flex grow={1} />
{showSignatureVerification && <EventVerificationIcon event={note} />}
<Link as={RouterLink} to={`/n/${normalizeToBech32(note.id, Bech32Prefix.Note)}`} whiteSpace="nowrap">
{moment(convertTimestampToDate(note.created_at)).fromNow()}
</Link>
</Flex>
</CardHeader>
<CardBody px="2" pt="0" pb="2">
<NoteContents event={note} trusted={following.includes(note.pubkey)} maxHeight={200} />
</CardBody>
</Card>
);
};
export default EmbeddedNote;

View File

@ -1,18 +1,31 @@
import { Link, LinkProps } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
import { truncatedId } from "../helpers/nostr-event";
import { nip19 } from "nostr-tools";
import { getEventRelays } from "../services/event-relays";
import relayScoreboardService from "../services/relay-scoreboard";
import { useMemo } from "react";
export function getSharableEncodedNoteId(eventId: string) {
const relays = getEventRelays(eventId).value;
const ranked = relayScoreboardService.getRankedRelays(relays);
const onlyTwo = ranked.slice(0, 2);
if (onlyTwo.length > 0) {
return nip19.neventEncode({ id: eventId, relays: onlyTwo });
} else return nip19.noteEncode(eventId);
}
export type NoteLinkProps = LinkProps & {
noteId: string;
};
export const NoteLink = ({ noteId, ...props }: NoteLinkProps) => {
const note1 = normalizeToBech32(noteId, Bech32Prefix.Note) ?? noteId;
export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: NoteLinkProps) => {
const encoded = useMemo(() => getSharableEncodedNoteId(noteId), [noteId]);
return (
<Link as={RouterLink} to={`/n/${note1}`} color="blue.500" {...props}>
{truncatedId(note1)}
<Link as={RouterLink} to={`/n/${encoded}`} color={color} {...props}>
{children || truncatedId(nip19.noteEncode(noteId))}
</Link>
);
};

View File

@ -0,0 +1,44 @@
import { Link as RouterLink } from "react-router-dom";
import moment from "moment";
import { Card, CardBody, CardHeader, Flex, Heading, Link } from "@chakra-ui/react";
import { NoteContents } from "./note-contents";
import { NostrEvent } from "../../types/nostr-event";
import { UserAvatarLink } from "../user-avatar-link";
import { UserLink } from "../user-link";
import { UserDnsIdentityIcon } from "../user-dns-identity";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { convertTimestampToDate } from "../../helpers/date";
import useSubject from "../../hooks/use-subject";
import appSettings from "../../services/app-settings";
import EventVerificationIcon from "../event-verification-icon";
import { TrustProvider } from "./trust";
import { NoteLink } from "../note-link";
export default function EmbeddedNote({ note }: { note: NostrEvent }) {
const { showSignatureVerification } = useSubject(appSettings);
return (
<TrustProvider event={note}>
<Card variant="outline">
<CardHeader padding="2">
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
<UserAvatarLink pubkey={note.pubkey} size="xs" />
<Heading size="sm" display="inline">
<UserLink pubkey={note.pubkey} />
</Heading>
<UserDnsIdentityIcon pubkey={note.pubkey} onlyIcon />
<Flex grow={1} />
{showSignatureVerification && <EventVerificationIcon event={note} />}
<NoteLink noteId={note.id} color="current" whiteSpace="nowrap">
{moment(convertTimestampToDate(note.created_at)).fromNow()}
</NoteLink>
</Flex>
</CardHeader>
<CardBody p="0">
<NoteContents event={note} maxHeight={200} />
</CardBody>
</Card>
</TrustProvider>
);
}

View File

@ -1,13 +1,7 @@
import React, { useMemo } from "react";
import { Link as RouterLink } from "react-router-dom";
import moment from "moment";
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
Button,
ButtonGroup,
Card,
CardBody,
@ -18,57 +12,29 @@ import {
Heading,
IconButton,
Link,
Spacer,
} from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import { UserAvatarLink } from "../user-avatar-link";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { NoteContents } from "./note-contents";
import { NoteMenu } from "./note-menu";
import { useUserContacts } from "../../hooks/use-user-contacts";
import { NoteRelays } from "./note-relays";
import { useIsMobile } from "../../hooks/use-is-mobile";
import { UserLink } from "../user-link";
import { UserDnsIdentityIcon } from "../user-dns-identity";
import { convertTimestampToDate } from "../../helpers/date";
import { useCurrentAccount } from "../../hooks/use-current-account";
import ReactionButton from "./buttons/reaction-button";
import NoteZapButton from "./note-zap-button";
import { ExpandProvider, useExpand } from "./expanded";
import { ExpandProvider } from "./expanded";
import useSubject from "../../hooks/use-subject";
import appSettings from "../../services/app-settings";
import EventVerificationIcon from "../event-verification-icon";
import { ReplyButton } from "./buttons/reply-button";
import { RepostButton } from "./buttons/repost-button";
import { QuoteRepostButton } from "./buttons/quote-repost-button";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { ExternalLinkIcon } from "../icons";
import SensitiveContentWarning from "../sensitive-content-warning";
import useAppSettings from "../../hooks/use-app-settings";
function NoteContentWithWarning({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) {
const account = useCurrentAccount();
const expand = useExpand();
const settings = useAppSettings();
const readRelays = useReadRelayUrls();
const contacts = useUserContacts(account.pubkey, readRelays);
const following = contacts?.contacts || [];
const contentWarning = event.tags.find((t) => t[0] === "content-warning")?.[1];
const showContentWarning = settings.showContentWarning && contentWarning && !expand?.expanded;
return showContentWarning ? (
<SensitiveContentWarning description={contentWarning} />
) : (
<NoteContents
event={event}
trusted={event.pubkey === account.pubkey || following.includes(event.pubkey)}
maxHeight={maxHeight}
/>
);
}
import NoteContentWithWarning from "./note-content-with-warning";
import { TrustProvider } from "./trust";
import { NoteLink } from "../note-link";
export type NoteProps = {
event: NostrEvent;
@ -83,50 +49,52 @@ export const Note = React.memo(({ event, maxHeight, variant = "outline" }: NoteP
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr"), [event]);
return (
<ExpandProvider>
<Card variant={variant}>
<CardHeader padding="2">
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
<UserAvatarLink pubkey={event.pubkey} size={isMobile ? "xs" : "sm"} />
<TrustProvider event={event}>
<ExpandProvider>
<Card variant={variant}>
<CardHeader padding="2">
<Flex flex="1" gap="2" alignItems="center" wrap="wrap">
<UserAvatarLink pubkey={event.pubkey} size={isMobile ? "xs" : "sm"} />
<Heading size="sm" display="inline">
<UserLink pubkey={event.pubkey} />
</Heading>
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Flex grow={1} />
{showSignatureVerification && <EventVerificationIcon event={event} />}
<Link as={RouterLink} to={`/n/${normalizeToBech32(event.id, Bech32Prefix.Note)}`} whiteSpace="nowrap">
{moment(convertTimestampToDate(event.created_at)).fromNow()}
</Link>
</Flex>
</CardHeader>
<CardBody p="0">
<NoteContentWithWarning event={event} maxHeight={maxHeight} />
</CardBody>
<CardFooter padding="2" display="flex" gap="2">
<ButtonGroup size="sm" variant="link">
<ReplyButton event={event} />
<RepostButton event={event} />
<QuoteRepostButton event={event} />
<NoteZapButton note={event} size="sm" />
{showReactions && <ReactionButton note={event} size="sm" />}
</ButtonGroup>
<Box flexGrow={1} />
{externalLink && (
<IconButton
as={Link}
icon={<ExternalLinkIcon />}
aria-label="Open External"
href={externalLink[1]}
size="sm"
variant="link"
target="_blank"
/>
)}
<NoteRelays event={event} size="sm" variant="link" />
<NoteMenu event={event} size="sm" variant="link" aria-label="More Options" />
</CardFooter>
</Card>
</ExpandProvider>
<Heading size="sm" display="inline">
<UserLink pubkey={event.pubkey} />
</Heading>
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Flex grow={1} />
{showSignatureVerification && <EventVerificationIcon event={event} />}
<NoteLink noteId={event.id} whiteSpace="nowrap" color="current">
{moment(convertTimestampToDate(event.created_at)).fromNow()}
</NoteLink>
</Flex>
</CardHeader>
<CardBody p="0">
<NoteContentWithWarning event={event} maxHeight={maxHeight} />
</CardBody>
<CardFooter padding="2" display="flex" gap="2">
<ButtonGroup size="sm" variant="link">
<ReplyButton event={event} />
<RepostButton event={event} />
<QuoteRepostButton event={event} />
<NoteZapButton note={event} size="sm" />
{showReactions && <ReactionButton note={event} size="sm" />}
</ButtonGroup>
<Box flexGrow={1} />
{externalLink && (
<IconButton
as={Link}
icon={<ExternalLinkIcon />}
aria-label="Open External"
href={externalLink[1]}
size="sm"
variant="link"
target="_blank"
/>
)}
<NoteRelays event={event} size="sm" variant="link" />
<NoteMenu event={event} size="sm" variant="link" aria-label="More Options" />
</CardFooter>
</Card>
</ExpandProvider>
</TrustProvider>
);
});

View File

@ -0,0 +1,20 @@
import { NostrEvent } from "../../types/nostr-event";
import { NoteContents } from "./note-contents";
import { useExpand } from "./expanded";
import SensitiveContentWarning from "../sensitive-content-warning";
import useAppSettings from "../../hooks/use-app-settings";
export default function NoteContentWithWarning({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) {
const expand = useExpand();
const settings = useAppSettings();
const contentWarning = event.tags.find((t) => t[0] === "content-warning")?.[1];
const showContentWarning = settings.showContentWarning && contentWarning && !expand?.expanded;
return showContentWarning ? (
<SensitiveContentWarning description={contentWarning} />
) : (
<NoteContents event={event} maxHeight={maxHeight} />
);
}

View File

@ -21,9 +21,10 @@ import {
embedNostrHashtags,
} from "../embed-types";
import { ImageGalleryProvider } from "../image-gallery";
import { useTrusted } from "./trust";
function buildContents(event: NostrEvent | DraftNostrEvent, trusted: boolean = false) {
let content: EmbedableContent = [event.content];
function buildContents(event: NostrEvent | DraftNostrEvent, trusted = false) {
let content: EmbedableContent = [event.content.trim()];
content = embedLightningInvoice(content);
content = embedTweet(content);
@ -59,12 +60,12 @@ const GradientOverlay = styled.div`
export type NoteContentsProps = {
event: NostrEvent | DraftNostrEvent;
trusted?: boolean;
maxHeight?: number;
};
export const NoteContents = React.memo(({ event, trusted, maxHeight }: NoteContentsProps) => {
const content = buildContents(event, trusted ?? false);
export const NoteContents = React.memo(({ event, maxHeight }: NoteContentsProps) => {
const trusted = useTrusted();
const content = buildContents(event, trusted);
const expand = useExpand();
const [innerHeight, setInnerHeight] = useState(0);
const ref = useRef<HTMLDivElement | null>(null);

View File

@ -13,7 +13,6 @@ import {
useToast,
} from "@chakra-ui/react";
import { useCopyToClipboard } from "react-use";
import { nip19 } from "nostr-tools";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { NostrEvent } from "../../types/nostr-event";
@ -21,8 +20,6 @@ import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
import { ClipboardIcon, CodeIcon, LikeIcon, RepostIcon, TrashIcon } from "../icons";
import NoteReactionsModal from "./note-zaps-modal";
import { getEventRelays } from "../../services/event-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
import NoteDebugModal from "../debug-modals/note-debug-modal";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { useCallback, useState } from "react";
@ -31,16 +28,7 @@ import { buildDeleteEvent } from "../../helpers/nostr-event";
import signingService from "../../services/signing";
import { nostrPostAction } from "../../classes/nostr-post-action";
import clientRelaysService from "../../services/client-relays";
function getShareLink(eventId: string) {
const relays = getEventRelays(eventId).value;
const ranked = relayScoreboardService.getRankedRelays(relays);
const onlyTwo = ranked.slice(0, 2);
if (onlyTwo.length > 0) {
return nip19.neventEncode({ id: eventId, relays: onlyTwo });
} else return nip19.noteEncode(eventId);
}
import { getSharableEncodedNoteId } from "../note-link";
export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) => {
const account = useCurrentAccount();
@ -80,7 +68,7 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit<MenuI
<MenuItem onClick={reactionsModal.onOpen} icon={<LikeIcon />}>
Zaps/Reactions
</MenuItem>
<MenuItem onClick={() => copyToClipboard("nostr:" + getShareLink(event.id))} icon={<RepostIcon />}>
<MenuItem onClick={() => copyToClipboard("nostr:" + getSharableEncodedNoteId(event.id))} icon={<RepostIcon />}>
Copy Share Link
</MenuItem>
{noteId && (

View File

@ -1,6 +1,6 @@
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useSingleEvent from "../../hooks/use-single-event";
import EmbeddedNote from "../embeded-note";
import EmbeddedNote from "./embeded-note";
import { NoteLink } from "../note-link";
const QuoteNote = ({ noteId, relay }: { noteId: string; relay?: string }) => {

View File

@ -0,0 +1,53 @@
import { Box, Flex, Heading, SkeletonText } from "@chakra-ui/react";
import { useAsync } from "react-use";
import clientRelaysService from "../../services/client-relays";
import singleEventService from "../../services/single-event";
import { isETag, NostrEvent } from "../../types/nostr-event";
import { ErrorFallback } from "../error-boundary";
import { Note } from ".";
import { NoteMenu } from "./note-menu";
import { UserAvatar } from "../user-avatar";
import { UserDnsIdentityIcon } from "../user-dns-identity";
import { UserLink } from "../user-link";
import { unique } from "../../helpers/array";
import { TrustProvider } from "./trust";
export default function RepostNote({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) {
const {
value: repostNote,
loading,
error,
} = useAsync(async () => {
const [_, eventId, relay] = event.tags.find(isETag) ?? [];
if (eventId) {
const readRelays = clientRelaysService.getReadUrls();
if (relay) readRelays.push(relay);
return singleEventService.requestEvent(eventId, unique(readRelays));
}
return null;
}, [event]);
return (
<TrustProvider event={event}>
<Flex gap="2" direction="column">
<Flex gap="2" alignItems="center" pl="1">
<UserAvatar pubkey={event.pubkey} size="xs" />
<Heading size="sm" display="inline">
<UserLink pubkey={event.pubkey} />
</Heading>
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<span>Shared note</span>
<Box flex={1} />
<NoteMenu event={event} size="sm" variant="link" aria-label="note options" />
</Flex>
{loading ? (
<SkeletonText />
) : repostNote ? (
<Note event={repostNote} maxHeight={maxHeight} />
) : (
<ErrorFallback error={error} />
)}
</Flex>
</TrustProvider>
);
}

View File

@ -0,0 +1,28 @@
import React, { PropsWithChildren, useContext } from "react";
import { NostrEvent } from "../../types/nostr-event";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { useUserContacts } from "../../hooks/use-user-contacts";
import { useCurrentAccount } from "../../hooks/use-current-account";
const TrustContext = React.createContext<boolean>(false);
export function useTrusted() {
return useContext(TrustContext);
}
export function TrustProvider({
children,
event,
trust = false,
}: PropsWithChildren & { event?: NostrEvent; trust?: boolean }) {
const parentTrust = useContext(TrustContext);
const account = useCurrentAccount();
const readRelays = useReadRelayUrls();
const contacts = useUserContacts(account.pubkey, readRelays);
const following = contacts?.contacts || [];
const isEventTrusted = trust || (!!event && (event.pubkey === account.pubkey || following.includes(event.pubkey)));
return <TrustContext.Provider value={parentTrust || isEventTrusted}>{children}</TrustContext.Provider>;
}

View File

@ -27,6 +27,7 @@ import { ImageIcon } from "../icons";
import { NoteLink } from "../note-link";
import { NoteContents } from "../note/note-contents";
import { PostResults } from "./post-results";
import { TrustProvider } from "../note/trust";
function emptyDraft(): DraftNostrEvent {
return {
@ -139,7 +140,9 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
</Text>
)}
{showPreview ? (
<NoteContents event={finalizeNote(draft)} trusted />
<TrustProvider trust>
<NoteContents event={finalizeNote(draft)} />
</TrustProvider>
) : (
<Textarea
autoFocus

View File

@ -2,7 +2,7 @@ import { Box, LinkBox, Text } from "@chakra-ui/react";
import { Link } from "react-router-dom";
import { UserAvatar } from "./user-avatar";
import { useUserMetadata } from "../hooks/use-user-metadata";
import { normalizeToBech32 } from "../helpers/nip19";
import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip19";
import { truncatedId } from "../helpers/nostr-event";
import { useCurrentAccount } from "../hooks/use-current-account";
@ -11,8 +11,14 @@ export const ProfileButton = () => {
const metadata = useUserMetadata(pubkey);
return (
<LinkBox as={Link} to={`/u/${pubkey}`} display="flex" gap="2" overflow="hidden">
<UserAvatar pubkey={pubkey} />
<LinkBox
as={Link}
to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}
display="flex"
gap="2"
overflow="hidden"
>
<UserAvatar pubkey={pubkey} noProxy />
<Box>
<Text fontWeight="bold">{metadata?.name}</Text>
<Text>{truncatedId(normalizeToBech32(pubkey) ?? "")}</Text>

View File

@ -1,50 +0,0 @@
import { Box, Flex, Heading, SkeletonText } from "@chakra-ui/react";
import { useAsync } from "react-use";
import clientRelaysService from "../services/client-relays";
import singleEventService from "../services/single-event";
import { isETag, NostrEvent } from "../types/nostr-event";
import { ErrorFallback } from "./error-boundary";
import { Note } from "./note";
import { NoteMenu } from "./note/note-menu";
import { UserAvatar } from "./user-avatar";
import { UserDnsIdentityIcon } from "./user-dns-identity";
import { UserLink } from "./user-link";
import { unique } from "../helpers/array";
export default function RepostNote({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) {
const {
value: repostNote,
loading,
error,
} = useAsync(async () => {
const [_, eventId, relay] = event.tags.find(isETag) ?? [];
if (eventId) {
const readRelays = clientRelaysService.getReadUrls();
if (relay) readRelays.push(relay);
return singleEventService.requestEvent(eventId, unique(readRelays));
}
return null;
}, [event]);
return (
<Flex gap="2" direction="column">
<Flex gap="2" alignItems="center" pl="1">
<UserAvatar pubkey={event.pubkey} size="xs" />
<Heading size="sm" display="inline">
<UserLink pubkey={event.pubkey} />
</Heading>
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<span>Shared note</span>
<Box flex={1} />
<NoteMenu event={event} size="sm" variant="link" aria-label="note options" />
</Flex>
{loading ? (
<SkeletonText />
) : repostNote ? (
<Note event={repostNote} maxHeight={maxHeight} />
) : (
<ErrorFallback error={error} />
)}
</Flex>
);
}

View File

@ -15,18 +15,21 @@ export const UserIdenticon = React.memo(({ pubkey }: { pubkey: string }) => {
export type UserAvatarProps = Omit<AvatarProps, "src"> & {
pubkey: string;
noProxy?: boolean;
};
export const UserAvatar = React.memo(({ pubkey, ...props }: UserAvatarProps) => {
export const UserAvatar = React.memo(({ pubkey, noProxy, ...props }: UserAvatarProps) => {
const { imageProxy, proxyUserMedia } = useSubject(appSettings);
const metadata = useUserMetadata(pubkey);
const picture = useMemo(() => {
if (metadata?.picture) {
const src = safeUrl(metadata?.picture);
if (imageProxy && src) {
return new URL(`/96/${src}`, imageProxy).toString();
} else if (proxyUserMedia) {
const last4 = String(pubkey).slice(pubkey.length - 4, pubkey.length);
return `https://media.nostr.band/thumbs/${last4}/${pubkey}-picture-64`;
if (!noProxy) {
if (imageProxy && src) {
return new URL(`/96/${src}`, imageProxy).toString();
} else if (proxyUserMedia) {
const last4 = String(pubkey).slice(pubkey.length - 4, pubkey.length);
return `https://media.nostr.band/thumbs/${last4}/${pubkey}-picture-64`;
}
}
return src;
}

View File

@ -1,4 +1,5 @@
import { bech32 } from "bech32";
import { nip19 } from "nostr-tools";
export function isHex(key?: string) {
if (key?.toLowerCase()?.match(/^[0-9a-f]{64}$/)) return true;
@ -60,6 +61,12 @@ export function hexStringToUint8(str: string) {
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;

View File

@ -5,7 +5,6 @@ import { NostrMultiSubscription } from "../classes/nostr-multi-subscription";
import db from "./db";
import { getReferences } from "../helpers/nostr-event";
import userContactsService from "./user-contacts";
import clientRelaysService from "./client-relays";
import { Subject } from "../classes/subject";
import { Kind } from "nostr-tools";

View File

@ -10,7 +10,7 @@ import { useContext } from "react";
import { PostModalContext } from "../../providers/post-modal-provider";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { useCurrentAccount } from "../../hooks/use-current-account";
import RepostNote from "../../components/repost-note";
import RepostNote from "../../components/note/repost-note";
export default function FollowingTab() {
const account = useCurrentAccount();

View File

@ -21,7 +21,7 @@ export default function AccountCard({ pubkey }: { pubkey: string }) {
cursor="pointer"
onClick={() => accountService.switchAccount(pubkey)}
>
<UserAvatar pubkey={pubkey} size="sm" />
<UserAvatar pubkey={pubkey} size="sm" noProxy />
<Text flex={1} mr="4" overflow="hidden">
{getUserDisplayName(metadata, pubkey)}
</Text>

View File

@ -14,7 +14,6 @@ import {
InputGroup,
InputRightElement,
Link,
useToast,
} from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { RelayUrlInput } from "../../components/relay-url-input";
@ -26,7 +25,6 @@ import signingService from "../../services/signing";
export default function LoginNsecView() {
const navigate = useNavigate();
const toast = useToast();
const [show, setShow] = useState(false);
const [error, setError] = useState(false);

View File

@ -9,27 +9,24 @@ import { useReadRelayUrls } from "../../hooks/use-client-relays";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { NostrEvent } from "../../types/nostr-event";
import { NoteLink } from "../../components/note-link";
const Kind1Notification = ({ event }: { event: NostrEvent }) => {
const navigate = useNavigate();
return (
<Card size="sm" variant="outline">
<CardHeader>
<Flex gap="4" alignItems="center">
<UserAvatar pubkey={event.pubkey} size="sm" />
<UserLink pubkey={event.pubkey} />
<Button onClick={() => navigate(`/n/${event.id}`)} ml="auto" variant="link">
{moment(convertTimestampToDate(event.created_at)).fromNow()}
</Button>
</Flex>
</CardHeader>
<CardBody pt={0}>
<Text>{event.content.replace("\n", " ").slice(0, 64)}</Text>
</CardBody>
</Card>
);
};
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
<Card size="sm" variant="outline">
<CardHeader>
<Flex gap="4" alignItems="center">
<UserAvatar pubkey={event.pubkey} size="sm" />
<UserLink pubkey={event.pubkey} />
<NoteLink noteId={event.id} color="current" ml="auto">
{moment(convertTimestampToDate(event.created_at)).fromNow()}
</NoteLink>
</Flex>
</CardHeader>
<CardBody pt={0}>
<Text>{event.content.replace("\n", " ").slice(0, 64)}</Text>
</CardBody>
</Card>
);
const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
if (event.kind === 1) {

View File

@ -21,6 +21,7 @@ import ZapModal from "../../components/zap-modal";
import { convertTimestampToDate } from "../../helpers/date";
import { truncatedId } from "../../helpers/nostr-event";
import QrScannerModal from "../../components/qr-scanner-modal";
import { safeDecode } from "../../helpers/nip19";
type relay = string;
type NostrBandSearchResults = {
@ -90,7 +91,7 @@ export default function SearchView() {
// set the search when the form is submitted
const handleSubmit = (e: React.SyntheticEvent) => {
e.preventDefault();
if (search.startsWith("nostr:")) {
if (search.startsWith("nostr:") || safeDecode(search)) {
navigate({ pathname: "/l/" + search }, { replace: true });
} else {
setSearchParams({ q: search }, { replace: true });

View File

@ -46,7 +46,7 @@ export default function Header({ pubkey }: { pubkey: string }) {
return (
<Flex direction="column" gap="2" px="2" pt="2">
<Flex gap="4">
<UserAvatar pubkey={pubkey} size={isMobile ? "md" : "xl"} />
<UserAvatar pubkey={pubkey} size={isMobile ? "md" : "xl"} noProxy />
<Flex direction="column" gap={isMobile ? 0 : 2} grow="1" overflow="hidden">
<Flex gap="2" justifyContent="space-between" width="100%">
<Flex gap="2" alignItems="center" wrap="wrap">

View File

@ -20,7 +20,7 @@ import moment from "moment";
import { useOutletContext } from "react-router-dom";
import { RelayIcon } from "../../components/icons";
import { Note } from "../../components/note";
import RepostNote from "../../components/repost-note";
import RepostNote from "../../components/note/repost-note";
import { isReply, isRepost, truncatedId } from "../../helpers/nostr-event";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";

View File

@ -5,18 +5,17 @@
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["node_modules"],
"references": [{ "path": "./tsconfig.node.json" }]
"exclude": ["node_modules"]
}

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

834
yarn.lock

File diff suppressed because it is too large Load Diff