mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 13:21:44 +01:00
add webrtc test
This commit is contained in:
parent
82c9f93a4f
commit
cfef0cc8b5
@ -68,7 +68,7 @@
|
||||
"nanoid": "^5.0.4",
|
||||
"ngeohash": "^0.6.3",
|
||||
"nostr-idb": "^2.1.4",
|
||||
"nostr-tools": "2.5.2",
|
||||
"nostr-tools": "^2.7.1",
|
||||
"nostr-wasm": "^0.1.0",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^18.2.0",
|
||||
@ -117,7 +117,8 @@
|
||||
"@types/zen-observable": "^0.8.7",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"camelcase": "^8.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.2.10",
|
||||
"vite-plugin-pwa": "^0.19.8",
|
||||
"workbox-build": "^7.0.0",
|
||||
|
@ -76,6 +76,7 @@ import MailboxesView from "./views/relays/mailboxes";
|
||||
import MediaServersView from "./views/relays/media-servers";
|
||||
import NIP05RelaysView from "./views/relays/nip05";
|
||||
import ContactListRelaysView from "./views/relays/contact-list";
|
||||
import WebRtcRelaysView from "./views/relays/webrtc";
|
||||
import UserDMsTab from "./views/user/dms";
|
||||
import LoginNostrConnectView from "./views/signin/nostr-connect";
|
||||
import ThreadsNotificationsView from "./views/notifications/threads";
|
||||
@ -291,6 +292,7 @@ const router = createHashRouter([
|
||||
{ path: "media-servers", element: <MediaServersView /> },
|
||||
{ path: "nip05", element: <NIP05RelaysView /> },
|
||||
{ path: "contacts", element: <ContactListRelaysView /> },
|
||||
{ path: "webrtc", element: <WebRtcRelaysView /> },
|
||||
{ path: "sets", element: <BrowseRelaySetsView /> },
|
||||
{ path: ":id", element: <RelaySetView /> },
|
||||
],
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NostrEvent, AbstractRelay } from "nostr-tools";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import _throttle from "lodash.throttle";
|
||||
import debug, { Debugger } from "debug";
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NostrEvent, AbstractRelay } from "nostr-tools";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import _throttle from "lodash.throttle";
|
||||
import debug, { Debugger } from "debug";
|
||||
import { getEventUID } from "nostr-idb";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Filter, NostrEvent, AbstractRelay } from "nostr-tools";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import _throttle from "lodash.throttle";
|
||||
import debug, { Debugger } from "debug";
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NostrEvent, AbstractRelay } from "nostr-tools";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import _throttle from "lodash.throttle";
|
||||
import debug, { Debugger } from "debug";
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Debugger } from "debug";
|
||||
import { AbstractRelay, Filter, NostrEvent, matchFilters } from "nostr-tools";
|
||||
import { Filter, NostrEvent, matchFilters } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import { SimpleRelay } from "nostr-idb";
|
||||
import _throttle from "lodash.throttle";
|
||||
import { nanoid } from "nanoid";
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { Filter } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import { isFilterEqual } from "../helpers/nostr/filter";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
import { AbstractRelay, Filter } from "nostr-tools";
|
||||
import { offlineMode } from "../services/offline-mode";
|
||||
import PersistentSubscription from "./persistent-subscription";
|
||||
import Process from "./process";
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { NostrEvent, AbstractRelay } from "nostr-tools";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import createDefer from "./deferred";
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { Filter, NostrEvent, Relay, Subscription } from "nostr-tools";
|
||||
import { Filter, NostrEvent, Relay } from "nostr-tools";
|
||||
import { Subscription } from "nostr-tools/abstract-relay";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import ControlledObservable from "./controlled-observable";
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import { AbstractRelay, Filter, Relay, Subscription, SubscriptionParams } from "nostr-tools";
|
||||
import { Filter, Relay } from "nostr-tools";
|
||||
import { AbstractRelay, Subscription, SubscriptionParams } from "nostr-tools/abstract-relay";
|
||||
|
||||
import relayPoolService from "../services/relay-pool";
|
||||
import Process from "./process";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ComponentWithAs, IconProps } from "@chakra-ui/react";
|
||||
import { SimpleRelay } from "nostr-idb";
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
let lastId = 0;
|
||||
export default class Process {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { logger } from "../helpers/debug";
|
||||
|
@ -1,6 +1,7 @@
|
||||
import dayjs from "dayjs";
|
||||
import { Debugger } from "debug";
|
||||
import { AbstractRelay, Filter, NostrEvent } from "nostr-tools";
|
||||
import { Filter, NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import MultiSubscription from "./multi-subscription";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Suspense, lazy } from "react";
|
||||
import type { DecodeResult } from "nostr-tools/lib/types/nip19";
|
||||
import type { DecodeResult } from "nostr-tools/nip19";
|
||||
import { CardProps, Spinner } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { EventTemplate, NostrEvent, kinds } from "nostr-tools";
|
||||
import dayjs from "dayjs";
|
||||
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import type { AddressPointer } from "nostr-tools/nip19";
|
||||
|
||||
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../../icons";
|
||||
import relayHintService from "../../../../services/event-relay-hint";
|
||||
|
@ -4,7 +4,7 @@ import { Select, SelectProps } from "@chakra-ui/react";
|
||||
import useUserCommunitiesList from "../../hooks/use-user-communities-list";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { getCommunityName } from "../../helpers/nostr/communities";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
||||
import { getEventCoordinate } from "../../helpers/nostr/event";
|
||||
|
||||
|
@ -1,14 +1,6 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
ButtonProps,
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
useForceUpdate,
|
||||
useInterval,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import { IconButton, IconButtonProps, useForceUpdate, useInterval, useToast } from "@chakra-ui/react";
|
||||
import { type AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import relayPoolService from "../../services/relay-pool";
|
||||
import { useSigningContext } from "../../providers/global/signing-provider";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ChangeEventHandler } from "react";
|
||||
import { Switch, useForceUpdate, useInterval, useToast } from "@chakra-ui/react";
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import { type AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import relayPoolService from "../../services/relay-pool";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Badge, useForceUpdate } from "@chakra-ui/react";
|
||||
import { useInterval } from "react-use";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import relayPoolService from "../../services/relay-pool";
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
|
||||
const getStatusText = (relay: AbstractRelay, connecting = false) => {
|
||||
|
@ -5,7 +5,7 @@ import { nanoid } from "nanoid";
|
||||
|
||||
import { ATag, DraftNostrEvent, ETag, isATag, isDTag, isETag, isPTag, NostrEvent, Tag } from "../../types/nostr-event";
|
||||
import { getMatchNostrLink } from "../regexp";
|
||||
import { AddressPointer, DecodeResult, EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer, DecodeResult, EventPointer } from "nostr-tools/nip19";
|
||||
import { safeJson } from "../parse";
|
||||
import { safeDecode } from "../nip19";
|
||||
import { safeRelayUrl, safeRelayUrls } from "../relay";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import { NostrEvent, isRTag } from "../../types/nostr-event";
|
||||
import { DecodeResult } from "nostr-tools/lib/types/nip19";
|
||||
import { DecodeResult } from "nostr-tools/nip19";
|
||||
import { getPointerFromTag } from "../nip19";
|
||||
|
||||
export const GOAL_KIND = 9041;
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { AbstractRelay, Filter, SubCloser, SubscribeManyParams, Subscription } from "nostr-tools";
|
||||
import { Filter } from "nostr-tools";
|
||||
import { SubCloser, SubscribeManyParams } from "nostr-tools/abstract-pool";
|
||||
import { AbstractRelay, Subscription } from "nostr-tools/abstract-relay";
|
||||
|
||||
// NOTE: only use this for equality checks and querying
|
||||
export function getRelayVariations(relay: string) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import type { AddressPointer } from "nostr-tools/nip19";
|
||||
|
||||
import useReplaceableEvent from "./use-replaceable-event";
|
||||
import { parseDVMMetadata } from "../helpers/nostr/dvm";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import type { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import type { AddressPointer } from "nostr-tools/nip19";
|
||||
|
||||
import { CustomAddressPointer, parseCoordinate } from "../helpers/nostr/event";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import type { EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import type { EventPointer } from "nostr-tools/nip19";
|
||||
|
||||
import { isHexKey } from "../helpers/nip19";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import type { ProfilePointer } from "nostr-tools/lib/types/nip19";
|
||||
import type { ProfilePointer } from "nostr-tools/nip19";
|
||||
import { isHexKey } from "../helpers/nip19";
|
||||
|
||||
export default function useParamsProfilePointer(key: string = "pubkey"): ProfilePointer {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AbstractRelay, NostrEvent } from "nostr-tools";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import { WIKI_PAGE_KIND } from "../helpers/nostr/wiki";
|
||||
import { logger } from "../helpers/debug";
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AbstractRelay, kinds } from "nostr-tools";
|
||||
import { kinds } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import Subject from "../classes/subject";
|
||||
import SuperMap from "../classes/super-map";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
import type { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import type { AddressPointer, EventPointer } from "nostr-tools/nip19";
|
||||
|
||||
import { NostrEvent, isDTag } from "../types/nostr-event";
|
||||
import relayScoreboardService from "./relay-scoreboard";
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { AbstractRelay, kinds } from "nostr-tools";
|
||||
import { kinds } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import Subject from "../classes/subject";
|
||||
import SuperMap from "../classes/super-map";
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CacheRelay, openDB } from "nostr-idb";
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import { logger } from "../helpers/debug";
|
||||
import { safeRelayUrl } from "../helpers/relay";
|
||||
import WasmRelay from "./wasm-relay";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import Process from "../classes/process";
|
||||
import relayPoolService from "./relay-pool";
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AbstractRelay, NostrEvent } from "nostr-tools";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
import _throttle from "lodash.throttle";
|
||||
|
||||
import SuperMap from "../classes/super-map";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { nip04, getPublicKey, finalizeEvent } from "nostr-tools";
|
||||
import { nip04, getPublicKey, finalizeEvent, EventTemplate } from "nostr-tools";
|
||||
import { hexToBytes } from "@noble/hashes/utils";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../types/nostr-event";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import { Account } from "./account";
|
||||
import db from "./db";
|
||||
import serialPortService from "./serial-port";
|
||||
@ -96,7 +96,7 @@ class SigningService {
|
||||
return await p;
|
||||
}
|
||||
|
||||
async requestSignature(draft: DraftNostrEvent, account: Account) {
|
||||
async requestSignature(draft: EventTemplate, account: Account) {
|
||||
const checkSig = (signed: NostrEvent) => {
|
||||
if (signed.pubkey !== account.pubkey) throw new Error("Signed with the wrong pubkey");
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import _throttle from "lodash.throttle";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import SuperMap from "../classes/super-map";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
@ -7,7 +8,6 @@ import { logger } from "../helpers/debug";
|
||||
import Subject from "../classes/subject";
|
||||
import relayPoolService from "./relay-pool";
|
||||
import Process from "../classes/process";
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import processManager from "./process-manager";
|
||||
import Code02 from "../components/icons/code-02";
|
||||
import BatchEventLoader from "../classes/batch-event-loader";
|
||||
|
8
src/types/nostr-extensions.d.ts
vendored
8
src/types/nostr-extensions.d.ts
vendored
@ -1,15 +1,19 @@
|
||||
import { DraftNostrEvent, NostrEvent } from "./nostr-event";
|
||||
import { EventTemplate, NostrEvent, UnsignedEvent } from "nostr-tools";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: {
|
||||
getPublicKey: () => Promise<string> | string;
|
||||
signEvent: (event: DraftNostrEvent) => Promise<NostrEvent> | NostrEvent;
|
||||
signEvent: (event: EventTemplate) => Promise<NostrEvent> | NostrEvent;
|
||||
getRelays?: () => Record<string, { read: boolean; write: boolean }> | string[];
|
||||
nip04?: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
||||
};
|
||||
nip44?: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
3
src/types/nostr-tools-wasm.d.ts
vendored
3
src/types/nostr-tools-wasm.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
declare module "nostr-tools/wasm" {
|
||||
export * from "nostr-tools/lib/types/wasm.d.ts";
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { AddressPointer, EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer, EventPointer } from "nostr-tools/nip19";
|
||||
import { Button, ButtonGroup, Flex, Heading, SkeletonText, Spinner } from "@chakra-ui/react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { EventPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { EventPointer } from "nostr-tools/nip19";
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
|
@ -23,7 +23,7 @@ import useCountCommunityMembers from "../../../hooks/use-count-community-members
|
||||
import { readablizeSats } from "../../../helpers/bolt11";
|
||||
import User01 from "../../../components/icons/user-01";
|
||||
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||
|
||||
function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
||||
|
@ -15,7 +15,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { ChevronLeftIcon } from "../../components/icons";
|
||||
import { parseCoordinate } from "../../helpers/nostr/event";
|
||||
import UserAvatarLink from "../../components/user/user-avatar-link";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
|
||||
export function useUsersJoinedCommunitiesLists(pubkeys: string[], additionalRelays?: Iterable<string>) {
|
||||
const readRelays = useReadRelays(additionalRelays);
|
||||
|
@ -5,7 +5,7 @@ import { Box, BoxProps } from "@chakra-ui/react";
|
||||
|
||||
import useUserMetadata from "../../../hooks/use-user-metadata";
|
||||
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
|
||||
|
||||
type DVMAvatarProps = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Card, CardProps, Heading, LinkBox, LinkOverlayProps, Text } from "@chakra-ui/react";
|
||||
import { Link as RouterLink, To } from "react-router-dom";
|
||||
import { useMemo } from "react";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||
|
@ -4,7 +4,7 @@ import { nip19 } from "nostr-tools";
|
||||
|
||||
import useUserMetadata from "../../../hooks/use-user-metadata";
|
||||
import { getDisplayName } from "../../../helpers/nostr/user-metadata";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
|
||||
|
||||
export function DVMName({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
|
||||
import { Select } from "@chakra-ui/react";
|
||||
|
||||
|
@ -26,7 +26,7 @@ import { DraftNostrEvent } from "../../../types/nostr-event";
|
||||
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||
import { DVMAvatarLink } from "./dvm-avatar";
|
||||
import DVMLink from "./dvm-name";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ChainedDVMJob, getEventIdsFromJobs } from "../../../helpers/nostr/dvm";
|
||||
import FeedStatus from "./feed-status";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useSingleEvents from "../../../hooks/use-single-events";
|
||||
import TimelineItem from "../../../components/timeline-page/generic-note-timeline/timeline-item";
|
||||
|
||||
|
@ -33,7 +33,7 @@ import RequireCurrentAccount from "../../providers/route/require-current-account
|
||||
import { CodeIcon } from "../../components/icons";
|
||||
import DebugChains from "./components/debug-chains";
|
||||
import Feed from "./components/feed";
|
||||
import { AddressPointer } from "nostr-tools/lib/types/nip19";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
|
||||
import DVMParams from "./components/dvm-params";
|
||||
import useUserMailboxes from "../../hooks/use-user-mailboxes";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { kinds, nip19 } from "nostr-tools";
|
||||
import type { DecodeResult } from "nostr-tools/lib/types/nip19";
|
||||
import type { DecodeResult } from "nostr-tools/nip19";
|
||||
import { Box, Button, Flex, Heading, SimpleGrid, Spacer, Spinner, Text } from "@chakra-ui/react";
|
||||
|
||||
import UserLink from "../../../components/user/user-link";
|
||||
|
@ -12,6 +12,7 @@ import { useUserDNSIdentity } from "../../hooks/use-user-dns-identity";
|
||||
import useUserContactRelays from "../../hooks/use-user-contact-relays";
|
||||
import UserSquare from "../../components/icons/user-square";
|
||||
import Image01 from "../../components/icons/image-01";
|
||||
import Server05 from "../../components/icons/server-05";
|
||||
|
||||
export default function RelaysView() {
|
||||
const account = useCurrentAccount();
|
||||
@ -69,6 +70,15 @@ export default function RelaysView() {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{/* <Button
|
||||
variant="outline"
|
||||
as={RouterLink}
|
||||
to="/relays/webrtc"
|
||||
leftIcon={<Server05 boxSize={6} />}
|
||||
colorScheme={location.pathname.startsWith("/relays/webrtc") ? "primary" : undefined}
|
||||
>
|
||||
WebRTC Relays
|
||||
</Button> */}
|
||||
{nip05?.exists && (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
356
src/views/relays/webrtc/connect.tsx
Normal file
356
src/views/relays/webrtc/connect.tsx
Normal file
@ -0,0 +1,356 @@
|
||||
import { Debugger } from "debug";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
EventTemplate,
|
||||
Filter,
|
||||
NostrEvent,
|
||||
SimplePool,
|
||||
finalizeEvent,
|
||||
generateSecretKey,
|
||||
getPublicKey,
|
||||
nip44,
|
||||
} from "nostr-tools";
|
||||
import { SubCloser, SubscribeManyParams } from "nostr-tools/abstract-pool";
|
||||
|
||||
import { logger } from "../../../helpers/debug";
|
||||
|
||||
const RTCDescriptionEventKind = 25050;
|
||||
const RTCICEEventKind = 25051;
|
||||
type Signer = {
|
||||
getPublicKey: () => Promise<string> | string;
|
||||
signEvent: (event: EventTemplate) => Promise<NostrEvent> | NostrEvent;
|
||||
nip44: {
|
||||
encrypt: (pubkey: string, plaintext: string) => Promise<string> | string;
|
||||
decrypt: (pubkey: string, ciphertext: string) => Promise<string> | string;
|
||||
};
|
||||
};
|
||||
|
||||
type Pool = {
|
||||
subscribeMany(relays: string[], filters: Filter[], params: SubscribeManyParams): SubCloser;
|
||||
publish(relays: string[], event: NostrEvent): Promise<string>[];
|
||||
};
|
||||
|
||||
type EventMap = {
|
||||
connect: [];
|
||||
disconnect: [];
|
||||
incomingCall: [NostrEvent];
|
||||
message: [string];
|
||||
};
|
||||
|
||||
class SimpleSigner {
|
||||
key: Uint8Array;
|
||||
constructor() {
|
||||
this.key = generateSecretKey();
|
||||
}
|
||||
|
||||
async getPublicKey() {
|
||||
return getPublicKey(this.key);
|
||||
}
|
||||
async signEvent(event: EventTemplate) {
|
||||
return finalizeEvent(event, this.key);
|
||||
}
|
||||
|
||||
nip44 = {
|
||||
encrypt: async (pubkey: string, plaintext: string) =>
|
||||
nip44.v2.encrypt(plaintext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
|
||||
decrypt: async (pubkey: string, ciphertext: string) =>
|
||||
nip44.v2.decrypt(ciphertext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
|
||||
};
|
||||
}
|
||||
|
||||
const defaultPool = new SimplePool();
|
||||
|
||||
class WebRTCPeer extends EventEmitter<EventMap> {
|
||||
log: Debugger;
|
||||
signer: Signer;
|
||||
pool: Pool;
|
||||
peer?: string;
|
||||
relays: string[] = [];
|
||||
iceServers: RTCIceServer[] = [];
|
||||
|
||||
connection?: RTCPeerConnection;
|
||||
channel?: RTCDataChannel;
|
||||
|
||||
listening = false;
|
||||
subscription?: SubCloser;
|
||||
|
||||
async isCaller() {
|
||||
if (!this.offerEvent) return null;
|
||||
return (await this.signer.getPublicKey()) === this.offerEvent?.pubkey;
|
||||
}
|
||||
get offer() {
|
||||
return this.connection?.localDescription;
|
||||
}
|
||||
offerEvent?: NostrEvent;
|
||||
get answer() {
|
||||
return this.connection?.remoteDescription;
|
||||
}
|
||||
answerEvent?: NostrEvent;
|
||||
|
||||
private candidateQueue: RTCIceCandidateInit[] = [];
|
||||
|
||||
constructor(signer: Signer, pool: Pool = defaultPool, relays?: string[], iceServers?: RTCIceServer[]) {
|
||||
super();
|
||||
this.log = logger.extend(`webrtc`);
|
||||
this.signer = signer;
|
||||
this.pool = pool;
|
||||
|
||||
if (iceServers) this.iceServers = iceServers;
|
||||
if (relays) this.relays = relays;
|
||||
}
|
||||
|
||||
private createConnection() {
|
||||
if (this.connection) return this.connection;
|
||||
|
||||
this.connection = new RTCPeerConnection({ iceServers: this.iceServers });
|
||||
this.log("Created local connection");
|
||||
|
||||
this.connection.onicecandidate = async ({ candidate }) => {
|
||||
if (candidate) {
|
||||
this.candidateQueue.push(candidate.toJSON());
|
||||
} else this.flushCandidateQueue();
|
||||
};
|
||||
|
||||
this.connection.onicegatheringstatechange = this.flushCandidateQueue.bind(this);
|
||||
|
||||
this.connection.ondatachannel = ({ channel }) => {
|
||||
this.log("Got data channel", channel);
|
||||
|
||||
if (channel.label !== "nostr") return;
|
||||
|
||||
this.channel = channel;
|
||||
this.channel.onclose = this.onChannelStateChange.bind(this);
|
||||
this.channel.onopen = this.onChannelStateChange.bind(this);
|
||||
this.channel.onmessage = this.handleChannelMessage.bind(this);
|
||||
};
|
||||
|
||||
return this.connection;
|
||||
}
|
||||
|
||||
private async flushCandidateQueue() {
|
||||
if (this.connection?.iceGatheringState !== "complete") return;
|
||||
|
||||
if (this.offerEvent && this.answerEvent && this.peer && this.candidateQueue.length > 0) {
|
||||
const cipherText = await this.signer.nip44.encrypt(this.peer, JSON.stringify(this.candidateQueue));
|
||||
const iceEvent = await this.signer.signEvent({
|
||||
kind: RTCICEEventKind,
|
||||
content: cipherText,
|
||||
tags: [["e", this.offerEvent.id]],
|
||||
created_at: dayjs().unix(),
|
||||
});
|
||||
|
||||
this.log(`Publishing ICE candidates`, this.candidateQueue);
|
||||
await this.pool.publish(this.relays, iceEvent);
|
||||
this.candidateQueue = [];
|
||||
}
|
||||
}
|
||||
|
||||
async makeCall(peer: string) {
|
||||
if (this.peer) throw new Error("Already calling peer");
|
||||
|
||||
this.stopListening();
|
||||
const pc = this.createConnection();
|
||||
|
||||
this.channel = pc.createDataChannel("nostr", { ordered: true });
|
||||
this.channel.onopen = this.onChannelStateChange.bind(this);
|
||||
this.channel.onclose = this.onChannelStateChange.bind(this);
|
||||
this.channel.onmessage = this.handleChannelMessage.bind(this);
|
||||
|
||||
this.log(`Making call to ${peer} `);
|
||||
|
||||
const offer = await pc.createOffer();
|
||||
const cipherText = await this.signer.nip44.encrypt(peer, JSON.stringify(offer));
|
||||
const offerEvent = await this.signer.signEvent({
|
||||
kind: RTCDescriptionEventKind,
|
||||
content: cipherText,
|
||||
tags: [["p", peer], ...this.relays.map((r) => ["relay", r])],
|
||||
created_at: dayjs().unix(),
|
||||
});
|
||||
|
||||
this.log("Created offer", offer);
|
||||
|
||||
// listen for answers and ice events
|
||||
this.subscription = this.pool.subscribeMany(
|
||||
this.relays,
|
||||
[
|
||||
{
|
||||
kinds: [RTCDescriptionEventKind, RTCICEEventKind],
|
||||
"#e": [offerEvent.id],
|
||||
authors: [peer],
|
||||
},
|
||||
],
|
||||
{
|
||||
onevent: async (event: NostrEvent) => {
|
||||
if (!this.offerEvent) return;
|
||||
if (!event.tags.some((t) => t[0] === "e" && t[1] === this.offerEvent?.id)) return;
|
||||
|
||||
console.log(event);
|
||||
|
||||
switch (event.kind) {
|
||||
case RTCDescriptionEventKind:
|
||||
await this.handleAnswer(event);
|
||||
// got answer, send ICE candidates
|
||||
await this.flushCandidateQueue();
|
||||
break;
|
||||
case RTCICEEventKind:
|
||||
await this.handleICEEvent(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
onclose: () => {
|
||||
this.log("Subscription Closed");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.peer = peer;
|
||||
|
||||
this.log("Publishing event", offerEvent);
|
||||
await this.pool.publish(this.relays, offerEvent);
|
||||
await pc.setLocalDescription(offer);
|
||||
|
||||
this.offerEvent = offerEvent;
|
||||
}
|
||||
|
||||
async handleAnswer(event: NostrEvent) {
|
||||
const pc = this.createConnection();
|
||||
|
||||
if (!pc.localDescription) throw new Error("Got answer without offering");
|
||||
|
||||
const plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
|
||||
const answer = JSON.parse(plaintext) as RTCSessionDescriptionInit;
|
||||
if (answer.type !== "answer") throw new Error("Unexpected rtc description type");
|
||||
|
||||
this.log("Got answer", answer);
|
||||
|
||||
await pc.setRemoteDescription(answer);
|
||||
|
||||
this.answerEvent = event;
|
||||
}
|
||||
|
||||
async answerCall(event: NostrEvent) {
|
||||
this.stopListening();
|
||||
const pc = this.createConnection();
|
||||
|
||||
this.log(`Answering call ${event.id} from ${event.pubkey}`);
|
||||
|
||||
const plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
|
||||
const offer = JSON.parse(plaintext) as RTCSessionDescriptionInit;
|
||||
if (offer.type !== "offer") throw new Error("Unexpected rtc description type");
|
||||
|
||||
this.relays = event.tags.filter((t) => t[0] === "relay" && t[1]).map((t) => t[1]);
|
||||
await pc.setRemoteDescription(offer);
|
||||
|
||||
const answer = await pc.createAnswer();
|
||||
const cipherText = await this.signer.nip44.encrypt(event.pubkey, JSON.stringify(answer));
|
||||
const answerEvent = await this.signer.signEvent({
|
||||
kind: RTCDescriptionEventKind,
|
||||
content: cipherText,
|
||||
tags: [
|
||||
["p", event.pubkey],
|
||||
["e", event.id],
|
||||
],
|
||||
created_at: dayjs().unix(),
|
||||
});
|
||||
|
||||
this.log("Created answer", answer);
|
||||
|
||||
this.peer = event.pubkey;
|
||||
this.offerEvent = event;
|
||||
|
||||
// listen for ice events
|
||||
this.subscription = this.pool.subscribeMany(
|
||||
this.relays,
|
||||
[{ kinds: [RTCICEEventKind], "#e": [event.id], authors: [event.pubkey] }],
|
||||
{
|
||||
onevent: async (event) => {
|
||||
if (!this.offerEvent) return;
|
||||
if (!event.tags.some((t) => t[0] === "e" && t[1] === this.offerEvent?.id)) return;
|
||||
|
||||
switch (event.kind) {
|
||||
case RTCICEEventKind:
|
||||
await this.handleICEEvent(event);
|
||||
break;
|
||||
}
|
||||
},
|
||||
onclose: () => {
|
||||
this.log("Subscription Closed");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
this.log("Publishing event", answerEvent);
|
||||
|
||||
await this.pool.publish(this.relays, answerEvent);
|
||||
await pc.setLocalDescription(answer);
|
||||
this.answerEvent = answerEvent;
|
||||
|
||||
// answered call, send ICE candidates
|
||||
await this.flushCandidateQueue();
|
||||
}
|
||||
|
||||
private async handleICEEvent(event: NostrEvent) {
|
||||
if (!this.connection) throw new Error("Got ICE event without connection");
|
||||
const pc = this.createConnection();
|
||||
|
||||
const plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
|
||||
const candidates = JSON.parse(plaintext) as RTCIceCandidateInit[];
|
||||
|
||||
this.log("Got candidates", candidates);
|
||||
|
||||
for (let candidate of candidates) {
|
||||
await pc.addIceCandidate(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
async listenForCall() {
|
||||
if (this.listening) throw new Error("Already listening");
|
||||
|
||||
this.listening = true;
|
||||
this.subscription = this.pool.subscribeMany(
|
||||
this.relays,
|
||||
[{ kinds: [RTCDescriptionEventKind], "#p": [await this.signer.getPublicKey()], since: dayjs().unix() }],
|
||||
{
|
||||
onevent: (event) => {
|
||||
this.emit("incomingCall", event);
|
||||
},
|
||||
onclose: () => {
|
||||
this.listening = false;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
stopListening() {
|
||||
if (!this.listening) return;
|
||||
|
||||
if (this.subscription) this.subscription.close();
|
||||
this.subscription = undefined;
|
||||
this.listening = false;
|
||||
}
|
||||
|
||||
private onChannelStateChange() {
|
||||
const readyState = this.channel?.readyState;
|
||||
console.log("Send channel state is: " + readyState);
|
||||
}
|
||||
|
||||
private handleChannelMessage(event: MessageEvent<any>) {
|
||||
if (typeof event.data === "string") this.emit("message", event.data);
|
||||
}
|
||||
|
||||
send(message: string) {
|
||||
this.channel?.send(message);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.log("Closing data channel");
|
||||
if (this.channel) this.channel.close();
|
||||
if (this.connection) this.connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
window.SimpleSigner = SimpleSigner;
|
||||
// @ts-expect-error
|
||||
window.WebRTCPeer = WebRTCPeer;
|
30
src/views/relays/webrtc/index.tsx
Normal file
30
src/views/relays/webrtc/index.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Button, ButtonGroup, Code, Flex, Heading, Link, Text } from "@chakra-ui/react";
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { useUserDNSIdentity } from "../../../hooks/use-user-dns-identity";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||
import { QrCodeIcon } from "../../../components/icons";
|
||||
|
||||
import "./connect";
|
||||
|
||||
export default function WebRtcRelaysView() {
|
||||
return (
|
||||
<Flex gap="2" direction="column" overflow="auto hidden" flex={1} px={{ base: "2", lg: 0 }}>
|
||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||
<BackButton hideFrom="lg" size="sm" />
|
||||
<Heading size="lg">WebRTC Relays</Heading>
|
||||
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
<Button leftIcon={<QrCodeIcon />}>Pair</Button>
|
||||
<Button colorScheme="primary">Connect</Button>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
|
||||
{/* <Text fontStyle="italic" mt="-2">
|
||||
These relays cant be modified by noStrudel, they must be set manually on your
|
||||
</Text> */}
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -18,8 +18,8 @@ import {
|
||||
useInterval,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { AbstractRelay } from "nostr-tools";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { AbstractRelay } from "nostr-tools/abstract-relay";
|
||||
|
||||
import relayPoolService from "../../../services/relay-pool";
|
||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||
|
@ -15,7 +15,8 @@ import {
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { AbstractRelay, NostrEvent, Subscription } from "nostr-tools";
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { AbstractRelay, Subscription } from "nostr-tools/abstract-relay";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { Subscription as IDBSubscription } from "nostr-idb";
|
||||
import _throttle from "lodash.throttle";
|
||||
@ -93,7 +94,7 @@ export default function EventConsoleView() {
|
||||
if (!relay || relay.url !== url.toString()) {
|
||||
r = await relayPoolService.requestRelay(url);
|
||||
await relayPoolService.requestConnect(r);
|
||||
setRelay(r);
|
||||
setRelay(r as AbstractRelay);
|
||||
} else r = relay;
|
||||
} else {
|
||||
if (relay) setRelay(null);
|
||||
|
@ -10,7 +10,7 @@
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
|
36
yarn.lock
36
yarn.lock
@ -6218,20 +6218,6 @@ nostr-idb@^2.1.4:
|
||||
idb "^8.0.0"
|
||||
nostr-tools "^2.1.3"
|
||||
|
||||
nostr-tools@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.5.2.tgz#54b445380ac2a7740ad90ed3b044bca93ecf23bd"
|
||||
integrity sha512-Ls2FKh694eudBye6q89yJ5JhXjQle1MWp1yD2sBZ5j9M3IOBEW8ia9IED5W6daSAjlT/Z/pV77yTkdF45c1Rbg==
|
||||
dependencies:
|
||||
"@noble/ciphers" "^0.5.1"
|
||||
"@noble/curves" "1.2.0"
|
||||
"@noble/hashes" "1.3.1"
|
||||
"@scure/base" "1.1.1"
|
||||
"@scure/bip32" "1.3.1"
|
||||
"@scure/bip39" "1.2.1"
|
||||
optionalDependencies:
|
||||
nostr-wasm v0.1.0
|
||||
|
||||
nostr-tools@^1.17.0:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-1.17.0.tgz#b6f62e32fedfd9e68ec0a7ce57f74c44fc768e8c"
|
||||
@ -6272,6 +6258,20 @@ nostr-tools@^2.3.2:
|
||||
optionalDependencies:
|
||||
nostr-wasm v0.1.0
|
||||
|
||||
nostr-tools@^2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.7.1.tgz#cfedfe6c7ebf7f127f3ac32a5b57c7e570c35f67"
|
||||
integrity sha512-4qAvlHSqBAA8lQMwRWE6dalSNdQT77Xut9lPiJZgEcb9RAlR69wR2+KVBAgnZVaabVYH7FJ7gOQXLw/jQBAYBg==
|
||||
dependencies:
|
||||
"@noble/ciphers" "^0.5.1"
|
||||
"@noble/curves" "1.2.0"
|
||||
"@noble/hashes" "1.3.1"
|
||||
"@scure/base" "1.1.1"
|
||||
"@scure/bip32" "1.3.1"
|
||||
"@scure/bip39" "1.2.1"
|
||||
optionalDependencies:
|
||||
nostr-wasm v0.1.0
|
||||
|
||||
nostr-wasm@^0.1.0, nostr-wasm@v0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94"
|
||||
@ -7773,10 +7773,10 @@ typed-array-length@^1.0.4:
|
||||
for-each "^0.3.3"
|
||||
is-typed-array "^1.1.9"
|
||||
|
||||
typescript@^5.3.3:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
typescript@^5.5.3:
|
||||
version "5.5.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa"
|
||||
integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==
|
||||
|
||||
typo-js@*:
|
||||
version "1.2.4"
|
||||
|
Loading…
x
Reference in New Issue
Block a user