webrtc view improvements

This commit is contained in:
hzrd149 2024-07-23 08:17:00 -05:00
parent 9cb18556dc
commit 11c0ac2ca6
13 changed files with 524 additions and 132 deletions

View File

@ -77,6 +77,7 @@ 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 WebRtcConnectView from "./views/relays/webrtc/connect";
import UserDMsTab from "./views/user/dms";
import LoginNostrConnectView from "./views/signin/nostr-connect";
import ThreadsNotificationsView from "./views/notifications/threads";
@ -91,6 +92,7 @@ import LoginNostrAddressView from "./views/signin/address";
import LoginNostrAddressCreate from "./views/signin/address/create";
import DatabaseView from "./views/relays/cache/database";
import TaskManagerProvider from "./views/task-manager/provider";
import WebRtcPairView from "./views/relays/webrtc/pair";
const TracksView = lazy(() => import("./views/tracks"));
const UserTracksTab = lazy(() => import("./views/user/tracks"));
const UserVideosTab = lazy(() => import("./views/user/videos"));
@ -292,7 +294,14 @@ const router = createHashRouter([
{ path: "media-servers", element: <MediaServersView /> },
{ path: "nip05", element: <NIP05RelaysView /> },
{ path: "contacts", element: <ContactListRelaysView /> },
{ path: "webrtc", element: <WebRtcRelaysView /> },
{
path: "webrtc",
children: [
{ path: "connect", element: <WebRtcConnectView /> },
{ path: "pair", element: <WebRtcPairView /> },
{ path: "", element: <WebRtcRelaysView /> },
],
},
{ path: "sets", element: <BrowseRelaySetsView /> },
{ path: ":id", element: <RelaySetView /> },
],

View File

@ -37,7 +37,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
relays: string[] = [];
iceServers: RTCIceServer[] = [];
connection?: RTCPeerConnection;
connection: RTCPeerConnection;
channel?: RTCDataChannel;
subscription?: SubCloser;
@ -65,11 +65,8 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
if (iceServers) this.iceServers = iceServers;
if (relays) this.relays = relays;
}
private createConnection() {
if (this.connection) return this.connection;
// create connection
this.connection = new RTCPeerConnection({ iceServers: this.iceServers });
this.log("Created local connection");
@ -78,20 +75,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
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.id, channel.label);
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);
};
this.connection.onconnectionstatechange = (event) => {
switch (this.connection?.connectionState) {
case "connected":
@ -103,7 +87,17 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
}
};
return this.connection;
// receive data channel
this.connection.ondatachannel = ({ channel }) => {
this.log("Got data channel", channel.id, channel.label);
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);
};
}
private async flushCandidateQueue() {
@ -127,7 +121,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
async makeCall(peer: string) {
if (this.peer) throw new Error("Already calling peer");
const pc = this.createConnection();
const pc = this.connection;
this.channel = pc.createDataChannel("nostr", { ordered: true });
this.channel.onopen = this.onChannelStateChange.bind(this);
@ -191,7 +185,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
}
async handleAnswer(event: NostrEvent) {
const pc = this.createConnection();
const pc = this.connection;
if (!pc.localDescription) throw new Error("Got answer without offering");
@ -207,7 +201,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
}
async answerCall(event: NostrEvent) {
const pc = this.createConnection();
const pc = this.connection;
this.log(`Answering call ${event.id} from ${event.pubkey}`);
@ -270,7 +264,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
private async handleICEEvent(event: NostrEvent) {
if (!this.connection) throw new Error("Got ICE event without connection");
const pc = this.createConnection();
const pc = this.connection;
const plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
const candidates = JSON.parse(plaintext) as RTCIceCandidateInit[];

View File

@ -1,3 +1,4 @@
import { NostrEvent } from "nostr-tools";
import NostrWebRTCPeer from "./nostr-webrtc-peer";
import { AbstractRelay, AbstractRelayConstructorOptions } from "nostr-tools/abstract-relay";
@ -95,6 +96,13 @@ export class WebRtcWebSocket extends EventTarget implements WebSocket {
}
export default class WebRtcRelayClient extends AbstractRelay {
stats = {
events: {
published: 0,
received: 0,
},
};
constructor(peer: NostrWebRTCPeer, opts: AbstractRelayConstructorOptions) {
super("wss://example.com", opts);
@ -108,6 +116,11 @@ export default class WebRtcRelayClient extends AbstractRelay {
return new WebRtcWebSocket(peer);
};
}
publish(event: NostrEvent): Promise<string> {
this.stats.events.published++;
return super.publish(event);
}
}
if (import.meta.env.DEV) {

View File

@ -18,6 +18,13 @@ export default class WebRtcRelayServer extends EventEmitter<EventMap> {
// A map of subscriptions
subscriptions = new Map<string, Subscription>();
stats = {
events: {
sent: 0,
received: 0,
},
};
constructor(peer: NostrWebRTCPeer, upstream: AbstractRelay) {
super();
this.peer = peer;
@ -42,11 +49,13 @@ export default class WebRtcRelayServer extends EventEmitter<EventMap> {
// Pass the data to appropriate handler
switch (data[0]) {
case "REQ":
case "COUNT":
await this.handleSubscriptionMessage(data);
break;
case "EVENT":
await this.handleEventMessage(data);
// only handle publish EVENT methods
if (typeof data[1] !== "string") {
await this.handleEventMessage(data);
}
break;
case "CLOSE":
await this.handleCloseMessage(data);
@ -68,7 +77,10 @@ export default class WebRtcRelayServer extends EventEmitter<EventMap> {
sub.fire();
} else {
sub = this.upstream.subscribe(filters, {
onevent: (event) => this.send(["EVENT", id, event]),
onevent: (event) => {
this.stats.events.sent++;
this.send(["EVENT", id, event]);
},
onclose: (reason) => this.send(["CLOSED", id, reason]),
oneose: () => this.send(["EOSE", id]),
});
@ -90,6 +102,7 @@ export default class WebRtcRelayServer extends EventEmitter<EventMap> {
try {
const result = await this.upstream.publish(event);
this.stats.events.received++;
this.peer.send(JSON.stringify(["OK", event.id, true, result]));
} catch (error) {
if (error instanceof Error) this.peer.send(JSON.stringify(["OK", event.id, false, error.message]));

View File

@ -10,3 +10,5 @@ export const SEARCH_RELAYS = safeRelayUrls([
export const WIKI_RELAYS = safeRelayUrls(["wss://relay.wikifreedia.xyz/"]);
export const COMMON_CONTACT_RELAY = safeRelayUrl("wss://purplepag.es") as string;
export const COMMON_CONTACT_RELAYS = [COMMON_CONTACT_RELAY];
export const DEFAULT_SIGNAL_RELAYS = safeRelayUrls(["wss://nostrue.com/", "wss://relay.damus.io"]);

View File

@ -1,10 +1,11 @@
import { NostrEvent } from "nostr-tools";
import accountService from "./account";
import { RelayMode } from "../classes/relay";
import userMailboxesService from "./user-mailboxes";
import { PersistentSubject } from "../classes/subject";
import { logger } from "../helpers/debug";
import RelaySet from "../classes/relay-set";
import { NostrEvent } from "nostr-tools";
import { safeRelayUrls } from "../helpers/relay";
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
@ -23,6 +24,10 @@ export const recommendedWriteRelays = new RelaySet(
safeRelayUrls(["wss://relay.damus.io/", "wss://nos.lol/", "wss://purplerelay.com/"]),
);
function isHttpRelay(url: string) {
return url.includes("ws://");
}
class ClientRelayService {
readRelays = new PersistentSubject(new RelaySet());
writeRelays = new PersistentSubject(new RelaySet());
@ -67,8 +72,8 @@ class ClientRelayService {
}
saveRelays() {
localStorage.setItem("read-relays", this.readRelays.value.urls.join(","));
localStorage.setItem("write-relays", this.writeRelays.value.urls.join(","));
localStorage.setItem("read-relays", this.readRelays.value.urls.filter(isHttpRelay).join(","));
localStorage.setItem("write-relays", this.writeRelays.value.urls.filter(isHttpRelay).join(","));
}
get outbox(): Iterable<string> {

View File

@ -2,6 +2,7 @@ import { generateSecretKey } from "nostr-tools";
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
import { PersistentSubject } from "../classes/subject";
import { DEFAULT_SIGNAL_RELAYS } from "../const";
class NullableLocalStorageEntry<T = string> extends PersistentSubject<T | null> {
key: string;
@ -131,12 +132,6 @@ const enableNoteThreadDrawer = new LocalStorageEntry(
);
// webrtc relay
const webRtcUseLocalIdentity = new LocalStorageEntry(
"nostr-webrtc-use-identity",
true,
(raw) => raw === "true",
(v) => String(v),
);
const webRtcLocalIdentity = new LocalStorageEntry(
"nostr-webrtc-identity",
generateSecretKey(),
@ -144,13 +139,26 @@ const webRtcLocalIdentity = new LocalStorageEntry(
(key) => bytesToHex(key),
true,
);
const webRtcSignalingRelays = new LocalStorageEntry(
"nostr-webrtc-signaling-relays",
DEFAULT_SIGNAL_RELAYS,
(raw) => raw.split(","),
(value) => value.join(","),
);
const webRtcRecentConnections = new LocalStorageEntry(
"nostr-webrtc-recent-connections",
[],
(raw) => raw.split(","),
(value) => value.join(","),
);
const localSettings = {
idbMaxEvents,
wasmPersistForDays,
enableNoteThreadDrawer,
webRtcUseLocalIdentity,
webRtcLocalIdentity,
webRtcSignalingRelays,
webRtcRecentConnections,
};
if (import.meta.env.DEV) {

View File

@ -9,20 +9,39 @@ import verifyEventMethod from "./verify-event";
import SimpleSigner from "../classes/simple-signer";
import { localRelay } from "./local-relay";
import localSettings from "./local-settings";
import NostrWebRTCPeer from "../classes/nostr-webrtc-peer";
class WebRtcRelaysService {
log = logger.extend("NostrWebRtcBroker");
broker: NostrWebRtcBroker;
pubkey?: string;
upstream: AbstractRelay | null;
approved: string[] = [];
calls: NostrEvent[] = [];
get answered() {
return this.calls.filter((event) => this.broker.peers.has(event.pubkey));
const answered: { call: NostrEvent; peer: NostrWebRTCPeer; pubkey: string }[] = [];
for (const call of this.calls) {
const peer = this.broker.peers.get(call.pubkey);
if (peer && peer.peer && peer.connection.connectionState !== "new") {
answered.push({ call, peer, pubkey: peer.peer });
}
}
return answered;
}
get unanswered() {
return this.calls.filter((event) => this.broker.peers.has(event.pubkey) === false);
get pendingOutgoing() {
const pending: { call: NostrEvent; peer: NostrWebRTCPeer }[] = [];
for (const call of this.calls) {
const pubkey = call.tags.find((t) => (t[0] = "p" && t[1]))?.[1];
if (!pubkey) continue;
const peer = this.broker.peers.get(pubkey);
if (peer && peer.connection.connectionState === "new") pending.push({ call, peer });
}
return pending;
}
get pendingIncoming() {
return this.calls.filter((event) => event.pubkey !== this.pubkey && this.broker.peers.has(event.pubkey) === false);
}
clients = new Map<string, WebRtcRelayClient>();
@ -35,11 +54,18 @@ class WebRtcRelaysService {
constructor(broker: NostrWebRtcBroker, upstream: AbstractRelay | null) {
this.upstream = upstream;
this.broker = broker;
this.getPubkey();
}
private async getPubkey() {
const pubkey = await this.broker.signer.getPublicKey();
this.pubkey = pubkey;
}
async handleCall(event: NostrEvent) {
if (!this.calls.includes(event)) {
this.log(`Received request from ${event.pubkey}`);
this.log(`Received call from ${event.pubkey}`);
this.calls.push(event);
}
@ -62,7 +88,7 @@ class WebRtcRelaysService {
}
async acceptCall(event: NostrEvent) {
this.log(`Accepting connection from ${event.pubkey}`);
this.log(`Approving calls from ${event.pubkey}`);
this.approved.push(event.pubkey);
await this.handleCall(event);
}
@ -72,6 +98,9 @@ class WebRtcRelaysService {
const peer = await this.broker.requestConnection(uri);
if (!peer.peer) return;
// add to the list of calls
if (peer.offerEvent) this.calls.push(peer.offerEvent);
if (this.upstream) {
const server = new WebRtcRelayServer(peer, this.upstream);
this.servers.set(peer.peer, server);

View File

@ -71,7 +71,7 @@ export default function RelaysView() {
</Button>
</>
)}
{/* <Button
<Button
variant="outline"
as={RouterLink}
to="/relays/webrtc"
@ -79,7 +79,7 @@ export default function RelaysView() {
colorScheme={location.pathname.startsWith("/relays/webrtc") ? "primary" : undefined}
>
WebRTC Relays
</Button> */}
</Button>
{nip05?.exists && (
<Button
variant="outline"

View File

@ -0,0 +1,101 @@
import { useState } from "react";
import { NostrEvent } from "nostr-tools";
import { Button, ButtonGroup, Flex, Heading, SimpleGrid, Text, useForceUpdate, useInterval } from "@chakra-ui/react";
import UserAvatar from "../../../../components/user/user-avatar";
import UserName from "../../../../components/user/user-name";
import NostrWebRTCPeer from "../../../../classes/nostr-webrtc-peer";
import WebRtcRelayClient from "../../../../classes/webrtc-relay-client";
import WebRtcRelayServer from "../../../../classes/webrtc-relay-server";
import { localRelay } from "../../../../services/local-relay";
import useCurrentAccount from "../../../../hooks/use-current-account";
import useUserContactList from "../../../../hooks/use-user-contact-list";
import { getPubkeysFromList } from "../../../../helpers/nostr/lists";
export default function Connection({
call,
peer,
client,
server,
}: {
call: NostrEvent;
peer: NostrWebRTCPeer;
client: WebRtcRelayClient;
server: WebRtcRelayServer;
}) {
const update = useForceUpdate();
useInterval(update, 1000);
// const toggleRead = () => {
// if(clientRelaysService.readRelays.value.has(client))
// };
const account = useCurrentAccount();
const contacts = useUserContactList(account?.pubkey);
const [sending, setSending] = useState(false);
const sendEvents = async () => {
if (!account?.pubkey || !localRelay) return;
setSending(true);
const sub = localRelay.subscribe([{ authors: [account.pubkey] }], {
onevent: (event) => {
client.publish(event);
update();
},
oneose: () => {
sub.close();
setSending(false);
},
});
};
const [requesting, setRequesting] = useState(false);
const requestEvents = async () => {
if (!contacts || !localRelay) return;
setRequesting(true);
const sub = client.subscribe([{ authors: getPubkeysFromList(contacts).map((p) => p.pubkey) }], {
onevent: (event) => {
if (localRelay) localRelay.publish(event);
update();
},
oneose: () => {
sub.close();
setRequesting(false);
},
});
};
return (
<Flex key={call.id} borderWidth="1px" rounded="md" p="2" gap="2" direction="column">
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={call.pubkey} size="sm" />
<UserName pubkey={call.pubkey} />
<Text>{peer.connection?.connectionState ?? "Unknown"}</Text>
<Button size="sm" ml="auto" colorScheme="red" isDisabled>
Close
</Button>
</Flex>
<Heading size="sm">Server:</Heading>
<SimpleGrid spacing="2" columns={{ base: 2, md: 3, lg: 4, xl: 5 }}>
<Text>Sent: {server.stats.events.sent}</Text>
<Text>Received: {server.stats.events.received}</Text>
</SimpleGrid>
<Heading size="sm">Client:</Heading>
<SimpleGrid spacing="2" columns={{ base: 2, md: 3, lg: 4, xl: 5 }}>
<Text>Published: {client.stats.events.published}</Text>
<Text>Received: {client.stats.events.received}</Text>
</SimpleGrid>
{account && (
<ButtonGroup ml="auto" size="sm">
<Button onClick={sendEvents} isLoading={sending}>
Send events
</Button>
<Button onClick={requestEvents} isLoading={requesting}>
Requests contacts
</Button>
</ButtonGroup>
)}
</Flex>
);
}

View File

@ -0,0 +1,111 @@
import { useEffect } from "react";
import { Alert, AlertIcon, Button, Flex, Heading, Input, Text, useForceUpdate, useInterval } from "@chakra-ui/react";
import { useForm } from "react-hook-form";
import BackButton from "../../../components/router/back-button";
import webRtcRelaysService from "../../../services/webrtc-relays";
import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
import localSettings from "../../../services/local-settings";
import useSubject from "../../../hooks/use-subject";
import NostrWebRtcBroker from "../../../classes/nostr-webrtc-broker";
export default function WebRtcConnectView() {
const update = useForceUpdate();
useInterval(update, 1000);
useEffect(() => {
webRtcRelaysService.broker.on("call", update);
return () => {
webRtcRelaysService.broker.off("call", update);
};
}, [update]);
const { register, handleSubmit, formState, reset, setValue } = useForm({
defaultValues: {
uri: "",
},
mode: "all",
});
const connect = handleSubmit(async (values) => {
webRtcRelaysService.connect(values.uri);
localSettings.webRtcRecentConnections.next([...localSettings.webRtcRecentConnections.value, values.uri]);
reset();
});
const recent = useSubject(localSettings.webRtcRecentConnections)
.map((uri) => ({ ...NostrWebRtcBroker.parseNostrWebRtcURI(uri), uri }))
.filter(({ pubkey }) => !webRtcRelaysService.broker.peers.has(pubkey));
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">Connect to WebRTC Relay</Heading>
</Flex>
<Text fontStyle="italic" mt="-2">
Scan or paste the WebRTC Connection URI of the relay you wish to connect to
</Text>
<Flex as="form" gap="2" onSubmit={connect}>
<Input placeholder="webrtc+nostr:npub1..." {...register("uri")} autoComplete="off" />
<QRCodeScannerButton onData={(data) => setValue("uri", data)} />
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
Connect
</Button>
</Flex>
{recent.length > 0 && (
<>
<Heading size="md" mt="2">
Recent Peers:
</Heading>
{recent.map(({ pubkey, uri }) => (
<Flex key={pubkey} borderWidth="1px" rounded="md" p="2" alignItems="center" gap="2">
<UserAvatar pubkey={pubkey} size="sm" />
<UserName pubkey={pubkey} />
<Button
size="sm"
ml="auto"
colorScheme="primary"
onClick={() => {
webRtcRelaysService.connect(uri);
update();
}}
>
Connect
</Button>
</Flex>
))}
</>
)}
<Heading size="md" mt="4">
Pending Connection Requests:
</Heading>
{webRtcRelaysService.pendingOutgoing.length > 0 ? (
<>
{webRtcRelaysService.pendingOutgoing.map(({ call, peer }) => (
<Flex key={call.id} borderWidth="1px" rounded="md" p="2" alignItems="center" gap="2">
{peer.peer && (
<>
<UserAvatar pubkey={peer.peer} size="sm" />
<UserName pubkey={peer.peer} />
</>
)}
<Text>{peer.connection.connectionState}</Text>
</Flex>
))}
</>
) : (
<Alert status="info">
<AlertIcon />
No connections requests
</Alert>
)}
</Flex>
);
}

View File

@ -1,37 +1,26 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect } from "react";
import {
Alert,
AlertIcon,
Button,
ButtonGroup,
Flex,
FormControl,
FormLabel,
Heading,
Input,
Link,
Text,
useForceUpdate,
useInterval,
} from "@chakra-ui/react";
import { getPublicKey, nip19 } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";
import BackButton from "../../../components/router/back-button";
import { QrCodeIcon } from "../../../components/icons";
import webRtcRelaysService from "../../../services/webrtc-relays";
import useSubject from "../../../hooks/use-subject";
import localSettings from "../../../services/local-settings";
import { CopyIconButton } from "../../../components/copy-icon-button";
import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
import { QrCodeIcon } from "../../../components/icons";
import Connection from "./components/connection";
function WebRtcRelaysPage() {
export default function WebRtcRelaysView() {
const update = useForceUpdate();
const identity = useSubject(localSettings.webRtcLocalIdentity);
const pubkey = useMemo(() => getPublicKey(identity), [identity]);
const npub = useMemo(() => nip19.npubEncode(pubkey), [pubkey]);
const uri = "webrtc+nostr:" + npub;
const [connectURI, setConnectURI] = useState("");
useInterval(update, 1000);
useEffect(() => {
webRtcRelaysService.broker.on("call", update);
@ -40,83 +29,50 @@ function WebRtcRelaysPage() {
};
}, [update]);
useInterval(update, 1000);
const unanswered = webRtcRelaysService.pendingIncoming.length;
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 as={RouterLink} to="/relays/webrtc/pair" leftIcon={<QrCodeIcon />}>
Pair{unanswered > 0 ? ` (${unanswered})` : ""}
</Button>
<Button as={RouterLink} to="/relays/webrtc/connect" colorScheme="primary">
Connect
</Button>
</ButtonGroup>
</Flex>
<FormControl>
<FormLabel>WebRTC Connection URI</FormLabel>
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={pubkey} size="sm" />
<Input readOnly userSelect="all" value={uri} />
<CopyIconButton value={uri} aria-label="Copy Npub" />
</Flex>
</FormControl>
<Text fontStyle="italic" mt="-2">
WebRTC Relays are temporary relays that can be accessed over{" "}
<Link href="https://webrtc.org/" target="_blank" color="blue.500">
WebRTC
</Link>
</Text>
<Heading size="md">Connect:</Heading>
<Flex gap="2">
<Input placeholder="webrtc+nostr:npub1..." value={connectURI} onChange={(e) => setConnectURI(e.target.value)} />
<QRCodeScannerButton onData={(data) => setConnectURI(data)} />
<Button
colorScheme="primary"
onClick={() => {
webRtcRelaysService.connect(connectURI);
setConnectURI("");
}}
>
Connect
</Button>
</Flex>
{webRtcRelaysService.answered.length > 0 && (
<>
<Heading size="md">Connections:</Heading>
{webRtcRelaysService.answered.map((event) => (
<Flex key={event.id} borderWidth="1px" rounded="md" p="2" alignItems="center" gap="2">
<UserAvatar pubkey={event.pubkey} size="sm" />
<UserName pubkey={event.pubkey} />
<Button size="sm" ml="auto" colorScheme="red">
Close
</Button>
</Flex>
))}
</>
)}
{webRtcRelaysService.unanswered.length > 0 && (
<>
<Heading size="md">Connection Requests:</Heading>
{webRtcRelaysService.unanswered.map((event) => (
<Flex key={event.id} borderWidth="1px" rounded="md" p="2" alignItems="center" gap="2">
<UserAvatar pubkey={event.pubkey} size="sm" />
<UserName pubkey={event.pubkey} />
<Button
size="sm"
ml="auto"
colorScheme="green"
onClick={() => {
webRtcRelaysService.acceptCall(event);
update();
}}
>
Accept
</Button>
</Flex>
))}
</>
<Heading size="md" mt="2">
Connections:
</Heading>
{webRtcRelaysService.answered.length > 0 ? (
webRtcRelaysService.answered.map(({ call, peer, pubkey }) => (
<Connection
key={pubkey}
peer={peer}
call={call}
client={webRtcRelaysService.clients.get(pubkey)!}
server={webRtcRelaysService.servers.get(pubkey)!}
/>
))
) : (
<Alert status="info">
<AlertIcon />
No connections yet, use the "Invite" or "Connect" buttons to connect to peer
</Alert>
)}
</Flex>
);
}
export default function WebRtcRelaysView() {
if (webRtcRelaysService) {
return <WebRtcRelaysPage />;
}
return <Heading>WebRTC Relays don't work without</Heading>;
}

View File

@ -0,0 +1,151 @@
import { useEffect, useMemo } from "react";
import {
Alert,
AlertIcon,
Box,
Button,
Flex,
FormControl,
FormHelperText,
FormLabel,
Heading,
IconButton,
Input,
Text,
useDisclosure,
useForceUpdate,
useInterval,
} from "@chakra-ui/react";
import { getPublicKey, kinds, nip19 } from "nostr-tools";
import BackButton from "../../../components/router/back-button";
import webRtcRelaysService from "../../../services/webrtc-relays";
import useSubject from "../../../hooks/use-subject";
import localSettings from "../../../services/local-settings";
import { CopyIconButton } from "../../../components/copy-icon-button";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
import QrCodeSvg from "../../../components/qr-code/qr-code-svg";
import { QrCodeIcon } from "../../../components/icons";
import { useForm } from "react-hook-form";
import dayjs from "dayjs";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import useCurrentAccount from "../../../hooks/use-current-account";
import useUserMetadata from "../../../hooks/use-user-metadata";
import { useAsync } from "react-use";
function NameForm() {
const publish = usePublishEvent();
const { register, handleSubmit, formState, reset } = useForm({ defaultValues: { name: "" }, mode: "all" });
const { value: pubkey } = useAsync(async () => webRtcRelaysService.broker.signer.getPublicKey());
const metadata = useUserMetadata(pubkey);
useEffect(() => {
if (metadata?.name) reset({ name: metadata.name }, { keepDirty: false, keepTouched: false });
}, [metadata?.name]);
const submit = handleSubmit(async (values) => {
const event = await webRtcRelaysService.broker.signer.signEvent({
kind: kinds.Metadata,
created_at: dayjs().unix(),
tags: [],
content: JSON.stringify({ name: values.name }),
});
await publish("Set WebRTC name", event);
});
return (
<Flex direction="column" gap="2" as="form" onSubmit={submit}>
<FormControl isRequired>
<FormLabel>Local relay name</FormLabel>
<Flex gap="2">
<Input {...register("name", { required: true })} isRequired autoComplete="off" />
<Button type="submit" isLoading={formState.isSubmitting}>
Set
</Button>
</Flex>
<FormHelperText>The name that will be shown to other peers</FormHelperText>
</FormControl>
</Flex>
);
}
export default function WebRtcPairView() {
const update = useForceUpdate();
useInterval(update, 1000);
useEffect(() => {
webRtcRelaysService.broker.on("call", update);
return () => {
webRtcRelaysService.broker.off("call", update);
};
}, [update]);
const account = useCurrentAccount();
const showQrCode = useDisclosure();
const identity = useSubject(localSettings.webRtcLocalIdentity);
const pubkey = useMemo(() => getPublicKey(identity), [identity]);
const npub = useMemo(() => nip19.npubEncode(pubkey), [pubkey]);
const uri = "webrtc+nostr:" + npub;
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">Pair with WebRTC relay</Heading>
</Flex>
<Text fontStyle="italic" mt="-2">
Share this URI with other users to allow them to connect to your local relay
</Text>
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={pubkey} size="sm" />
<Input readOnly userSelect="all" value={uri} />
<IconButton icon={<QrCodeIcon boxSize="1.5em" />} aria-label="Show QR Code" onClick={showQrCode.onToggle} />
<CopyIconButton value={uri} aria-label="Copy Npub" />
</Flex>
{showQrCode.isOpen && (
<Box w="full" maxW="sm" mx="auto">
<QrCodeSvg content={uri} />
</Box>
)}
{pubkey !== account?.pubkey && <NameForm />}
<Heading size="md" mt="4">
Connection Requests:
</Heading>
{webRtcRelaysService.pendingIncoming.length > 0 ? (
<>
{webRtcRelaysService.pendingIncoming.map((event) => (
<Flex key={event.id} borderWidth="1px" rounded="md" p="2" alignItems="center" gap="2">
<UserAvatar pubkey={event.pubkey} size="sm" />
<UserName pubkey={event.pubkey} />
<Button
size="sm"
ml="auto"
colorScheme="green"
onClick={() => {
webRtcRelaysService.acceptCall(event);
update();
}}
>
Accept
</Button>
</Flex>
))}
</>
) : (
<Alert status="info">
<AlertIcon />
No connections requests
</Alert>
)}
</Flex>
);
}