mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-02 08:58:36 +02:00
webrtc view improvements
This commit is contained in:
parent
9cb18556dc
commit
11c0ac2ca6
11
src/app.tsx
11
src/app.tsx
@ -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 /> },
|
||||
],
|
||||
|
@ -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[];
|
||||
|
@ -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) {
|
||||
|
@ -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]));
|
||||
|
@ -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"]);
|
||||
|
@ -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> {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
|
101
src/views/relays/webrtc/components/connection.tsx
Normal file
101
src/views/relays/webrtc/components/connection.tsx
Normal 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>
|
||||
);
|
||||
}
|
111
src/views/relays/webrtc/connect.tsx
Normal file
111
src/views/relays/webrtc/connect.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>;
|
||||
}
|
||||
|
151
src/views/relays/webrtc/pair.tsx
Normal file
151
src/views/relays/webrtc/pair.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user