From 6b05527ea079e28e10473be445f4705f6ce4e1e2 Mon Sep 17 00:00:00 2001 From: weboko Date: Sat, 14 Jan 2023 02:06:31 +0100 Subject: [PATCH] create UI abstration and refactor code for better readability --- examples/noise-js/index.html | 15 +- examples/noise-js/index.js | 483 +++++++++++++++++++---------------- 2 files changed, 270 insertions(+), 228 deletions(-) diff --git a/examples/noise-js/index.html b/examples/noise-js/index.html index efec816..fea39fc 100644 --- a/examples/noise-js/index.html +++ b/examples/noise-js/index.html @@ -7,6 +7,19 @@ +

Waku Node Status: connecting...

@@ -42,7 +55,7 @@

Messages

-
+ diff --git a/examples/noise-js/index.js b/examples/noise-js/index.js index adee825..89a9c9a 100644 --- a/examples/noise-js/index.js +++ b/examples/noise-js/index.js @@ -11,32 +11,120 @@ import * as noise from "@waku/noise"; import protobuf from "protobufjs"; import QRCode from "qrcode"; -const messagesDiv = document.getElementById("messages"); -const nicknameInput = document.getElementById("nick-input"); -const textInput = document.getElementById("text-input"); -const sendButton = document.getElementById("send-btn"); -const sendingStatusSpan = document.getElementById("sending-status"); -const chatArea = document.getElementById("chat-area"); -const qrCanvas = document.getElementById("qr-canvas"); -const qrUrl = document.getElementById("qr-url"); -const wakuStatusSpan = document.getElementById("waku-status"); -const handshakeStatusSpan = document.getElementById("handshake-status"); -const qrUrlContainer = document.getElementById("qr-url-container"); -const copyURLButton = document.getElementById("copy-url"); -const openTabButton = document.getElementById("open-tab"); +// Protobuf +const ProtoChatMessage = new protobuf.Type("ChatMessage") + .add(new protobuf.Field("timestamp", 1, "uint64")) + .add(new protobuf.Field("nick", 2, "string")) + .add(new protobuf.Field("text", 3, "bytes")); -copyURLButton.onclick = () => { - const copyText = document.getElementById("qr-url"); - copyText.select(); - copyText.setSelectionRange(0, 99999); - navigator.clipboard.writeText(copyText.value); -}; +main(); -openTabButton.onclick = () => { - window.open(qrUrl.value, "_blank"); -}; +async function main() { + const ui = initUI(); + ui.waku.connecting(); -function getPairingInfofromUrl() { + // Starting the node + const node = await createLightNode({ + libp2p: { + peerDiscovery: [ + new PeerDiscoveryStaticPeers(getPredefinedBootstrapNodes(Fleet.Test)), + ], + }, + }); + + try { + await node.start(); + await waitForRemotePeer(node, [Protocols.Filter, Protocols.LightPush]); + + ui.waku.connected(); + + const [sender, responder] = getSenderAndResponder(node); + const myStaticKey = noise.generateX25519KeyPair(); + const urlPairingInfo = getPairingInfoFromURL(); + + if (urlPairingInfo) { + ui.shareInfo.hide(); + } + + const pairingObj = new noise.WakuPairing( + sender, + responder, + myStaticKey, + urlPairingInfo || new noise.ResponderParameters() + ); + const pExecute = pairingObj.execute(120000); // timeout after 2m + + scheduleHandshakeAuthConfirmation(pairingObj, ui); + + let encoder; + let decoder; + + try { + ui.handshake.waiting(); + + if (!urlPairingInfo) { + const pairingURL = buildPairingURLFromObj(pairingObj); + ui.shareInfo.setURL(pairingURL); + ui.shareInfo.renderQR(pairingURL); + } + + [encoder, decoder] = await pExecute; + + ui.handshake.connected(); + ui.shareInfo.hide(); + } catch (err) { + ui.handshake.error(err.message); + ui.hide(); + } + + /* + // The information needs to be backed up to decrypt messages sent with + // codecs generated with the handshake. The `handshakeResult` variable + // contains private information that needs to be stored safely + const contentTopic = pairingObj.contentTopic; + const handshakeResult = pairingObj.getHandshakeResult(); + + // To restore the codecs for decrypting older messages, or continuing an existing + // session, use this: + [encoder, decoder] = WakuPairing.getSecureCodec(contentTopic, handshakeResult); + */ + ui.message.display(); + + await node.filter.subscribe( + [decoder], + ui.message.onReceive.bind(ui.message) + ); + + ui.message.onSend(async (text, nick) => { + const timestamp = Math.floor(Date.now() / 1000); + const message = ProtoChatMessage.create({ + nick, + timestamp, + text: utils.utf8ToBytes(text), + }); + const payload = ProtoChatMessage.encode(message).finish(); + + await node.lightPush.push(encoder, { payload, timestamp }); + }); + } catch (err) { + ui.waku.error(err.message); + ui.hide(); + } +} + +function buildPairingURLFromObj(pairingObj) { + const pInfo = pairingObj.getPairingInfo(); + + // Data to encode in the QR code. The qrMessageNametag too to the QR string (separated by ) + const messageNameTagParam = `messageNameTag=${utils.bytesToHex( + pInfo.qrMessageNameTag + )}`; + const qrCodeParam = `qrCode=${encodeURIComponent(pInfo.qrCode)}`; + + return `${window.location.href}?${messageNameTagParam}&${qrCodeParam}`; +} + +function getPairingInfoFromURL() { const urlParams = new URLSearchParams(window.location.search); const messageNameTag = urlParams.get("messageNameTag"); @@ -111,220 +199,161 @@ function getSenderAndResponder(node) { return [sender, responder]; } -async function confirmAuthCodeFlow(pairingObj) { +async function scheduleHandshakeAuthConfirmation(pairingObj, ui) { const authCode = await pairingObj.getAuthCode(); - handshakeStatusSpan.innerHTML = "executing handshake"; + ui.handshake.connecting(); pairingObj.validateAuthCode(confirm("Confirm that authcode is: " + authCode)); } -async function hideQR() { - qrCanvas.remove(); - qrUrlContainer.remove(); -} +function initUI() { + const messagesList = document.getElementById("messages"); + const nicknameInput = document.getElementById("nick-input"); + const textInput = document.getElementById("text-input"); + const sendButton = document.getElementById("send-btn"); + const sendingStatusSpan = document.getElementById("sending-status"); + const chatArea = document.getElementById("chat-area"); + const wakuStatusSpan = document.getElementById("waku-status"); + const handshakeStatusSpan = document.getElementById("handshake-status"); -async function disableUI() { - hideQR(); - chatArea.remove(); -} + const qrCanvas = document.getElementById("qr-canvas"); + const qrUrlContainer = document.getElementById("qr-url-container"); + const qrUrl = document.getElementById("qr-url"); + const copyURLButton = document.getElementById("copy-url"); + const openTabButton = document.getElementById("open-tab"); -// Function to update the fields to guide the user by disabling buttons. -const updateFields = () => { - const readyToSend = nicknameInput.value !== ""; - textInput.disabled = !readyToSend; - sendButton.disabled = !readyToSend; -}; + copyURLButton.onclick = () => { + const copyText = document.getElementById("qr-url"); // need to get it each time otherwise copying does not work + copyText.select(); + copyText.setSelectionRange(0, 99999); + navigator.clipboard.writeText(copyText.value); + }; -// Protobuf -const ProtoChatMessage = new protobuf.Type("ChatMessage") - .add(new protobuf.Field("timestamp", 1, "uint64")) - .add(new protobuf.Field("nick", 2, "string")) - .add(new protobuf.Field("text", 3, "bytes")); + openTabButton.onclick = () => { + window.open(qrUrl.value, "_blank"); + }; -let messages = []; + const disableChatUIStateIfNeeded = () => { + const readyToSend = nicknameInput.value !== ""; + textInput.disabled = !readyToSend; + sendButton.disabled = !readyToSend; + }; + nicknameInput.onchange = disableChatUIStateIfNeeded; + nicknameInput.onblur = disableChatUIStateIfNeeded; -const updateMessages = () => { - messagesDiv.innerHTML = ""; -}; - -const onMessage = (wakuMessage) => { - const { timestamp, nick, text } = ProtoChatMessage.decode( - wakuMessage.payload - ); - const time = new Date(); - time.setTime(Number(timestamp) * 1000); - - messages.push( - `(${nick}) ${utils.bytesToUtf8( - text - )} [${time.toISOString()}]` - ); - updateMessages(); -}; - -async function main() { - // Starting the node - const node = await createLightNode({ - libp2p: { - peerDiscovery: [ - new PeerDiscoveryStaticPeers(getPredefinedBootstrapNodes(Fleet.Test)), - ], + return { + shareInfo: { + setURL(url) { + qrUrl.value = url; + }, + hide() { + qrCanvas.remove(); + qrUrlContainer.remove(); + }, + renderQR(url) { + QRCode.toCanvas(qrCanvas, url, (err) => { + if (err) { + throw err; + } + }); + }, }, - }); + waku: { + _val(msg) { + wakuStatusSpan.innerText = msg; + }, + _class(name) { + wakuStatusSpan.className = name; + }, + connecting() { + this._val("connecting..."); + this._class("progress"); + }, + connected() { + this._val("connected"); + this._class("success"); + }, + error(msg) { + this._val(msg); + this._class("error"); + }, + }, + handshake: { + _val(val) { + handshakeStatusSpan.innerText = val; + }, + _class(name) { + handshakeStatusSpan.className = name; + }, + error(msg) { + this._val(msg); + this._class("error"); + }, + waiting() { + this._val("waiting for handshake..."); + this._class("progress"); + }, + generating() { + this._val("generating QR code..."); + this._class("progress"); + }, + connecting() { + this._val("executing handshake..."); + this._class("progress"); + }, + connected() { + this._val("handshake completed!"); + this._class("success"); + }, + }, + message: { + _render({ time, text, nick }) { + messagesList.innerHTML += ` +
  • + (${nick}) + ${text} + [${new Date(time).toISOString()}] +
  • + `; + }, + _status(text, className) { + sendingStatusSpan.innerText = text; + sendingStatusSpan.className = className; + }, + onReceive({ payload }) { + const { timestamp, nick, text } = ProtoChatMessage.decode(payload); - try { - await node.start(); + this._render({ + nick, + time: timestamp * 1000, + text: utils.bytesToUtf8(text), + }); + }, + onSend(cb) { + sendButton.addEventListener("click", async () => { + try { + this._status("sending...", "progress"); + await cb(textInput.value, nicknameInput.value); + this._status("sent", "success"); - await waitForRemotePeer(node, [Protocols.Filter, Protocols.LightPush]); - - wakuStatusSpan.innerHTML = "connected"; - - const [sender, responder] = getSenderAndResponder(node); - - const myStaticKey = noise.generateX25519KeyPair(); - - const pairingParameters = getPairingInfofromUrl(); - - const initiator = pairingParameters ? true : false; - - let encoder; - let decoder; - - if (initiator) { - console.log("Initiator"); - hideQR(); // Initiator does not require a QR code - - const pairingObj = new noise.WakuPairing( - sender, - responder, - myStaticKey, - pairingParameters - ); - const pExecute = pairingObj.execute(120000); // timeout after 2m - - confirmAuthCodeFlow(pairingObj); - - try { - handshakeStatusSpan.innerHTML = "waiting for handshake..."; - - [encoder, decoder] = await pExecute; - - handshakeStatusSpan.innerHTML = "handshake completed!"; - } catch (err) { - handshakeStatusSpan.innerHTML = err.message; - disableUI(); - console.error(err); - } - - /* - // The information needs to be backed up to decrypt messages sent with - // codecs generated with the handshake. The `handshakeResult` variable - // contains private information that needs to be stored safely - const contentTopic = pairingObj.contentTopic; - const handshakeResult = pairingObj.getHandshakeResult(); - - // To restore the codecs for decrypting older messages, or continuing an existing - // session, use this: - [encoder, decoder] = WakuPairing.getSecureCodec(contentTopic, handshakeResult); - */ - } else { - console.log("Responder"); - - const pairingObj = new noise.WakuPairing( - sender, - responder, - myStaticKey, - new noise.ResponderParameters() - ); - const pExecute = pairingObj.execute(120000); // timeout after 2m - - confirmAuthCodeFlow(pairingObj); - - const pInfo = pairingObj.getPairingInfo(); - - // Data to encode in the QR code. The qrMessageNametag too to the QR string (separated by ) - const messageNameTagParam = `messageNameTag=${utils.bytesToHex( - pInfo.qrMessageNameTag - )}`; - const qrCodeParam = `qrCode=${encodeURIComponent(pInfo.qrCode)}`; - const qrURLString = `${window.location.href}?${messageNameTagParam}&${qrCodeParam}`; - - handshakeStatusSpan.innerHTML = "generating QR code..."; - - console.log("Generating QR..."); - - QRCode.toCanvas(qrCanvas, qrURLString, (err) => { - if (err) { - handshakeStatusSpan.innerHTML = err.message; - disableUI(); - console.error(err); - } else { - handshakeStatusSpan.innerHTML = "waiting for handshake..."; - qrUrl.value = qrURLString; - } - }); - - try { - handshakeStatusSpan.innerHTML = "waiting for handshake..."; - - [encoder, decoder] = await pExecute; - - handshakeStatusSpan.innerHTML = "handshake completed!"; - - hideQR(); - } catch (err) { - handshakeStatusSpan.innerHTML = err.message; - disableUI(); - console.error(err); - } - - /* - // The information needs to be backed up to decrypt messages sent with - // codecs generated with the handshake. The `handshakeResult` variable - // contains private information that needs to be stored safely - const contentTopic = pairingObj.contentTopic; - const handshakeResult = pairingObj.getHandshakeResult(); - - // To restore the codecs for decrypting older messages, or continuing an existing - // session, use this: - [encoder, decoder] = WakuPairing.getSecureCodec(contentTopic, handshakeResult); - */ - } - - nicknameInput.onchange = updateFields; - nicknameInput.onblur = updateFields; - - sendButton.onclick = async () => { - const text = utils.utf8ToBytes(textInput.value); - const timestamp = new Date(); - const msg = ProtoChatMessage.create({ - text, - nick: nicknameInput.value, - timestamp: Math.floor(timestamp.valueOf() / 1000), - }); - const payload = ProtoChatMessage.encode(msg).finish(); - sendingStatusSpan.innerText = "sending..."; - await node.lightPush.push(encoder, { payload, timestamp }); - sendingStatusSpan.innerText = "sent!"; - - onMessage({ payload }); - - textInput.value = null; - setTimeout(() => { - sendingStatusSpan.innerText = ""; - }, 5000); - }; - - await node.filter.subscribe([decoder], onMessage); - - chatArea.style.display = "block"; - } catch (err) { - wakuStatusSpan.innerHTML = err.message; - disableUI(); - return; - } + this._render({ + time: Date.now(), // a bit different from what receiver will see but for the matter of example is good enough + text: textInput.value, + nick: nicknameInput.value, + }); + textInput.value = ""; + } catch (e) { + this._status(`error: ${e.message}`, "error"); + } + }); + }, + display() { + chatArea.style.display = "block"; + this._status("waiting for input", "progress"); + }, + }, + hide() { + this.shareInfo.hide(); + chatArea.remove(); + }, + }; } -main();