mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-29 11:12:12 +01:00
Merge branch 'next'
This commit is contained in:
commit
32cc4fc108
5
.changeset/brave-mayflies-laugh.md
Normal file
5
.changeset/brave-mayflies-laugh.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Dont proxy main user profile image
|
5
.changeset/brown-lies-hide.md
Normal file
5
.changeset/brown-lies-hide.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Dont blur images on shared notes
|
5
.changeset/fuzzy-pumpkins-allow.md
Normal file
5
.changeset/fuzzy-pumpkins-allow.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Make all note links nevent
|
5
.changeset/good-mails-play.md
Normal file
5
.changeset/good-mails-play.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Trim note content
|
5
.changeset/wise-gorillas-jog.md
Normal file
5
.changeset/wise-gorillas-jog.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Update nostr-tools dependency
|
5
.changeset/witty-seals-attack.md
Normal file
5
.changeset/witty-seals-attack.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": patch
|
||||
---
|
||||
|
||||
Fix link regexp
|
36
package.json
36
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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]}
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
44
src/components/note/embeded-note.tsx
Normal file
44
src/components/note/embeded-note.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
20
src/components/note/note-content-with-warning.tsx
Normal file
20
src/components/note/note-content-with-warning.tsx
Normal 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} />
|
||||
);
|
||||
}
|
@ -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);
|
||||
|
@ -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 && (
|
||||
|
@ -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 }) => {
|
||||
|
53
src/components/note/repost-note.tsx
Normal file
53
src/components/note/repost-note.tsx
Normal 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>
|
||||
);
|
||||
}
|
28
src/components/note/trust.tsx
Normal file
28
src/components/note/trust.tsx
Normal 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>;
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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 });
|
||||
|
@ -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">
|
||||
|
@ -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";
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user