mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-27 20:17:05 +02:00
webrtc view improvements
This commit is contained in:
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 NIP05RelaysView from "./views/relays/nip05";
|
||||||
import ContactListRelaysView from "./views/relays/contact-list";
|
import ContactListRelaysView from "./views/relays/contact-list";
|
||||||
import WebRtcRelaysView from "./views/relays/webrtc";
|
import WebRtcRelaysView from "./views/relays/webrtc";
|
||||||
|
import WebRtcConnectView from "./views/relays/webrtc/connect";
|
||||||
import UserDMsTab from "./views/user/dms";
|
import UserDMsTab from "./views/user/dms";
|
||||||
import LoginNostrConnectView from "./views/signin/nostr-connect";
|
import LoginNostrConnectView from "./views/signin/nostr-connect";
|
||||||
import ThreadsNotificationsView from "./views/notifications/threads";
|
import ThreadsNotificationsView from "./views/notifications/threads";
|
||||||
@@ -91,6 +92,7 @@ import LoginNostrAddressView from "./views/signin/address";
|
|||||||
import LoginNostrAddressCreate from "./views/signin/address/create";
|
import LoginNostrAddressCreate from "./views/signin/address/create";
|
||||||
import DatabaseView from "./views/relays/cache/database";
|
import DatabaseView from "./views/relays/cache/database";
|
||||||
import TaskManagerProvider from "./views/task-manager/provider";
|
import TaskManagerProvider from "./views/task-manager/provider";
|
||||||
|
import WebRtcPairView from "./views/relays/webrtc/pair";
|
||||||
const TracksView = lazy(() => import("./views/tracks"));
|
const TracksView = lazy(() => import("./views/tracks"));
|
||||||
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
||||||
const UserVideosTab = lazy(() => import("./views/user/videos"));
|
const UserVideosTab = lazy(() => import("./views/user/videos"));
|
||||||
@@ -292,7 +294,14 @@ const router = createHashRouter([
|
|||||||
{ path: "media-servers", element: <MediaServersView /> },
|
{ path: "media-servers", element: <MediaServersView /> },
|
||||||
{ path: "nip05", element: <NIP05RelaysView /> },
|
{ path: "nip05", element: <NIP05RelaysView /> },
|
||||||
{ path: "contacts", element: <ContactListRelaysView /> },
|
{ 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: "sets", element: <BrowseRelaySetsView /> },
|
||||||
{ path: ":id", element: <RelaySetView /> },
|
{ path: ":id", element: <RelaySetView /> },
|
||||||
],
|
],
|
||||||
|
@@ -37,7 +37,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
|
|||||||
relays: string[] = [];
|
relays: string[] = [];
|
||||||
iceServers: RTCIceServer[] = [];
|
iceServers: RTCIceServer[] = [];
|
||||||
|
|
||||||
connection?: RTCPeerConnection;
|
connection: RTCPeerConnection;
|
||||||
channel?: RTCDataChannel;
|
channel?: RTCDataChannel;
|
||||||
|
|
||||||
subscription?: SubCloser;
|
subscription?: SubCloser;
|
||||||
@@ -65,11 +65,8 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
|
|||||||
|
|
||||||
if (iceServers) this.iceServers = iceServers;
|
if (iceServers) this.iceServers = iceServers;
|
||||||
if (relays) this.relays = relays;
|
if (relays) this.relays = relays;
|
||||||
}
|
|
||||||
|
|
||||||
private createConnection() {
|
|
||||||
if (this.connection) return this.connection;
|
|
||||||
|
|
||||||
|
// create connection
|
||||||
this.connection = new RTCPeerConnection({ iceServers: this.iceServers });
|
this.connection = new RTCPeerConnection({ iceServers: this.iceServers });
|
||||||
this.log("Created local connection");
|
this.log("Created local connection");
|
||||||
|
|
||||||
@@ -78,20 +75,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
|
|||||||
this.candidateQueue.push(candidate.toJSON());
|
this.candidateQueue.push(candidate.toJSON());
|
||||||
} else this.flushCandidateQueue();
|
} else this.flushCandidateQueue();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.connection.onicegatheringstatechange = this.flushCandidateQueue.bind(this);
|
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) => {
|
this.connection.onconnectionstatechange = (event) => {
|
||||||
switch (this.connection?.connectionState) {
|
switch (this.connection?.connectionState) {
|
||||||
case "connected":
|
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() {
|
private async flushCandidateQueue() {
|
||||||
@@ -127,7 +121,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
|
|||||||
async makeCall(peer: string) {
|
async makeCall(peer: string) {
|
||||||
if (this.peer) throw new Error("Already calling peer");
|
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 = pc.createDataChannel("nostr", { ordered: true });
|
||||||
this.channel.onopen = this.onChannelStateChange.bind(this);
|
this.channel.onopen = this.onChannelStateChange.bind(this);
|
||||||
@@ -191,7 +185,7 @@ export default class NostrWebRTCPeer extends EventEmitter<EventMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleAnswer(event: NostrEvent) {
|
async handleAnswer(event: NostrEvent) {
|
||||||
const pc = this.createConnection();
|
const pc = this.connection;
|
||||||
|
|
||||||
if (!pc.localDescription) throw new Error("Got answer without offering");
|
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) {
|
async answerCall(event: NostrEvent) {
|
||||||
const pc = this.createConnection();
|
const pc = this.connection;
|
||||||
|
|
||||||
this.log(`Answering call ${event.id} from ${event.pubkey}`);
|
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) {
|
private async handleICEEvent(event: NostrEvent) {
|
||||||
if (!this.connection) throw new Error("Got ICE event without connection");
|
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 plaintext = await this.signer.nip44.decrypt(event.pubkey, event.content);
|
||||||
const candidates = JSON.parse(plaintext) as RTCIceCandidateInit[];
|
const candidates = JSON.parse(plaintext) as RTCIceCandidateInit[];
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { NostrEvent } from "nostr-tools";
|
||||||
import NostrWebRTCPeer from "./nostr-webrtc-peer";
|
import NostrWebRTCPeer from "./nostr-webrtc-peer";
|
||||||
import { AbstractRelay, AbstractRelayConstructorOptions } from "nostr-tools/abstract-relay";
|
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 {
|
export default class WebRtcRelayClient extends AbstractRelay {
|
||||||
|
stats = {
|
||||||
|
events: {
|
||||||
|
published: 0,
|
||||||
|
received: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
constructor(peer: NostrWebRTCPeer, opts: AbstractRelayConstructorOptions) {
|
constructor(peer: NostrWebRTCPeer, opts: AbstractRelayConstructorOptions) {
|
||||||
super("wss://example.com", opts);
|
super("wss://example.com", opts);
|
||||||
|
|
||||||
@@ -108,6 +116,11 @@ export default class WebRtcRelayClient extends AbstractRelay {
|
|||||||
return new WebRtcWebSocket(peer);
|
return new WebRtcWebSocket(peer);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publish(event: NostrEvent): Promise<string> {
|
||||||
|
this.stats.events.published++;
|
||||||
|
return super.publish(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
|
@@ -18,6 +18,13 @@ export default class WebRtcRelayServer extends EventEmitter<EventMap> {
|
|||||||
// A map of subscriptions
|
// A map of subscriptions
|
||||||
subscriptions = new Map<string, Subscription>();
|
subscriptions = new Map<string, Subscription>();
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
events: {
|
||||||
|
sent: 0,
|
||||||
|
received: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
constructor(peer: NostrWebRTCPeer, upstream: AbstractRelay) {
|
constructor(peer: NostrWebRTCPeer, upstream: AbstractRelay) {
|
||||||
super();
|
super();
|
||||||
this.peer = peer;
|
this.peer = peer;
|
||||||
@@ -42,11 +49,13 @@ export default class WebRtcRelayServer extends EventEmitter<EventMap> {
|
|||||||
// Pass the data to appropriate handler
|
// Pass the data to appropriate handler
|
||||||
switch (data[0]) {
|
switch (data[0]) {
|
||||||
case "REQ":
|
case "REQ":
|
||||||
case "COUNT":
|
|
||||||
await this.handleSubscriptionMessage(data);
|
await this.handleSubscriptionMessage(data);
|
||||||
break;
|
break;
|
||||||
case "EVENT":
|
case "EVENT":
|
||||||
await this.handleEventMessage(data);
|
// only handle publish EVENT methods
|
||||||
|
if (typeof data[1] !== "string") {
|
||||||
|
await this.handleEventMessage(data);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "CLOSE":
|
case "CLOSE":
|
||||||
await this.handleCloseMessage(data);
|
await this.handleCloseMessage(data);
|
||||||
@@ -68,7 +77,10 @@ export default class WebRtcRelayServer extends EventEmitter<EventMap> {
|
|||||||
sub.fire();
|
sub.fire();
|
||||||
} else {
|
} else {
|
||||||
sub = this.upstream.subscribe(filters, {
|
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]),
|
onclose: (reason) => this.send(["CLOSED", id, reason]),
|
||||||
oneose: () => this.send(["EOSE", id]),
|
oneose: () => this.send(["EOSE", id]),
|
||||||
});
|
});
|
||||||
@@ -90,6 +102,7 @@ export default class WebRtcRelayServer extends EventEmitter<EventMap> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.upstream.publish(event);
|
const result = await this.upstream.publish(event);
|
||||||
|
this.stats.events.received++;
|
||||||
this.peer.send(JSON.stringify(["OK", event.id, true, result]));
|
this.peer.send(JSON.stringify(["OK", event.id, true, result]));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) this.peer.send(JSON.stringify(["OK", event.id, false, error.message]));
|
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 WIKI_RELAYS = safeRelayUrls(["wss://relay.wikifreedia.xyz/"]);
|
||||||
export const COMMON_CONTACT_RELAY = safeRelayUrl("wss://purplepag.es") as string;
|
export const COMMON_CONTACT_RELAY = safeRelayUrl("wss://purplepag.es") as string;
|
||||||
export const COMMON_CONTACT_RELAYS = [COMMON_CONTACT_RELAY];
|
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 accountService from "./account";
|
||||||
import { RelayMode } from "../classes/relay";
|
import { RelayMode } from "../classes/relay";
|
||||||
import userMailboxesService from "./user-mailboxes";
|
import userMailboxesService from "./user-mailboxes";
|
||||||
import { PersistentSubject } from "../classes/subject";
|
import { PersistentSubject } from "../classes/subject";
|
||||||
import { logger } from "../helpers/debug";
|
import { logger } from "../helpers/debug";
|
||||||
import RelaySet from "../classes/relay-set";
|
import RelaySet from "../classes/relay-set";
|
||||||
import { NostrEvent } from "nostr-tools";
|
|
||||||
import { safeRelayUrls } from "../helpers/relay";
|
import { safeRelayUrls } from "../helpers/relay";
|
||||||
|
|
||||||
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
|
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/"]),
|
safeRelayUrls(["wss://relay.damus.io/", "wss://nos.lol/", "wss://purplerelay.com/"]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function isHttpRelay(url: string) {
|
||||||
|
return url.includes("ws://");
|
||||||
|
}
|
||||||
|
|
||||||
class ClientRelayService {
|
class ClientRelayService {
|
||||||
readRelays = new PersistentSubject(new RelaySet());
|
readRelays = new PersistentSubject(new RelaySet());
|
||||||
writeRelays = new PersistentSubject(new RelaySet());
|
writeRelays = new PersistentSubject(new RelaySet());
|
||||||
@@ -67,8 +72,8 @@ class ClientRelayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveRelays() {
|
saveRelays() {
|
||||||
localStorage.setItem("read-relays", this.readRelays.value.urls.join(","));
|
localStorage.setItem("read-relays", this.readRelays.value.urls.filter(isHttpRelay).join(","));
|
||||||
localStorage.setItem("write-relays", this.writeRelays.value.urls.join(","));
|
localStorage.setItem("write-relays", this.writeRelays.value.urls.filter(isHttpRelay).join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
get outbox(): Iterable<string> {
|
get outbox(): Iterable<string> {
|
||||||
|
@@ -2,6 +2,7 @@ import { generateSecretKey } from "nostr-tools";
|
|||||||
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
||||||
|
|
||||||
import { PersistentSubject } from "../classes/subject";
|
import { PersistentSubject } from "../classes/subject";
|
||||||
|
import { DEFAULT_SIGNAL_RELAYS } from "../const";
|
||||||
|
|
||||||
class NullableLocalStorageEntry<T = string> extends PersistentSubject<T | null> {
|
class NullableLocalStorageEntry<T = string> extends PersistentSubject<T | null> {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -131,12 +132,6 @@ const enableNoteThreadDrawer = new LocalStorageEntry(
|
|||||||
);
|
);
|
||||||
|
|
||||||
// webrtc relay
|
// webrtc relay
|
||||||
const webRtcUseLocalIdentity = new LocalStorageEntry(
|
|
||||||
"nostr-webrtc-use-identity",
|
|
||||||
true,
|
|
||||||
(raw) => raw === "true",
|
|
||||||
(v) => String(v),
|
|
||||||
);
|
|
||||||
const webRtcLocalIdentity = new LocalStorageEntry(
|
const webRtcLocalIdentity = new LocalStorageEntry(
|
||||||
"nostr-webrtc-identity",
|
"nostr-webrtc-identity",
|
||||||
generateSecretKey(),
|
generateSecretKey(),
|
||||||
@@ -144,13 +139,26 @@ const webRtcLocalIdentity = new LocalStorageEntry(
|
|||||||
(key) => bytesToHex(key),
|
(key) => bytesToHex(key),
|
||||||
true,
|
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 = {
|
const localSettings = {
|
||||||
idbMaxEvents,
|
idbMaxEvents,
|
||||||
wasmPersistForDays,
|
wasmPersistForDays,
|
||||||
enableNoteThreadDrawer,
|
enableNoteThreadDrawer,
|
||||||
webRtcUseLocalIdentity,
|
|
||||||
webRtcLocalIdentity,
|
webRtcLocalIdentity,
|
||||||
|
webRtcSignalingRelays,
|
||||||
|
webRtcRecentConnections,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
|
@@ -9,20 +9,39 @@ import verifyEventMethod from "./verify-event";
|
|||||||
import SimpleSigner from "../classes/simple-signer";
|
import SimpleSigner from "../classes/simple-signer";
|
||||||
import { localRelay } from "./local-relay";
|
import { localRelay } from "./local-relay";
|
||||||
import localSettings from "./local-settings";
|
import localSettings from "./local-settings";
|
||||||
|
import NostrWebRTCPeer from "../classes/nostr-webrtc-peer";
|
||||||
|
|
||||||
class WebRtcRelaysService {
|
class WebRtcRelaysService {
|
||||||
log = logger.extend("NostrWebRtcBroker");
|
log = logger.extend("NostrWebRtcBroker");
|
||||||
broker: NostrWebRtcBroker;
|
broker: NostrWebRtcBroker;
|
||||||
|
pubkey?: string;
|
||||||
upstream: AbstractRelay | null;
|
upstream: AbstractRelay | null;
|
||||||
|
|
||||||
approved: string[] = [];
|
approved: string[] = [];
|
||||||
|
|
||||||
calls: NostrEvent[] = [];
|
calls: NostrEvent[] = [];
|
||||||
get answered() {
|
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() {
|
get pendingOutgoing() {
|
||||||
return this.calls.filter((event) => this.broker.peers.has(event.pubkey) === false);
|
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>();
|
clients = new Map<string, WebRtcRelayClient>();
|
||||||
@@ -35,11 +54,18 @@ class WebRtcRelaysService {
|
|||||||
constructor(broker: NostrWebRtcBroker, upstream: AbstractRelay | null) {
|
constructor(broker: NostrWebRtcBroker, upstream: AbstractRelay | null) {
|
||||||
this.upstream = upstream;
|
this.upstream = upstream;
|
||||||
this.broker = broker;
|
this.broker = broker;
|
||||||
|
|
||||||
|
this.getPubkey();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPubkey() {
|
||||||
|
const pubkey = await this.broker.signer.getPublicKey();
|
||||||
|
this.pubkey = pubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleCall(event: NostrEvent) {
|
async handleCall(event: NostrEvent) {
|
||||||
if (!this.calls.includes(event)) {
|
if (!this.calls.includes(event)) {
|
||||||
this.log(`Received request from ${event.pubkey}`);
|
this.log(`Received call from ${event.pubkey}`);
|
||||||
this.calls.push(event);
|
this.calls.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +88,7 @@ class WebRtcRelaysService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async acceptCall(event: NostrEvent) {
|
async acceptCall(event: NostrEvent) {
|
||||||
this.log(`Accepting connection from ${event.pubkey}`);
|
this.log(`Approving calls from ${event.pubkey}`);
|
||||||
this.approved.push(event.pubkey);
|
this.approved.push(event.pubkey);
|
||||||
await this.handleCall(event);
|
await this.handleCall(event);
|
||||||
}
|
}
|
||||||
@@ -72,6 +98,9 @@ class WebRtcRelaysService {
|
|||||||
const peer = await this.broker.requestConnection(uri);
|
const peer = await this.broker.requestConnection(uri);
|
||||||
if (!peer.peer) return;
|
if (!peer.peer) return;
|
||||||
|
|
||||||
|
// add to the list of calls
|
||||||
|
if (peer.offerEvent) this.calls.push(peer.offerEvent);
|
||||||
|
|
||||||
if (this.upstream) {
|
if (this.upstream) {
|
||||||
const server = new WebRtcRelayServer(peer, this.upstream);
|
const server = new WebRtcRelayServer(peer, this.upstream);
|
||||||
this.servers.set(peer.peer, server);
|
this.servers.set(peer.peer, server);
|
||||||
|
@@ -71,7 +71,7 @@ export default function RelaysView() {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* <Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
as={RouterLink}
|
as={RouterLink}
|
||||||
to="/relays/webrtc"
|
to="/relays/webrtc"
|
||||||
@@ -79,7 +79,7 @@ export default function RelaysView() {
|
|||||||
colorScheme={location.pathname.startsWith("/relays/webrtc") ? "primary" : undefined}
|
colorScheme={location.pathname.startsWith("/relays/webrtc") ? "primary" : undefined}
|
||||||
>
|
>
|
||||||
WebRTC Relays
|
WebRTC Relays
|
||||||
</Button> */}
|
</Button>
|
||||||
{nip05?.exists && (
|
{nip05?.exists && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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 {
|
import {
|
||||||
|
Alert,
|
||||||
|
AlertIcon,
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Flex,
|
Flex,
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
Heading,
|
Heading,
|
||||||
Input,
|
Link,
|
||||||
|
Text,
|
||||||
useForceUpdate,
|
useForceUpdate,
|
||||||
useInterval,
|
useInterval,
|
||||||
} from "@chakra-ui/react";
|
} 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 BackButton from "../../../components/router/back-button";
|
||||||
import { QrCodeIcon } from "../../../components/icons";
|
|
||||||
import webRtcRelaysService from "../../../services/webrtc-relays";
|
import webRtcRelaysService from "../../../services/webrtc-relays";
|
||||||
import useSubject from "../../../hooks/use-subject";
|
import { QrCodeIcon } from "../../../components/icons";
|
||||||
import localSettings from "../../../services/local-settings";
|
import Connection from "./components/connection";
|
||||||
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";
|
|
||||||
|
|
||||||
function WebRtcRelaysPage() {
|
export default function WebRtcRelaysView() {
|
||||||
const update = useForceUpdate();
|
const update = useForceUpdate();
|
||||||
const identity = useSubject(localSettings.webRtcLocalIdentity);
|
useInterval(update, 1000);
|
||||||
const pubkey = useMemo(() => getPublicKey(identity), [identity]);
|
|
||||||
const npub = useMemo(() => nip19.npubEncode(pubkey), [pubkey]);
|
|
||||||
|
|
||||||
const uri = "webrtc+nostr:" + npub;
|
|
||||||
|
|
||||||
const [connectURI, setConnectURI] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
webRtcRelaysService.broker.on("call", update);
|
webRtcRelaysService.broker.on("call", update);
|
||||||
|
|
||||||
@@ -40,83 +29,50 @@ function WebRtcRelaysPage() {
|
|||||||
};
|
};
|
||||||
}, [update]);
|
}, [update]);
|
||||||
|
|
||||||
useInterval(update, 1000);
|
const unanswered = webRtcRelaysService.pendingIncoming.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap="2" direction="column" overflow="auto hidden" flex={1} px={{ base: "2", lg: 0 }}>
|
<Flex gap="2" direction="column" overflow="auto hidden" flex={1} px={{ base: "2", lg: 0 }}>
|
||||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
<BackButton hideFrom="lg" size="sm" />
|
<BackButton hideFrom="lg" size="sm" />
|
||||||
<Heading size="lg">WebRTC Relays</Heading>
|
<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>
|
</Flex>
|
||||||
|
|
||||||
<FormControl>
|
<Text fontStyle="italic" mt="-2">
|
||||||
<FormLabel>WebRTC Connection URI</FormLabel>
|
WebRTC Relays are temporary relays that can be accessed over{" "}
|
||||||
<Flex gap="2" alignItems="center">
|
<Link href="https://webrtc.org/" target="_blank" color="blue.500">
|
||||||
<UserAvatar pubkey={pubkey} size="sm" />
|
WebRTC
|
||||||
<Input readOnly userSelect="all" value={uri} />
|
</Link>
|
||||||
<CopyIconButton value={uri} aria-label="Copy Npub" />
|
</Text>
|
||||||
</Flex>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Heading size="md">Connect:</Heading>
|
<Heading size="md" mt="2">
|
||||||
<Flex gap="2">
|
Connections:
|
||||||
<Input placeholder="webrtc+nostr:npub1..." value={connectURI} onChange={(e) => setConnectURI(e.target.value)} />
|
</Heading>
|
||||||
<QRCodeScannerButton onData={(data) => setConnectURI(data)} />
|
{webRtcRelaysService.answered.length > 0 ? (
|
||||||
<Button
|
webRtcRelaysService.answered.map(({ call, peer, pubkey }) => (
|
||||||
colorScheme="primary"
|
<Connection
|
||||||
onClick={() => {
|
key={pubkey}
|
||||||
webRtcRelaysService.connect(connectURI);
|
peer={peer}
|
||||||
setConnectURI("");
|
call={call}
|
||||||
}}
|
client={webRtcRelaysService.clients.get(pubkey)!}
|
||||||
>
|
server={webRtcRelaysService.servers.get(pubkey)!}
|
||||||
Connect
|
/>
|
||||||
</Button>
|
))
|
||||||
</Flex>
|
) : (
|
||||||
|
<Alert status="info">
|
||||||
{webRtcRelaysService.answered.length > 0 && (
|
<AlertIcon />
|
||||||
<>
|
No connections yet, use the "Invite" or "Connect" buttons to connect to peer
|
||||||
<Heading size="md">Connections:</Heading>
|
</Alert>
|
||||||
{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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user