diff --git a/light-chat/index.html b/light-chat/index.html new file mode 100644 index 0000000..90b8037 --- /dev/null +++ b/light-chat/index.html @@ -0,0 +1,50 @@ + + + + + + JS-Waku light chat + + + + +
+
+

Status:

+ +
+ Peer's information + +

Content topic

+

+ +

Local Peer Id

+

+ +

Remote Peer Id

+

+ +

Remote peer's multiaddr

+

+
+
+ +
+ + +
+ + + + + diff --git a/light-chat/index.js b/light-chat/index.js new file mode 100644 index 0000000..c4c40d5 --- /dev/null +++ b/light-chat/index.js @@ -0,0 +1,164 @@ +import * as utils from "https://unpkg.com/@waku/byte-utils@0.0.2/bundle/index.js"; +import * as wakuCreate from "https://unpkg.com/@waku/create@0.0.4/bundle/index.js"; +import { waitForRemotePeer } from "https://unpkg.com/@waku/core@0.0.6/bundle/lib/wait_for_remote_peer.js"; +import * as wakuMessage from "https://unpkg.com/@waku/core@0.0.6/bundle/lib/waku_message/version_0.js"; + +const MULTI_ADDR = "/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/443/wss/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm"; +const CONTENT_TOPIC = "/toy-chat/2/huilong/proto"; +const PROTOCOLS = ["filter", "lightpush"]; + +const ui = initUI(); +runApp(ui).catch((err) => { + console.error(err); + ui.setStatus(`error: ${err.message}`, "error"); +}); + +async function runApp(ui) { + ui.setStatus("connecting...", "progress"); + + const { info, sendMessage, unsubscribeFromMessages } = await initWakuContext({ + protocols: PROTOCOLS, + multiAddr: MULTI_ADDR, + contentTopic: CONTENT_TOPIC, + onMessageReceived: ui.renderMessage, + }); + + ui.setStatus("connected", "success"); + + ui.setLocalPeer(info.localPeerId); + ui.setRemotePeer(info.remotePeerIds); + ui.setRemoteMultiAddr(info.multiAddr); + ui.setContentTopic(info.contentTopic); + + ui.onSendMessage(sendMessage); + + ui.onExit(async () => { + ui.setStatus("disconnecting...", "progress"); + await unsubscribeFromMessages(); + ui.setStatus("disconnected", "terminated"); + ui.resetMessages(); + }); +} + +async function initWakuContext({ + multiAddr, + protocols, + contentTopic, + onMessageReceived, +}) { + const Decoder = new wakuMessage.DecoderV0(contentTopic); + const Encoder = new wakuMessage.EncoderV0(contentTopic); + + const ChatMessage = 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")); + + const node = await wakuCreate.createLightNode({ defaultBootstrap: true }); + + await node.start(); + await waitForRemotePeer(node, protocols); + + // Set a filter by using Decoder for a given ContentTopic + const unsubscribeFromMessages = await node.filter.subscribe([Decoder], (wakuMessage) => { + const messageObj = ChatMessage.decode(wakuMessage.payload); + onMessageReceived({ + ...messageObj, + text: utils.bytesToUtf8(messageObj.text), + }); + }); + + const localPeerId = node.libp2p.peerId.toString(); + + const remotePeers = await node.libp2p.peerStore.all(); + const remotePeerIds = remotePeers.map(peer => peer.id.toString()); + + return { + unsubscribeFromMessages, + info: { + multiAddr, + contentTopic, + localPeerId, + remotePeerIds, + }, + sendMessage: async ({ text, nick }) => { + if (!text || !nick) { + return; + } + + const protoMessage = ChatMessage.create({ + nick, + timestamp: Date.now(), + text: utils.utf8ToBytes(text), + }); + + await node.lightPush.push(Encoder, { + payload: ChatMessage.encode(protoMessage).finish(), + }); + } + }; +} + +// UI adapter +function initUI() { + const exitButton = document.getElementById("exit"); + const sendButton = document.getElementById("send"); + + const statusBlock = document.getElementById("status"); + const localPeerBlock = document.getElementById("localPeerId"); + const remotePeerId = document.getElementById("remotePeerId"); + const remoteMultiAddr = document.getElementById("remoteMultiAddr"); + const contentTopicBlock = document.getElementById("contentTopic"); + + const messagesBlock = document.getElementById("messages"); + + const nickText = document.getElementById("nickText"); + const messageText = document.getElementById("messageText"); + + return { + // UI events + onExit: (cb) => { + exitButton.addEventListener("click", cb); + }, + onSendMessage: (cb) => { + sendButton.addEventListener("click", async () => { + await cb({ + nick: nickText.value, + text: messageText.value, + }); + messageText.value = ""; + }); + }, + // UI renderers + setStatus: (value, className) => { + statusBlock.innerHTML = `${value}`; + }, + setLocalPeer: (id) => { + localPeerBlock.innerText = id.toString(); + }, + setRemotePeer: (ids) => { + remotePeerId.innerText = ids.join("\n"); + }, + setRemoteMultiAddr: (multiAddr) => { + remoteMultiAddr.innerText = multiAddr.toString(); + }, + setContentTopic: (topic) => { + contentTopicBlock.innerText = topic.toString(); + }, + renderMessage: (messageObj) => { + const { nick, text, timestamp } = messageObj; + const date = new Date(timestamp); + + // WARNING: XSS vulnerable + messagesBlock.innerHTML += ` +
+

${nick} (${date.toDateString()}):

+

${text}

+
+ `; + }, + resetMessages: () => { + messagesBlock.innerHTML = ""; + }, + }; +} diff --git a/light-chat/package.json b/light-chat/package.json new file mode 100644 index 0000000..63237e7 --- /dev/null +++ b/light-chat/package.json @@ -0,0 +1,14 @@ +{ + "name": "light-chat", + "version": "0.1.0", + "private": true, + "homepage": "/light-chat", + "devDependencies": { + "serve": "^14.1.2", + "gh-pages": "^4.0.0" + }, + "scripts": { + "start": "serve .", + "deploy": "gh-pages -d ." + } +} diff --git a/light-chat/style.css b/light-chat/style.css new file mode 100644 index 0000000..ff54ed1 --- /dev/null +++ b/light-chat/style.css @@ -0,0 +1,153 @@ +* { + margin: 0; + padding: 0; + word-wrap: break-word; + box-sizing: border-box; +} + +html, body { + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; +} + +html { + font-size: 16px; + overflow: hidden; +} + +body { + display: flex; + align-items: center; + padding: 10px; + justify-content: center; +} + +details { + margin-bottom: 15px; +} + +details p { + margin-bottom: 10px; +} + +summary { + cursor: pointer; + max-width: 100%; + margin-bottom: 5px; +} + +span { + font-weight: 300; +} + +input, textarea { + line-height: 1rem; + padding: 5px; +} + +textarea { + min-height: 3rem; +} + +h3 { + margin-bottom: 5px; +} + +.content { + width: 800px; + min-width: 300px; + max-width: 800px; + height: 100%; + display: flex; + flex-direction: column; + align-content: space-between; +} + +#messages { + overflow-y: scroll; + overflow-x: hidden; +} + +.message + .message { + margin-top: 15px; +} + +.message :first-child { + font-weight: bold; +} + +.message p + p { + margin-top: 5px; +} + +.message span { + font-size: 0.8rem; +} + +.inputArea { + display: flex; + gap: 10px; + flex-direction: column; + margin-top: 20px; +} + +.controls { + margin-top: 10px; + display: flex; + gap: 10px; +} + +.controls button { + flex-grow: 1; + cursor: pointer; + padding: 10px; +} + +#send { + background-color: #32d1a0; + border: none; + color: white; +} +#send:hover { + background-color: #3abd96; +} +#send:active { + background-color: #3ba183; +} + +#exit { + color: white; + border: none; + background-color: #ff3a31; +} +#exit:hover { + background-color: #e4423a; +} +#exit:active { + background-color: #c84740; +} + +.success { + color: #3ba183; +} + +.progress { + color: #9ea13b; +} + +.terminated { + color: black; +} + +.error { + color: #c84740; +} + +.footer { + display: flex; + width: 100%; + flex-direction: column; + align-self: flex-end; +}