init with default examples
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "next/core-web-vitals",
|
||||
"overrides": [],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest"
|
||||
},
|
||||
"rules": {}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
* @waku-org/js-waku-developers
|
|
@ -0,0 +1,15 @@
|
|||
name: Add new issues to Waku project board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.3.0
|
||||
with:
|
||||
project-url: https://github.com/orgs/waku-org/projects/2
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
|
@ -0,0 +1,45 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
examples_build_and_test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
example:
|
||||
[
|
||||
relay-angular-chat,
|
||||
relay-reactjs-chat,
|
||||
web-chat,
|
||||
noise-js,
|
||||
noise-rtc,
|
||||
relay-direct-rtc
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install NodeJS
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "*/package-lock.json"
|
||||
|
||||
- name: install
|
||||
run: npm install
|
||||
working-directory: "examples/${{ matrix.example }}"
|
||||
|
||||
- name: build
|
||||
run: npm run build
|
||||
working-directory: "examples/${{ matrix.example }}"
|
||||
|
||||
- name: test
|
||||
run: npm run test --if-present
|
||||
working-directory: "examples/${{ matrix.example }}"
|
|
@ -0,0 +1,5 @@
|
|||
dist
|
||||
build
|
||||
node_modules
|
||||
yarn.lock
|
||||
.DS_Store
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Status Research & Development GmbH
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2021 Status Research & Development GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,71 @@
|
|||
# Waku Lab
|
||||
|
||||
## Description
|
||||
|
||||
This repository is dedicated for proof of concepts, examples and research done around core Waku libraries.
|
||||
|
||||
See https://examples.waku.org/ for more examples.
|
||||
|
||||
### Web Chat App
|
||||
|
||||
- [code](examples/web-chat)
|
||||
- [website](https://lab.waku.org/web-chat)
|
||||
- Demonstrates: Group chat, React/TypeScript, Relay, Store.
|
||||
|
||||
### Waku Light Client in JavaScript
|
||||
|
||||
Send messages between several users (or just one) using light client targetted protocols.
|
||||
|
||||
- [code](examples/light-js)
|
||||
- [website](https://lab.waku.org/light-js)
|
||||
- Demonstrates: Waku Light node: Filter + Light Push, Pure Javascript/HTML using ESM/unpkg bundle.
|
||||
|
||||
### Minimal Angular (v13) Waku Relay
|
||||
|
||||
A barebone messaging app to illustrate the seamless integration of `js-waku` into AngularJS.
|
||||
|
||||
- [code](examples/relay-angular-chat)
|
||||
- [website](https://lab.waku.org/relay-angular-chat)
|
||||
- Demonstrates: Group messaging, Angular, Waku Relay, Protobuf using `protobufjs`, No async/await syntax.
|
||||
|
||||
### Waku Relay in JavaScript
|
||||
|
||||
This example uses Waku Relay to send and receive simple text messages.
|
||||
|
||||
- [code](examples/relay-js)
|
||||
- [website](https://lab.waku.org/relay-js)
|
||||
- Demonstrates: Waku Relay, Pure Javascript/HTML using ESM/unpkg bundle.
|
||||
|
||||
### Waku Relay in ReactJS
|
||||
|
||||
A barebone chat app to illustrate the seamless integration of `js-waku` into ReactJS.
|
||||
|
||||
- [code](examples/relay-reactjs-chat)
|
||||
- [website](https://lab.waku.org/relay-reactjs-chat)
|
||||
- Demonstrates: Group chat, React/JavaScript, Waku Relay, Protobuf using `protobufjs`.
|
||||
|
||||
### Noise JS
|
||||
|
||||
- [code](examples/noise-js)
|
||||
- [website](https://lab.waku.org/noise-js)
|
||||
- Demonstrates: LightPush, Filter, [Noise encryption](https://rfc.vac.dev/spec/35/).
|
||||
|
||||
### Noise RTC
|
||||
|
||||
- [code](examples/noise-rtc)
|
||||
- [website](https://lab.waku.org/noise-rtc)
|
||||
- Demonstrates: LightPush, Filter, [Noise encryption](https://rfc.vac.dev/spec/35/), WebRTC.
|
||||
|
||||
### Relay Direct RTC
|
||||
|
||||
- [code](examples/relay-direct-rtc)
|
||||
- [website](https://lab.waku.org/relay-direct-rtc)
|
||||
- Demonstrates: Relay over WebRTC.
|
||||
|
||||
|
||||
# Continuous Integration
|
||||
|
||||
The `master` branch is being built by Jenkins CI:
|
||||
https://ci.infra.status.im/job/website/job/lab.waku.org/
|
||||
|
||||
Based on the [`ci/Jenkinsfile`](./ci/Jenkinsfile).
|
|
@ -0,0 +1,11 @@
|
|||
FROM node:16-alpine3.16
|
||||
|
||||
LABEL maintainer="jakub@status.im"
|
||||
|
||||
RUN apk --no-cache add openssh git
|
||||
|
||||
# Jenkins user needs a specific UID/GID to work
|
||||
RUN addgroup -g 1001 jenkins \
|
||||
&& adduser -u 1001 -G jenkins -D jenkins
|
||||
USER jenkins
|
||||
ENV HOME="/home/jenkins"
|
|
@ -0,0 +1,83 @@
|
|||
pipeline {
|
||||
agent {
|
||||
dockerfile {
|
||||
label 'linux'
|
||||
dir 'ci'
|
||||
}
|
||||
}
|
||||
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
/* manage how many builds we keep */
|
||||
buildDiscarder(logRotator(
|
||||
numToKeepStr: '20',
|
||||
daysToKeepStr: '30',
|
||||
))
|
||||
}
|
||||
|
||||
environment {
|
||||
SITE_DOMAIN = 'examples.waku.org'
|
||||
GIT_AUTHOR_NAME = 'status-im-auto'
|
||||
GIT_AUTHOR_EMAIL = 'auto@status.im'
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=no'
|
||||
PUPPETEER_SKIP_DOWNLOAD = 'true'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Pre') {
|
||||
steps {
|
||||
sh 'npm install --silent'
|
||||
/* TODO: Build the main page. */
|
||||
sh 'mkdir -p build/docs'
|
||||
sh 'cp index.html build/docs/'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Examples') {
|
||||
parallel {
|
||||
stage('relay-angular-chat') { steps { script { buildExample() } } }
|
||||
stage('relay-reactjs-chat') { steps { script { buildExample() } } }
|
||||
stage('web-chat') { steps { script { buildExample() } } }
|
||||
stage('noise-js') { steps { script { buildExample() } } }
|
||||
stage('noise-rtc') { steps { script { buildExample() } } }
|
||||
stage('relay-direct-rtc') { steps { script { buildExample() } } }
|
||||
}
|
||||
}
|
||||
|
||||
stage('HTML Examples') {
|
||||
parallel {
|
||||
stage('relay-js') { steps { script { copyExample() } } }
|
||||
stage('light-js') { steps { script { copyExample() } } }
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish') {
|
||||
steps { script {
|
||||
sh "echo ${SITE_DOMAIN} > build/docs/CNAME"
|
||||
sshagent(credentials: ['status-im-auto-ssh']) {
|
||||
sh 'node ci/deploy.js'
|
||||
}
|
||||
} }
|
||||
}
|
||||
}
|
||||
post {
|
||||
always { cleanWs() }
|
||||
}
|
||||
}
|
||||
|
||||
def buildExample(example=STAGE_NAME) {
|
||||
def dest = "${WORKSPACE}/build/docs/${example}"
|
||||
dir("examples/${example}") {
|
||||
sh 'npm install --silent'
|
||||
sh 'npm run build'
|
||||
sh "mkdir -p ${dest}"
|
||||
sh "cp -r build/. ${dest}"
|
||||
}
|
||||
}
|
||||
|
||||
def copyExample(example=STAGE_NAME) {
|
||||
def source = "examples/${example}"
|
||||
def dest = "build/docs/${example}"
|
||||
sh "mkdir -p ${dest}"
|
||||
sh "cp -r ${source}/. ${dest}"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
# Description
|
||||
|
||||
Configuration of CI builds executed under a Jenkins instance at https://ci.status.im/.
|
||||
|
||||
# Website
|
||||
|
||||
The `Jenkinsfile.gh-pages` file builds the documentation site with this job:
|
||||
https://ci.infra.status.im/job/website/job/lab.waku.org/
|
||||
|
||||
And deploys it via `gh-pages` branch and [GitHub Pages](https://pages.github.com/) to:
|
||||
https://lab.waku.org/
|
|
@ -0,0 +1,43 @@
|
|||
const { promisify } = require("util");
|
||||
const { publish } = require("gh-pages");
|
||||
const ghpublish = promisify(publish);
|
||||
|
||||
/* fix for "Unhandled promise rejections" */
|
||||
process.on("unhandledRejection", (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
const Args = process.argv.slice(2);
|
||||
const USE_HTTPS = Args[0] && Args[0].toUpperCase() === "HTTPS";
|
||||
|
||||
const branch = "gh-pages";
|
||||
const org = "waku-org";
|
||||
const repo = "waku-lab";
|
||||
/* use SSH auth by default */
|
||||
let repoUrl = USE_HTTPS
|
||||
? `https://github.com/${org}/${repo}.git`
|
||||
: `git@github.com:${org}/${repo}.git`;
|
||||
|
||||
/* alternative auth using GitHub user and API token */
|
||||
if (process.env.GH_USER != undefined) {
|
||||
repoUrl =
|
||||
"https://" +
|
||||
process.env.GH_USER +
|
||||
":" +
|
||||
process.env.GH_TOKEN +
|
||||
"@" +
|
||||
`github.com/${org}/${repo}.git`;
|
||||
}
|
||||
|
||||
const main = async (url, branch) => {
|
||||
console.log(`Pushing to: ${url}`);
|
||||
console.log(`On branch: ${branch}`);
|
||||
await ghpublish("build/docs", {
|
||||
repo: url,
|
||||
branch: branch,
|
||||
dotfiles: true,
|
||||
silent: false,
|
||||
});
|
||||
};
|
||||
|
||||
main(repoUrl, branch);
|
|
@ -0,0 +1,13 @@
|
|||
# Using Waku Light Push and Filter in JavaScript
|
||||
|
||||
**Demonstrates**:
|
||||
|
||||
- Waku Light node: Waku Filter + Waku Light Push
|
||||
- Pure Javascript/HTML.
|
||||
- Use minified bundle of js from unpkg.com, no import, no package manager.
|
||||
|
||||
This example uses Waku Filter to listen to messages and Waku Light Push to send messages.
|
||||
|
||||
To test the example, simply download the `index.html` file from this folder and open it in a browser.
|
||||
|
||||
The `master` branch's HEAD is deployed at https://examples.waku.org/light-js/.
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,280 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>JS-Waku light node example</title>
|
||||
<link rel="apple-touch-icon" href="./favicon.png" />
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h2>Status</h2>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
|
||||
<div>
|
||||
<h2>Local Peer Id</h2>
|
||||
</div>
|
||||
<div id="peer-id"></div>
|
||||
|
||||
<div>
|
||||
<h2>Select Peer</h2>
|
||||
</div>
|
||||
<div>
|
||||
<button id="getPeersButton" type="button">Refresh Peers</button>
|
||||
<select id="peer-select">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="remote-multiaddr">Remote peer's multiaddr</label>
|
||||
</div>
|
||||
<div style="margin-right: 1em">
|
||||
<input
|
||||
id="remote-multiaddr"
|
||||
type="text"
|
||||
value=""
|
||||
style="width: 100%; max-width: 900px"
|
||||
/>
|
||||
</div>
|
||||
<button disabled id="dial" type="button">Dial</button>
|
||||
|
||||
<div>
|
||||
<h2>Remote Peers</h2>
|
||||
</div>
|
||||
<div id="remote-peer-id"></div>
|
||||
<br />
|
||||
<button disabled id="subscribe" type="button">Subscribe with Filter</button>
|
||||
<button disabled id="unsubscribe" type="button">
|
||||
Unsubscribe with Filter
|
||||
</button>
|
||||
<br />
|
||||
<label for="textInput">Message text</label>
|
||||
<input id="textInput" placeholder="Type your message here" type="text" />
|
||||
<button disabled id="sendButton" type="button">
|
||||
Send message using Light Push
|
||||
</button>
|
||||
<br />
|
||||
<div id="messages"></div>
|
||||
|
||||
<div>
|
||||
<h2>Store</h2>
|
||||
</div>
|
||||
<button disabled id="queryStoreButton" type="button">
|
||||
Retrieve past messages
|
||||
</button>
|
||||
|
||||
<script src="https://unpkg.com/@multiformats/multiaddr@12.1.1/dist/index.min.js"></script>
|
||||
<script type="module">
|
||||
import {
|
||||
createLightNode,
|
||||
waitForRemotePeer,
|
||||
createEncoder,
|
||||
createDecoder,
|
||||
utf8ToBytes,
|
||||
bytesToUtf8,
|
||||
} from "https://unpkg.com/@waku/sdk@0.0.20/bundle/index.js";
|
||||
import {
|
||||
enrTree,
|
||||
DnsNodeDiscovery,
|
||||
} from "https://unpkg.com/@waku/dns-discovery@0.0.16/bundle/index.js";
|
||||
import { messageHash } from "https://unpkg.com/@waku/message-hash@0.1.8/bundle/index.js";
|
||||
|
||||
const peerIdDiv = document.getElementById("peer-id");
|
||||
const remotePeerIdDiv = document.getElementById("remote-peer-id");
|
||||
const statusDiv = document.getElementById("status");
|
||||
const remoteMultiAddrDiv = document.getElementById("remote-multiaddr");
|
||||
const dialButton = document.getElementById("dial");
|
||||
const subscribeButton = document.getElementById("subscribe");
|
||||
const unsubscribeButton = document.getElementById("unsubscribe");
|
||||
const queryStoreButton = document.getElementById("queryStoreButton");
|
||||
const messagesDiv = document.getElementById("messages");
|
||||
const textInput = document.getElementById("textInput");
|
||||
const sendButton = document.getElementById("sendButton");
|
||||
const getPeersButton = document.getElementById("getPeersButton");
|
||||
const peersSelector = document.getElementById("peer-select");
|
||||
|
||||
const ContentTopic = "/js-waku-examples/1/chat/utf8";
|
||||
const decoder = createDecoder(ContentTopic);
|
||||
const encoder = createEncoder({ contentTopic: ContentTopic });
|
||||
// Each key is a unique identifier for the message. Each value is an obj { text, timestamp }
|
||||
let messages = {};
|
||||
let unsubscribe;
|
||||
|
||||
const updateMessages = (msgs, div) => {
|
||||
div.innerHTML = "<ul>";
|
||||
Object.values(msgs)
|
||||
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
|
||||
.forEach(
|
||||
(msg) =>
|
||||
(div.innerHTML +=
|
||||
"<li>" + `${msg.text} - ${msg.timestamp}` + "</li>")
|
||||
);
|
||||
div.innerHTML += "</ul>";
|
||||
};
|
||||
|
||||
try {
|
||||
await getPeers();
|
||||
} catch (e) {
|
||||
console.log("Failed to find a peer", e);
|
||||
remoteMultiAddrDiv.value =
|
||||
"/dns4/node-01.ac-cn-hongkong-c.wakuv2.test.statusim.net/tcp/8000/wss/p2p/16Uiu2HAkvWiyFsgRhuJEb9JfjYxEkoHLgnUQmr1N5mKWnYjxYRVm";
|
||||
}
|
||||
|
||||
statusDiv.innerHTML = "<p>Creating Waku node.</p>";
|
||||
const node = await createLightNode();
|
||||
|
||||
statusDiv.innerHTML = "<p>Starting Waku node.</p>";
|
||||
await node.start();
|
||||
|
||||
window.waku = node;
|
||||
console.info(
|
||||
"Use window.waku to access the waku node running in the browser directly through the console."
|
||||
);
|
||||
|
||||
// Queries all peers from libp2p peer store and updates list of connected peers
|
||||
const updatePeersList = async () => {
|
||||
// Generate <p> element with connection string from each peer
|
||||
const peers = await node.libp2p.peerStore.all();
|
||||
const peerIdElements = peers.map((peer) => {
|
||||
const element = document.createElement("p");
|
||||
element.textContent = `${peer.addresses[1].multiaddr}/p2p/${peer.id}`;
|
||||
return element;
|
||||
});
|
||||
// Update elements displaying list of peers
|
||||
remotePeerIdDiv.replaceChildren(...peerIdElements);
|
||||
};
|
||||
|
||||
// Refreshes list of connected peers each time a new one is detected
|
||||
node.store.addLibp2pEventListener("peer:connect", async (event) => {
|
||||
const peerId = event.detail;
|
||||
console.log(`Peer connected with peer id: ${peerId}`);
|
||||
// Wait half a second after receiving event for peer to show up in peer store
|
||||
setTimeout(async () => {
|
||||
updatePeersList();
|
||||
}, 500);
|
||||
|
||||
// Update status
|
||||
statusDiv.innerHTML = `<p>Peer dialed: ${peerId}</p>`;
|
||||
// Enable send and subscribe inputs as we are now connected to a peer
|
||||
textInput.disabled = false;
|
||||
sendButton.disabled = false;
|
||||
subscribeButton.disabled = false;
|
||||
queryStoreButton.disabled = false;
|
||||
});
|
||||
|
||||
statusDiv.innerHTML = "<p>Waku node started.</p>";
|
||||
peerIdDiv.innerHTML = "<p>" + node.libp2p.peerId.toString() + "</p>";
|
||||
dialButton.disabled = false;
|
||||
|
||||
dialButton.onclick = async () => {
|
||||
const ma = remoteMultiAddrDiv.value;
|
||||
if (!ma) {
|
||||
statusDiv.innerHTML = "<p>Error: No multiaddr provided.</p>";
|
||||
return;
|
||||
}
|
||||
statusDiv.innerHTML = "<p>Dialing peer.</p>";
|
||||
let multiaddr;
|
||||
try {
|
||||
multiaddr = MultiformatsMultiaddr.multiaddr(ma);
|
||||
} catch (err) {
|
||||
statusDiv.innerHTML = "<p>Error: invalid multiaddr provided</p>";
|
||||
throw err;
|
||||
}
|
||||
await node.dial(multiaddr, ["filter", "lightpush", "store"]);
|
||||
};
|
||||
|
||||
const messageReceivedCallback = (wakuMessage) => {
|
||||
// create a unique key for the message
|
||||
const msgHash =
|
||||
bytesToUtf8(messageHash(ContentTopic, wakuMessage)) +
|
||||
wakuMessage.proto.timestamp;
|
||||
const text = bytesToUtf8(wakuMessage.payload);
|
||||
// store message by its key
|
||||
messages[msgHash + wakuMessage.proto.timestamp] = {
|
||||
text,
|
||||
timestamp: wakuMessage.timestamp,
|
||||
};
|
||||
// call function to refresh display of messages
|
||||
updateMessages(messages, messagesDiv);
|
||||
};
|
||||
|
||||
subscribeButton.onclick = async () => {
|
||||
unsubscribe = await node.filter.subscribe(
|
||||
[decoder],
|
||||
messageReceivedCallback
|
||||
);
|
||||
unsubscribeButton.disabled = false;
|
||||
subscribeButton.disabled = true;
|
||||
};
|
||||
|
||||
queryStoreButton.onclick = async () => {
|
||||
await node.store.queryWithOrderedCallback(
|
||||
[decoder],
|
||||
messageReceivedCallback
|
||||
);
|
||||
};
|
||||
|
||||
unsubscribeButton.onclick = async () => {
|
||||
await unsubscribe();
|
||||
unsubscribe = undefined;
|
||||
unsubscribeButton.disabled = true;
|
||||
subscribeButton.disabled = false;
|
||||
};
|
||||
|
||||
sendButton.onclick = async () => {
|
||||
const text = textInput.value;
|
||||
|
||||
await node.lightPush.send(encoder, {
|
||||
payload: utf8ToBytes(text),
|
||||
});
|
||||
console.log("Message sent!");
|
||||
textInput.value = null;
|
||||
};
|
||||
|
||||
getPeersButton.onclick = async () => {
|
||||
await getPeers(statusDiv, remoteMultiAddrDiv);
|
||||
};
|
||||
|
||||
peersSelector.addEventListener("change", function (event) {
|
||||
remoteMultiAddrDiv.value = event.target.value;
|
||||
});
|
||||
|
||||
async function getPeers() {
|
||||
// Display status
|
||||
statusDiv.innerHTML = "<p>Discovering peers</p>";
|
||||
|
||||
// Clear all options in select element
|
||||
peersSelector.innerHTML = "";
|
||||
|
||||
// Get peers using DNS discovery
|
||||
const defaultNodeCount = 5;
|
||||
const dnsDiscovery = await DnsNodeDiscovery.dnsOverHttp();
|
||||
const peers = await dnsDiscovery.getPeers([enrTree["TEST"]], {
|
||||
relay: defaultNodeCount,
|
||||
store: defaultNodeCount,
|
||||
filter: defaultNodeCount,
|
||||
lightPush: defaultNodeCount,
|
||||
});
|
||||
|
||||
// Create an option element for each peer's multiaddr and append to select element
|
||||
const optionElements = peers.map((peer) => {
|
||||
const optionElement = document.createElement("option");
|
||||
optionElement.value = `${peer.multiaddrs[1]}/p2p/${peer.peerId}`;
|
||||
optionElement.text = `${peer.multiaddrs[1]}/p2p/${peer.peerId}`;
|
||||
return optionElement;
|
||||
});
|
||||
peersSelector.append(...optionElements);
|
||||
|
||||
// Set first peer as selected
|
||||
peersSelector.options[0].selected = true;
|
||||
remoteMultiAddrDiv.value = peersSelector.options[0].value;
|
||||
|
||||
statusDiv.innerHTML = "<p>Peers discovered</p>";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Light JS",
|
||||
"description": "Send messages between several users (or just one) using light client targeted protocols.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# Waku Noise Pairing Example App
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
Browse to http://localhost:8080
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,200 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>Waku Noise</title>
|
||||
<link rel="apple-touch-icon" href="./favicon.png" />
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
<style>
|
||||
* {
|
||||
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;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h3:last-of-type {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 span,
|
||||
h3 span {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.progress {
|
||||
color: #9ea13b;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #3ba183;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #c84740;
|
||||
}
|
||||
|
||||
button.progress {
|
||||
color: white;
|
||||
background-color: #9ea13b;
|
||||
}
|
||||
|
||||
button.success {
|
||||
color: white;
|
||||
background-color: #3ba183;
|
||||
}
|
||||
|
||||
button.error {
|
||||
color: white;
|
||||
background-color: #c84740;
|
||||
}
|
||||
|
||||
.pairingInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pairingInfo input {
|
||||
display: block;
|
||||
min-width: 250px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5rem;
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pairingInfo button {
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.pairingInfo button + button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.chatArea {
|
||||
}
|
||||
|
||||
.chatArea ul {
|
||||
margin-bottom: 30px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.chatArea ul li + li {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chatArea div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chatArea div > * {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5rem;
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="status">
|
||||
<h3>
|
||||
<b>Waku Node Status:</b> <span id="waku-status">connecting...</span>
|
||||
</h3>
|
||||
<h3 id="handshake-span">
|
||||
<b>Handshake Status:</b>
|
||||
<span id="handshake-status" class="progress">waiting for waku</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="pairingInfo" id="qr-url-container" style="display: none">
|
||||
<h2>Pairing information</h2>
|
||||
<input
|
||||
type="text"
|
||||
id="qr-url"
|
||||
readonly
|
||||
placeholder="generating URL..."
|
||||
/>
|
||||
<div>
|
||||
<button id="copy-url" style="width: 100px">Copy URL</button>
|
||||
<button id="open-tab" style="width: 150px">Open in new tab</button>
|
||||
</div>
|
||||
<canvas id="qr-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="chatArea" id="chat-area" style="display: none">
|
||||
<h2>Chat</h2>
|
||||
<ul id="messages"></ul>
|
||||
|
||||
<div>
|
||||
<input id="nick-input" placeholder="Choose a nickname" type="text" />
|
||||
<textarea
|
||||
id="text-input"
|
||||
placeholder="Type your message here"
|
||||
type="text"
|
||||
></textarea>
|
||||
<button id="send-btn" type="button" disabled>Send message</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
NOTICE: you can try also try to pair your browser with a
|
||||
<a href="https://github.com/waku-org/go-waku/tree/master/examples/noise"
|
||||
>go-waku based node</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,274 @@
|
|||
import { createLightNode, waitForRemotePeer } from "@waku/sdk";
|
||||
import * as utils from "@waku/utils/bytes";
|
||||
import * as noise from "@waku/noise";
|
||||
import protobuf from "protobufjs";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
// 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"));
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
const ui = initUI();
|
||||
ui.waku.connecting();
|
||||
|
||||
// Starting the node
|
||||
const node = await createLightNode({
|
||||
defaultBootstrap: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await node.start();
|
||||
await waitForRemotePeer(node, ["filter", "lightpush"]);
|
||||
|
||||
ui.waku.connected();
|
||||
|
||||
const myStaticKey = noise.generateX25519KeyPair();
|
||||
const urlPairingInfo = getPairingInfoFromURL();
|
||||
|
||||
const pairingObj = new noise.WakuPairing(
|
||||
node.lightPush,
|
||||
node.filter,
|
||||
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);
|
||||
ui.shareInfo.show();
|
||||
}
|
||||
|
||||
[encoder, decoder] = await pExecute;
|
||||
|
||||
ui.handshake.connected();
|
||||
ui.shareInfo.hide();
|
||||
} catch (err) {
|
||||
ui.handshake.error(err.message);
|
||||
ui.hide();
|
||||
}
|
||||
|
||||
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.send(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");
|
||||
const qrCodeString = urlParams.get("qrCode");
|
||||
|
||||
if (!(messageNameTag && qrCodeString)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new noise.InitiatorParameters(
|
||||
decodeURIComponent(qrCodeString),
|
||||
utils.hexToBytes(messageNameTag)
|
||||
);
|
||||
}
|
||||
|
||||
async function scheduleHandshakeAuthConfirmation(pairingObj, ui) {
|
||||
const authCode = await pairingObj.getAuthCode();
|
||||
ui.handshake.connecting();
|
||||
pairingObj.validateAuthCode(confirm("Confirm that authcode is: " + authCode));
|
||||
}
|
||||
|
||||
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 chatArea = document.getElementById("chat-area");
|
||||
const wakuStatusSpan = document.getElementById("waku-status");
|
||||
const handshakeStatusSpan = document.getElementById("handshake-status");
|
||||
|
||||
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");
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
openTabButton.onclick = () => {
|
||||
window.open(qrUrl.value, "_blank");
|
||||
};
|
||||
|
||||
const disableChatUIStateIfNeeded = () => {
|
||||
const readyToSend = nicknameInput.value !== "";
|
||||
textInput.disabled = !readyToSend;
|
||||
sendButton.disabled = !readyToSend;
|
||||
};
|
||||
nicknameInput.onchange = disableChatUIStateIfNeeded;
|
||||
nicknameInput.onblur = disableChatUIStateIfNeeded;
|
||||
|
||||
return {
|
||||
shareInfo: {
|
||||
setURL(url) {
|
||||
qrUrl.value = url;
|
||||
},
|
||||
hide() {
|
||||
qrUrlContainer.style.display = "none";
|
||||
},
|
||||
show() {
|
||||
qrUrlContainer.style.display = "flex";
|
||||
},
|
||||
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 += `
|
||||
<li>
|
||||
(${nick})
|
||||
<strong>${text}</strong>
|
||||
<i>[${new Date(time).toISOString()}]</i>
|
||||
</li>
|
||||
`;
|
||||
},
|
||||
_status(text, className) {
|
||||
sendButton.className = className;
|
||||
},
|
||||
onReceive({ payload }) {
|
||||
const { timestamp, nick, text } = ProtoChatMessage.decode(payload);
|
||||
|
||||
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");
|
||||
|
||||
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.style.display = "none";
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Waku Noise",
|
||||
"description": "Example showing Waku noise capabilities.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@waku/noise-example",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"start": "webpack-dev-server"
|
||||
},
|
||||
"dependencies": {
|
||||
"@waku/sdk": "0.0.18",
|
||||
"@waku/noise": "0.0.3-31510da",
|
||||
"@waku/utils": "0.0.10",
|
||||
"protobufjs": "^7.1.2",
|
||||
"qrcode": "^1.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./index.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "build"),
|
||||
filename: "index.js",
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
mode: "development",
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: ["index.html", "favicon.ico", "favicon.png", "manifest.json"],
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
State of this example: **work in progress**
|
||||
|
||||
What's done:
|
||||
- By using `js-noise` users can establish secure communication channel.
|
||||
- This channel is used to exchange `offer/answer` to initiate direct WebRTC connection.
|
||||
|
||||
What should be done:
|
||||
- `STUN` server: in order not to loose benefits of peer-to-peer protocols used underneath we should find a way to be able to retrieve coordinates of a user to build `offer/answer` by not involving one `STUN` server for it;
|
||||
- `TURN` server: similarly to prev point we should be able to cover a need to create WebRTC connection for users who are behind secure `NAT` and are not able to communicated just as it is.
|
||||
|
||||
Additional reading that explains why `STUN/TURN` is not easily removable from the equation: https://github.com/libp2p/specs/pull/497/files#diff-2cb0b0dcc282bd123b21f5a0610e8a01b516fc453b95c655cf7e16734f2f7b11R48-R53
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,198 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>Waku NoiseRTC</title>
|
||||
<link rel="apple-touch-icon" href="./favicon.png" />
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
<style>
|
||||
* {
|
||||
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;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: space-between;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h3:last-of-type {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 span,
|
||||
h3 span {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.progress {
|
||||
color: #9ea13b;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #3ba183;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #c84740;
|
||||
}
|
||||
|
||||
button.progress {
|
||||
color: white;
|
||||
background-color: #9ea13b;
|
||||
}
|
||||
|
||||
button.success {
|
||||
color: white;
|
||||
background-color: #3ba183;
|
||||
}
|
||||
|
||||
button.error {
|
||||
color: white;
|
||||
background-color: #c84740;
|
||||
}
|
||||
|
||||
.pairingInfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pairingInfo input {
|
||||
display: block;
|
||||
min-width: 250px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5rem;
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pairingInfo button {
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.pairingInfo button + button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.chatArea {
|
||||
}
|
||||
|
||||
.chatArea ul {
|
||||
margin-bottom: 30px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.chatArea ul li + li {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chatArea div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chatArea div > * {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5rem;
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="status">
|
||||
<h3>
|
||||
<b>Waku Node Status:</b>
|
||||
<span id="waku-status" class="progress">connecting...</span>
|
||||
</h3>
|
||||
<h3 id="handshake-span">
|
||||
<b>Handshake Status:</b>
|
||||
<span id="handshake-status" class="progress">waiting for waku</span>
|
||||
</h3>
|
||||
<h3>
|
||||
<b>RTC Status:</b>
|
||||
<span id="rtc-status" class="progress"
|
||||
>waiting for noise to be established</span
|
||||
>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="pairingInfo" id="qr-url-container" style="display: none">
|
||||
<h2>Pairing information</h2>
|
||||
<input
|
||||
type="text"
|
||||
id="qr-url"
|
||||
readonly
|
||||
placeholder="generating URL..."
|
||||
/>
|
||||
<div>
|
||||
<button id="copy-url" style="width: 100px">Copy URL</button>
|
||||
<button id="open-tab" style="width: 100px">Open in new</button>
|
||||
</div>
|
||||
<canvas id="qr-canvas"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="chatArea" id="chat-area" style="display: none">
|
||||
<h2>Chat</h2>
|
||||
<ul id="messages"></ul>
|
||||
|
||||
<div>
|
||||
<input id="nick-input" placeholder="Choose a nickname" type="text" />
|
||||
<textarea
|
||||
id="text-input"
|
||||
placeholder="Type your message here"
|
||||
type="text"
|
||||
></textarea>
|
||||
<button id="send-btn" type="button" disabled>Send message</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,464 @@
|
|||
import { createLightNode, waitForRemotePeer } from "@waku/sdk";
|
||||
import * as utils from "@waku/utils/bytes";
|
||||
import * as noise from "@waku/noise";
|
||||
import protobuf from "protobufjs";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
// Protobuf
|
||||
const ProtoMessage = new protobuf.Type("Message").add(
|
||||
new protobuf.Field("data", 3, "string")
|
||||
);
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
const ui = initUI();
|
||||
ui.waku.connecting();
|
||||
|
||||
// Starting the node
|
||||
const node = await createLightNode({ defaultBootstrap: true });
|
||||
|
||||
try {
|
||||
await node.start();
|
||||
await waitForRemotePeer(node, ["filter", "lightpush"]);
|
||||
|
||||
ui.waku.connected();
|
||||
|
||||
const myStaticKey = noise.generateX25519KeyPair();
|
||||
const urlPairingInfo = getPairingInfoFromURL();
|
||||
|
||||
const pairingObj = new noise.WakuPairing(
|
||||
node.lightPush,
|
||||
node.filter,
|
||||
myStaticKey,
|
||||
urlPairingInfo || new noise.ResponderParameters()
|
||||
);
|
||||
const pExecute = pairingObj.execute(120000); // timeout after 2m
|
||||
|
||||
scheduleHandshakeAuthConfirmation(pairingObj, ui);
|
||||
|
||||
let sendWakuMessage;
|
||||
let listenToWakuMessages;
|
||||
|
||||
try {
|
||||
ui.handshake.waiting();
|
||||
|
||||
if (!urlPairingInfo) {
|
||||
const pairingURL = buildPairingURLFromObj(pairingObj);
|
||||
ui.shareInfo.setURL(pairingURL);
|
||||
ui.shareInfo.renderQR(pairingURL);
|
||||
ui.shareInfo.show();
|
||||
}
|
||||
|
||||
[sendWakuMessage, listenToWakuMessages] = await buildWakuMessage(
|
||||
node,
|
||||
pExecute
|
||||
);
|
||||
|
||||
ui.handshake.connected();
|
||||
ui.shareInfo.hide();
|
||||
} catch (err) {
|
||||
ui.handshake.error(err.message);
|
||||
ui.hide();
|
||||
}
|
||||
|
||||
ui.message.display();
|
||||
|
||||
const { peerConnection, sendMessage: sendRTCMessage } = initRTC({
|
||||
ui,
|
||||
onReceive: ui.message.onReceive.bind(ui.message),
|
||||
});
|
||||
|
||||
peerConnection.onicecandidate = async (event) => {
|
||||
if (event.candidate) {
|
||||
console.log("candidate sent");
|
||||
try {
|
||||
ui.rtc.sendingCandidate();
|
||||
await sendWakuMessage({
|
||||
type: "candidate",
|
||||
candidate: event.candidate,
|
||||
});
|
||||
} catch (error) {
|
||||
ui.rtc.error(error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sendOffer = async () => {
|
||||
console.log("offer sent");
|
||||
ui.rtc.sendingOffer();
|
||||
|
||||
try {
|
||||
const offer = await peerConnection.createOffer();
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
|
||||
await sendWakuMessage({
|
||||
type: "offer",
|
||||
offer,
|
||||
});
|
||||
} catch (error) {
|
||||
ui.rtc.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const sendAnswer = async (data) => {
|
||||
console.log("answer sent");
|
||||
ui.rtc.sendingAnswer();
|
||||
try {
|
||||
await peerConnection.setRemoteDescription(
|
||||
new RTCSessionDescription(data.offer)
|
||||
);
|
||||
|
||||
const answer = await peerConnection.createAnswer();
|
||||
peerConnection.setLocalDescription(answer);
|
||||
|
||||
await sendWakuMessage({
|
||||
type: "answer",
|
||||
answer,
|
||||
});
|
||||
} catch (error) {
|
||||
ui.rtc.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const receiveAnswer = async (data) => {
|
||||
try {
|
||||
console.log("answer received");
|
||||
await peerConnection.setRemoteDescription(
|
||||
new RTCSessionDescription(data.answer)
|
||||
);
|
||||
console.log("answer saved");
|
||||
|
||||
await sendWakuMessage({
|
||||
type: "ready",
|
||||
text: "received answer",
|
||||
});
|
||||
} catch (error) {
|
||||
ui.rtc.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const receiveCandidate = async (data) => {
|
||||
try {
|
||||
console.log("candidate saved");
|
||||
await peerConnection.addIceCandidate(
|
||||
new RTCIceCandidate(data.candidate)
|
||||
);
|
||||
} catch (error) {
|
||||
ui.rtc.error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWakuMessages = async (data) => {
|
||||
if (data.type === "offer") {
|
||||
await sendAnswer(data);
|
||||
}
|
||||
|
||||
if (data.type === "answer") {
|
||||
await receiveAnswer(data);
|
||||
}
|
||||
|
||||
if (data.type === "ready") {
|
||||
console.log("RTC: partner is", data.text);
|
||||
}
|
||||
|
||||
if (data.type === "candidate") {
|
||||
await receiveCandidate(data);
|
||||
}
|
||||
};
|
||||
|
||||
await listenToWakuMessages(handleWakuMessages);
|
||||
ui.message.onSend(sendRTCMessage);
|
||||
|
||||
// if we are initiator of Noise handshake
|
||||
// let's initiate Web RTC as well
|
||||
if (!urlPairingInfo) {
|
||||
await sendOffer();
|
||||
}
|
||||
} 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");
|
||||
const qrCodeString = urlParams.get("qrCode");
|
||||
|
||||
if (!(messageNameTag && qrCodeString)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new noise.InitiatorParameters(
|
||||
decodeURIComponent(qrCodeString),
|
||||
utils.hexToBytes(messageNameTag)
|
||||
);
|
||||
}
|
||||
|
||||
async function scheduleHandshakeAuthConfirmation(pairingObj, ui) {
|
||||
const authCode = await pairingObj.getAuthCode();
|
||||
ui.handshake.connecting();
|
||||
pairingObj.validateAuthCode(confirm("Confirm that authcode is: " + authCode));
|
||||
}
|
||||
|
||||
async function buildWakuMessage(node, noiseExecute) {
|
||||
const [encoder, decoder] = await noiseExecute;
|
||||
|
||||
const sendMessage = async (message) => {
|
||||
let payload = ProtoMessage.create({
|
||||
data: JSON.stringify(message),
|
||||
});
|
||||
payload = ProtoMessage.encode(payload).finish();
|
||||
|
||||
return node.lightPush.send(encoder, { payload });
|
||||
};
|
||||
|
||||
const listenToMessages = async (fn) => {
|
||||
return node.filter.subscribe([decoder], ({ payload }) => {
|
||||
const { data } = ProtoMessage.decode(payload);
|
||||
fn(JSON.parse(data));
|
||||
});
|
||||
};
|
||||
|
||||
return [sendMessage, listenToMessages];
|
||||
}
|
||||
|
||||
function initRTC({ ui, onReceive }) {
|
||||
const configuration = {
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
};
|
||||
const peerConnection = new RTCPeerConnection(configuration);
|
||||
const sendChannel = peerConnection.createDataChannel("chat");
|
||||
|
||||
let receiveChannel;
|
||||
|
||||
sendChannel.onopen = (event) => {
|
||||
ui.rtc.ready();
|
||||
console.log("onopen send", event);
|
||||
};
|
||||
|
||||
peerConnection.ondatachannel = (event) => {
|
||||
receiveChannel = event.channel;
|
||||
|
||||
receiveChannel.onmessage = (event) => {
|
||||
onReceive(JSON.parse(event.data));
|
||||
};
|
||||
|
||||
receiveChannel.onopen = (event) => {
|
||||
ui.rtc.ready();
|
||||
console.log("onopen receive", event);
|
||||
};
|
||||
};
|
||||
|
||||
const sendMessage = (text, nick) => {
|
||||
sendChannel.send(JSON.stringify({ text, nick, timestamp: Date.now() }));
|
||||
};
|
||||
|
||||
return {
|
||||
peerConnection,
|
||||
sendChannel,
|
||||
receiveChannel,
|
||||
sendMessage,
|
||||
};
|
||||
}
|
||||
|
||||
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 chatArea = document.getElementById("chat-area");
|
||||
const wakuStatusSpan = document.getElementById("waku-status");
|
||||
const handshakeStatusSpan = document.getElementById("handshake-status");
|
||||
|
||||
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");
|
||||
|
||||
const rtcStatus = document.getElementById("rtc-status");
|
||||
const connectChat = document.getElementById("connect-chat-btn");
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
openTabButton.onclick = () => {
|
||||
window.open(qrUrl.value, "_blank");
|
||||
};
|
||||
|
||||
const disableChatUIStateIfNeeded = () => {
|
||||
const readyToSend = nicknameInput.value !== "";
|
||||
textInput.disabled = !readyToSend;
|
||||
sendButton.disabled = !readyToSend;
|
||||
};
|
||||
nicknameInput.onchange = disableChatUIStateIfNeeded;
|
||||
nicknameInput.onblur = disableChatUIStateIfNeeded;
|
||||
|
||||
return {
|
||||
shareInfo: {
|
||||
setURL(url) {
|
||||
qrUrl.value = url;
|
||||
},
|
||||
hide() {
|
||||
qrUrlContainer.style.display = "none";
|
||||
},
|
||||
show() {
|
||||
qrUrlContainer.style.display = "flex";
|
||||
},
|
||||
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 += `
|
||||
<li>
|
||||
(${nick})
|
||||
<strong>${text}</strong>
|
||||
<i>[${new Date(time).toISOString()}]</i>
|
||||
</li>
|
||||
`;
|
||||
},
|
||||
_status(text, className) {
|
||||
sendButton.className = className;
|
||||
},
|
||||
onReceive(data) {
|
||||
const { timestamp, nick, text } = data;
|
||||
|
||||
this._render({
|
||||
nick,
|
||||
time: timestamp,
|
||||
text,
|
||||
});
|
||||
},
|
||||
onSend(cb) {
|
||||
sendButton.addEventListener("click", async () => {
|
||||
try {
|
||||
this._status("sending...", "progress");
|
||||
await cb(textInput.value, nicknameInput.value);
|
||||
this._status("sent", "success");
|
||||
|
||||
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");
|
||||
},
|
||||
},
|
||||
rtc: {
|
||||
_val(msg) {
|
||||
rtcStatus.innerText = msg;
|
||||
},
|
||||
_class(name) {
|
||||
rtcStatus.className = name;
|
||||
},
|
||||
sendingOffer() {
|
||||
this._val("sending offer");
|
||||
this._class("progress");
|
||||
},
|
||||
sendingAnswer() {
|
||||
this._val("sending answer");
|
||||
this._class("progress");
|
||||
},
|
||||
sendingCandidate() {
|
||||
this._val("sending ice candidate");
|
||||
this._class("progress");
|
||||
},
|
||||
ready() {
|
||||
this._val("ready");
|
||||
this._class("success");
|
||||
},
|
||||
error(msg) {
|
||||
this._val(msg);
|
||||
this._class("error");
|
||||
},
|
||||
onConnect(cb) {
|
||||
connectChat.addEventListener("click", cb);
|
||||
},
|
||||
},
|
||||
hide() {
|
||||
this.shareInfo.hide();
|
||||
chatArea.style.display = "none";
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Waku NoiseRTC",
|
||||
"description": "Example showing WebRTC with Waku Noise.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@waku/noise-rtc",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"start": "webpack-dev-server"
|
||||
},
|
||||
"dependencies": {
|
||||
"@waku/sdk": "0.0.18",
|
||||
"@waku/noise": "0.0.3-31510da",
|
||||
"@waku/utils": "0.0.10",
|
||||
"protobufjs": "^7.1.2",
|
||||
"qrcode": "^1.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./index.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "build"),
|
||||
filename: "index.js",
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
mode: "development",
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: ["index.html", "favicon.ico", "favicon.png", "manifest.json"],
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
/.angular/cache
|
|
@ -0,0 +1,26 @@
|
|||
# Minimal Angular (v13) Waku Relay App
|
||||
|
||||
**Demonstrates**:
|
||||
|
||||
- Group messaging
|
||||
- Angular
|
||||
- Waku Relay
|
||||
- Protobuf using `protobufjs`
|
||||
- No async/await syntax
|
||||
|
||||
A barebone messaging app to illustrate the seamless integration of `js-waku` into AngularJS.
|
||||
|
||||
The `master` branch's HEAD is deployed at https://examples.waku.org/relay-angular-chat/.
|
||||
|
||||
To run a development version locally, do:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/waku-org/js-waku-examples
|
||||
cd js-waku-examples/examples/relay-angular-chat
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
### Known issues
|
||||
|
||||
There is a problem when using `npm` to install/run the Angular app.
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"relay-angular-chat": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "build",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "relay-angular-chat:build:production"
|
||||
},
|
||||
"development": {
|
||||
"browserTarget": "relay-angular-chat:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "relay-angular-chat:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.css"],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "relay-angular-chat",
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: "",
|
||||
frameworks: ["jasmine", "@angular-devkit/build-angular"],
|
||||
plugins: [
|
||||
require("karma-jasmine"),
|
||||
require("karma-chrome-launcher"),
|
||||
require("karma-jasmine-html-reporter"),
|
||||
require("karma-coverage"),
|
||||
require("@angular-devkit/build-angular/plugins/karma"),
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true, // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require("path").join(__dirname, "./coverage/relay-angular-chat"),
|
||||
subdir: ".",
|
||||
reporters: [{ type: "html" }, { type: "text-summary" }],
|
||||
},
|
||||
reporters: ["progress", "kjhtml"],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ["Chrome"],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true,
|
||||
});
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"name": "@waku/relay-angular-chat",
|
||||
"version": "0.1.0",
|
||||
"homepage": "/relay-angular-chat",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build --base-href /relay-angular-chat --deploy-url /relay-angular-chat/",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "exit 0",
|
||||
"test:local": "ng test",
|
||||
"test:ci": "ng test --watch=false --browsers=ChromeHeadless"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~14.2.11",
|
||||
"@angular/common": "~14.2.11",
|
||||
"@angular/compiler": "~14.2.11",
|
||||
"@angular/core": "~14.2.11",
|
||||
"@angular/forms": "~14.2.11",
|
||||
"@angular/platform-browser": "~14.2.11",
|
||||
"@angular/platform-browser-dynamic": "~14.2.11",
|
||||
"@angular/router": "~14.2.11",
|
||||
"@waku/core": "^0.0.6",
|
||||
"@waku/create": "^0.0.4",
|
||||
"@waku/interfaces": "^0.0.5",
|
||||
"protobufjs": "^7.1.2",
|
||||
"rxjs": "~7.5.7",
|
||||
"tslib": "^2.4.1",
|
||||
"zone.js": "~0.11.8"
|
||||
},
|
||||
"browser": {
|
||||
"util": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~14.2.10",
|
||||
"@angular/cli": "~14.2.10",
|
||||
"@angular/compiler-cli": "~14.2.11",
|
||||
"@types/jasmine": "~4.3.0",
|
||||
"@types/node": "^17.0.45",
|
||||
"is-ci-cli": "^2.2.0",
|
||||
"jasmine-core": "~4.3.0",
|
||||
"karma": "~6.4.1",
|
||||
"karma-chrome-launcher": "~3.1.1",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.0.0",
|
||||
"typescript": "~4.7.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
declare module "protons";
|
|
@ -0,0 +1,13 @@
|
|||
declare module "time-cache" {
|
||||
interface ITimeCache {
|
||||
put(key: string, value: any, validity: number): void;
|
||||
get(key: string): any;
|
||||
has(key: string): boolean;
|
||||
}
|
||||
|
||||
type TimeCache = ITimeCache;
|
||||
|
||||
function TimeCache(options: object): TimeCache;
|
||||
|
||||
export = TimeCache;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/* Application-wide Styles */
|
||||
h1 {
|
||||
color: #369;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 250%;
|
||||
}
|
||||
h2,
|
||||
h3 {
|
||||
color: #444;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-weight: lighter;
|
||||
}
|
||||
body {
|
||||
margin: 2em;
|
||||
}
|
||||
body,
|
||||
input[type="text"],
|
||||
button {
|
||||
color: #333;
|
||||
font-family: Cambria, Georgia, serif;
|
||||
}
|
||||
/* everywhere else */
|
||||
* {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<h1>{{ title }}</h1>
|
||||
<p>Waku node's status: {{ wakuStatus }}</p>
|
||||
<app-messages></app-messages>
|
|
@ -0,0 +1,32 @@
|
|||
import { TestBed } from "@angular/core/testing";
|
||||
import { AppComponent } from "./app.component";
|
||||
import { MessagesComponent } from "./messages/messages.component";
|
||||
|
||||
describe("AppComponent", () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AppComponent, MessagesComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
xit("should create the app", () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
xit(`should have as title 'relay-angular-chat'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual("relay-angular-chat");
|
||||
});
|
||||
|
||||
xit("should render title", () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector(".h1")?.textContent).toContain(
|
||||
"relay-angular-chat"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import { Component } from "@angular/core";
|
||||
import { WakuService } from "./waku.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
templateUrl: "./app.component.html",
|
||||
styleUrls: ["./app.component.css"],
|
||||
})
|
||||
export class AppComponent {
|
||||
title: string = "relay-angular-chat";
|
||||
wakuStatus!: string;
|
||||
|
||||
constructor(private wakuService: WakuService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.wakuService.init();
|
||||
this.wakuService.wakuStatus.subscribe((wakuStatus) => {
|
||||
this.wakuStatus = wakuStatus;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
import { BrowserModule } from "@angular/platform-browser";
|
||||
import { AppComponent } from "./app.component";
|
||||
import { MessagesComponent } from "./messages/messages.component";
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, MessagesComponent],
|
||||
imports: [BrowserModule],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
|
@ -0,0 +1,9 @@
|
|||
<button (click)="sendMessage()" [disabled]="wakuStatus !== 'Connected'">
|
||||
Send Message
|
||||
</button>
|
||||
<h2>Messages</h2>
|
||||
<ul class="messages">
|
||||
<li *ngFor="let message of messages">
|
||||
<span>{{ message.timestamp }} {{ message.text }}</span>
|
||||
</li>
|
||||
</ul>
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { MessagesComponent } from "./messages.component";
|
||||
|
||||
describe("MessagesComponent", () => {
|
||||
let component: MessagesComponent;
|
||||
let fixture: ComponentFixture<MessagesComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [MessagesComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MessagesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
xit("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,84 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
import { WakuService } from "../waku.service";
|
||||
import type { WakuPrivacy } from "@waku/interfaces";
|
||||
import protobuf from "protobufjs";
|
||||
import { DecoderV0, EncoderV0 } from "@waku/core/lib/waku_message/version_0";
|
||||
import type { MessageV0 } from "@waku/core/lib/waku_message/version_0";
|
||||
|
||||
const ProtoChatMessage = new protobuf.Type("ChatMessage")
|
||||
.add(new protobuf.Field("timestamp", 1, "uint32"))
|
||||
.add(new protobuf.Field("text", 2, "string"));
|
||||
|
||||
interface MessageInterface {
|
||||
timestamp: Date;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-messages",
|
||||
templateUrl: "./messages.component.html",
|
||||
styleUrls: ["./messages.component.css"],
|
||||
})
|
||||
export class MessagesComponent implements OnInit {
|
||||
contentTopic: string = `/js-waku-examples/1/chat/proto`;
|
||||
decoder: DecoderV0;
|
||||
encoder: EncoderV0;
|
||||
messages: MessageInterface[] = [];
|
||||
messageCount: number = 0;
|
||||
waku!: WakuPrivacy;
|
||||
wakuStatus!: string;
|
||||
deleteObserver?: () => void;
|
||||
|
||||
constructor(private wakuService: WakuService) {
|
||||
this.decoder = new DecoderV0(this.contentTopic);
|
||||
this.encoder = new EncoderV0(this.contentTopic);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.wakuService.wakuStatus.subscribe((wakuStatus) => {
|
||||
this.wakuStatus = wakuStatus;
|
||||
});
|
||||
|
||||
this.wakuService.waku.subscribe((waku) => {
|
||||
this.waku = waku;
|
||||
this.deleteObserver = this.waku.relay.addObserver(
|
||||
this.decoder,
|
||||
this.processIncomingMessages
|
||||
);
|
||||
});
|
||||
|
||||
window.onbeforeunload = () => this.ngOnDestroy();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.deleteObserver) this.deleteObserver();
|
||||
}
|
||||
|
||||
sendMessage(): void {
|
||||
const time = new Date().getTime();
|
||||
|
||||
const protoMsg = ProtoChatMessage.create({
|
||||
timestamp: time,
|
||||
text: `Here is a message #${this.messageCount}`,
|
||||
});
|
||||
|
||||
const payload = ProtoChatMessage.encode(protoMsg).finish();
|
||||
this.waku.relay.send(this.encoder, { payload }).then(() => {
|
||||
console.log(`Message #${this.messageCount} sent`);
|
||||
this.messageCount += 1;
|
||||
});
|
||||
}
|
||||
|
||||
processIncomingMessages = (wakuMessage: MessageV0) => {
|
||||
if (!wakuMessage.payload) return;
|
||||
|
||||
const { text, timestamp } = ProtoChatMessage.decode(
|
||||
wakuMessage.payload
|
||||
) as unknown as { text: string; timestamp: bigint };
|
||||
const time = new Date();
|
||||
time.setTime(Number(timestamp));
|
||||
const message = { text, timestamp: time };
|
||||
|
||||
this.messages.push(message);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { WakuService } from "./waku.service";
|
||||
|
||||
describe("WakuService", () => {
|
||||
let service: WakuService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(WakuService);
|
||||
});
|
||||
|
||||
it("should be created", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import { Injectable } from "@angular/core";
|
||||
import { BehaviorSubject, Subject } from "rxjs";
|
||||
import { createPrivacyNode } from "@waku/create";
|
||||
import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer";
|
||||
import type { WakuPrivacy } from "@waku/interfaces";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class WakuService {
|
||||
private wakuSubject = new Subject<WakuPrivacy>();
|
||||
public waku = this.wakuSubject.asObservable();
|
||||
|
||||
private wakuStatusSubject = new BehaviorSubject("");
|
||||
public wakuStatus = this.wakuStatusSubject.asObservable();
|
||||
|
||||
constructor() {}
|
||||
|
||||
init() {
|
||||
createPrivacyNode({ defaultBootstrap: true }).then((waku) => {
|
||||
waku.start().then(() => {
|
||||
this.wakuSubject.next(waku);
|
||||
this.wakuStatusSubject.next("Connecting...");
|
||||
|
||||
waitForRemotePeer(waku).then(() => {
|
||||
this.wakuStatusSubject.next("Connected");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const environment = {
|
||||
production: true,
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
// This file can be replaced during build by using the `fileReplacements` array.
|
||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
|
||||
// The list of file replacements can be found in `angular.json`.
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
};
|
||||
|
||||
/*
|
||||
* For easier debugging in development mode, you can import the following file
|
||||
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
|
||||
*
|
||||
* This import should be commented out in production mode because it will have a negative impact
|
||||
* on performance if an error is thrown.
|
||||
*/
|
||||
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>RelayAngularChat</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="apple-touch-icon" href="./favicon.png" />
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
import { enableProdMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
|
||||
import { AppModule } from "./app/app.module";
|
||||
import { environment } from "./environments/environment";
|
||||
|
||||
import "zone.js";
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch((err) => console.error(err));
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Relay Chat",
|
||||
"description": "A barebone messaging app to illustrate the Angular Relay guide.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/* You can add global styles to this file, and also import other style files */
|
|
@ -0,0 +1,30 @@
|
|||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||
|
||||
import "zone.js/testing";
|
||||
import { getTestBed } from "@angular/core/testing";
|
||||
import {
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting,
|
||||
} from "@angular/platform-browser-dynamic/testing";
|
||||
|
||||
declare const require: {
|
||||
context(
|
||||
path: string,
|
||||
deep?: boolean,
|
||||
filter?: RegExp
|
||||
): {
|
||||
<T>(id: string): T;
|
||||
keys(): string[];
|
||||
};
|
||||
};
|
||||
|
||||
// First, initialize the Angular testing environment.
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserDynamicTestingModule,
|
||||
platformBrowserDynamicTesting()
|
||||
);
|
||||
|
||||
// Then we find all the tests.
|
||||
const context = require.context("./", true, /\.spec\.ts$/);
|
||||
// And load the modules.
|
||||
context.keys().map(context);
|
|
@ -0,0 +1,10 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"sourceMap": true,
|
||||
"declaration": false,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"target": "es2020",
|
||||
"module": "es2020",
|
||||
"lib": ["es2020", "dom"],
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": ["jasmine"]
|
||||
},
|
||||
"files": ["src/test.ts"],
|
||||
"include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# Direct WebRTC connection for Waku Relay
|
||||
|
||||
**Demonstrates**:
|
||||
|
||||
- Waku Relay node with direct WebRTC connection
|
||||
- Pure Javascript/HTML.
|
||||
|
||||
This example uses WebRTC transport and Waku Relay to exchange messages.
|
||||
|
||||
To test the example run `npm install` and then `npm start`.
|
||||
|
||||
The `master` branch's HEAD is deployed at https://examples.waku.org/relay-direct-chat/.
|
||||
|
||||
### Steps to run an example:
|
||||
1. Get a Waku node that implements `/libp2p/circuit/relay/0.2.0/hop` and `/libp2p/circuit/relay/0.2.0/stop`
|
||||
1.1. Find `go-waku` node or
|
||||
1.2. Build and then run `go-waku` node with following command: `./build/waku --ws true --relay true --circuit-relay true`
|
||||
2. Copy node's multiaddr (e.g `/ip4/192.168.0.101/tcp/60001/ws/p2p/16Uiu2HAm9w2xeDWFJm5eeGLZfJdaPtkNatQD1xrzK5EFWSeXdFvu`)
|
||||
3. In `relay-chat` example's folder run `npm install` and then `npm start`
|
||||
4. Use `go-waku`'s multiaddr for **Remote node multiaddr** and press dial. Repeat in two more tabs.
|
||||
5. In `tab2` copy **Local Peer Id** and use as **WebRTC Peer** in `tab1` and press dial.
|
||||
6. In `tab1` or `tab2` press **Ensure WebRTC Relay connection**
|
||||
7. In `tab1` press **Drop non WebRTC connections**
|
||||
8. In `tab1` enter **Nickname** and **Message** and send.
|
||||
9. See the message in `tab3` which was connected only to `go-waku` node.
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,73 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>Relay direct chat</title>
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<link rel="apple-touch-icon" href="./favicon.png" />
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<h3>Status: <span id="status"></span></h3>
|
||||
|
||||
<h4><label for="remoteNode">Remote node multiaddr</label></h4>
|
||||
<div>
|
||||
<input
|
||||
id="remoteNode"
|
||||
value="/dns4/node-01.ac-cn-hongkong-c.go-waku.prod.statusim.net/tcp/443/wss/p2p/16Uiu2HAm1fVVprL9EaDpDw4frNX3CPfZu5cBqhtk9Ncm6WCvprpv"
|
||||
/>
|
||||
<button id="connectRemoteNode">Dial</button>
|
||||
</div>
|
||||
|
||||
<h4><label for="webrtcPeer">WebRTC Peer</label></h4>
|
||||
<div>
|
||||
<input id="webrtcPeer" />
|
||||
<button id="connectWebrtcPeer">Dial</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="relayWebRTC">Ensure WebRTC Relay connection</button>
|
||||
<button id="dropNonWebRTC">Drop non WebRTC connections</button>
|
||||
</div>
|
||||
|
||||
<details open>
|
||||
<summary>Peer's information</summary>
|
||||
|
||||
<h4>Content topic</h4>
|
||||
<p id="contentTopic"></p>
|
||||
|
||||
<h4>Local Peer Id</h4>
|
||||
<p id="localPeerId"></p>
|
||||
|
||||
<h4>Remote Peer Id</h4>
|
||||
<p id="remotePeerId"></p>
|
||||
|
||||
<h4>Relay mesh's protocols</h4>
|
||||
<p id="relayMeshInfo"></p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="inputArea">
|
||||
<input type="text" id="nickText" placeholder="Nickname" />
|
||||
<textarea id="messageText" placeholder="Message"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button id="send">Send</button>
|
||||
<button id="exit">Exit chat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="//cdn.jsdelivr.net/npm/protobufjs@7.X.X/dist/protobuf.min.js"></script>
|
||||
<script type="module" src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,340 @@
|
|||
import {
|
||||
createRelayNode,
|
||||
bytesToUtf8,
|
||||
utf8ToBytes,
|
||||
createDecoder,
|
||||
createEncoder,
|
||||
} from "@waku/sdk";
|
||||
|
||||
import { webSockets } from "@libp2p/websockets";
|
||||
import { all as filterAll } from "@libp2p/websockets/filters";
|
||||
|
||||
import { webRTC } from "@libp2p/webrtc";
|
||||
import { circuitRelayTransport } from "libp2p/circuit-relay";
|
||||
|
||||
const CONTENT_TOPIC = "/toy-chat/2/huilong/proto";
|
||||
|
||||
const ui = initUI();
|
||||
runApp(ui).catch((err) => {
|
||||
console.error(err);
|
||||
ui.setStatus(`error: ${err.message}`, "error");
|
||||
});
|
||||
|
||||
async function runApp(ui) {
|
||||
const {
|
||||
info,
|
||||
sendMessage,
|
||||
unsubscribeFromMessages,
|
||||
dial,
|
||||
dialWebRTCpeer,
|
||||
dropNetworkConnections,
|
||||
ensureWebRTCconnectionInRelayMesh,
|
||||
} = await initWakuContext({
|
||||
ui,
|
||||
contentTopic: CONTENT_TOPIC,
|
||||
});
|
||||
|
||||
ui.setLocalPeer(info.localPeerId);
|
||||
ui.setContentTopic(info.contentTopic);
|
||||
|
||||
ui.onSendMessage(sendMessage);
|
||||
ui.onRemoteNodeConnect(dial);
|
||||
ui.onWebrtcConnect(dialWebRTCpeer);
|
||||
ui.onRelayWebRTC(ensureWebRTCconnectionInRelayMesh);
|
||||
ui.onDropNonWebRTC(dropNetworkConnections);
|
||||
|
||||
ui.onExit(async () => {
|
||||
ui.setStatus("disconnecting...", "progress");
|
||||
await unsubscribeFromMessages();
|
||||
ui.setStatus("disconnected", "terminated");
|
||||
ui.resetMessages();
|
||||
});
|
||||
}
|
||||
|
||||
async function initWakuContext({ ui, contentTopic }) {
|
||||
const Decoder = createDecoder(contentTopic);
|
||||
const Encoder = createEncoder({ 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"));
|
||||
|
||||
ui.setStatus("starting...", "progress");
|
||||
|
||||
const node = await createRelayNode({
|
||||
libp2p: {
|
||||
addresses: {
|
||||
listen: ["/webrtc"],
|
||||
},
|
||||
connectionGater: {
|
||||
denyDialMultiaddr: () => {
|
||||
// refuse to deny localhost addresses
|
||||
return false;
|
||||
},
|
||||
},
|
||||
transports: [
|
||||
webRTC({}),
|
||||
circuitRelayTransport({
|
||||
discoverRelays: 1,
|
||||
}),
|
||||
webSockets({ filter: filterAll }),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await node.start();
|
||||
|
||||
// Set a filter by using Decoder for a given ContentTopic
|
||||
const unsubscribeFromMessages = await node.relay.subscribe(
|
||||
[Decoder],
|
||||
(wakuMessage) => {
|
||||
const messageObj = ChatMessage.decode(wakuMessage.payload);
|
||||
ui.renderMessage({
|
||||
...messageObj,
|
||||
text: bytesToUtf8(messageObj.text),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
ui.setStatus("started", "success");
|
||||
|
||||
const localPeerId = node.libp2p.peerId.toString();
|
||||
|
||||
const remotePeers = await node.libp2p.peerStore.all();
|
||||
const remotePeerIds = new Set(remotePeers.map((peer) => peer.id.toString()));
|
||||
|
||||
ui.setRemotePeer(Array.from(remotePeerIds.keys()));
|
||||
|
||||
node.libp2p.addEventListener("peer:connect", async (event) => {
|
||||
remotePeerIds.add(event.detail.toString());
|
||||
ui.setRemotePeer(Array.from(remotePeerIds.keys()));
|
||||
ui.setRelayMeshInfo(node.relay.gossipSub);
|
||||
});
|
||||
|
||||
node.libp2p.addEventListener("peer:disconnect", (event) => {
|
||||
remotePeerIds.delete(event.detail.toString());
|
||||
ui.setRemotePeer(Array.from(remotePeerIds.keys()));
|
||||
ui.setRelayMeshInfo(node.relay.gossipSub);
|
||||
});
|
||||
|
||||
node.libp2p.addEventListener("peer:identify", (event) => {
|
||||
const peer = event.detail;
|
||||
|
||||
if (!peer.protocols.includes("/webrtc-signaling/0.0.1")) {
|
||||
return;
|
||||
}
|
||||
|
||||
ui.setWebrtcPeer(peer.peerId.toString());
|
||||
ui.setRelayMeshInfo(node.relay.gossipSub);
|
||||
});
|
||||
|
||||
window.node = node;
|
||||
|
||||
return {
|
||||
unsubscribeFromMessages,
|
||||
info: {
|
||||
contentTopic,
|
||||
localPeerId,
|
||||
},
|
||||
sendMessage: async ({ text, nick }) => {
|
||||
if (!text || !nick) {
|
||||
return;
|
||||
}
|
||||
|
||||
const protoMessage = ChatMessage.create({
|
||||
nick,
|
||||
timestamp: Date.now(),
|
||||
text: utf8ToBytes(text),
|
||||
});
|
||||
|
||||
await node.relay.send(Encoder, {
|
||||
payload: ChatMessage.encode(protoMessage).finish(),
|
||||
});
|
||||
},
|
||||
dial: async (multiaddr) => {
|
||||
ui.setStatus("connecting...", "progress");
|
||||
await node.dial(multiaddr);
|
||||
ui.setStatus("connected", "success");
|
||||
},
|
||||
dialWebRTCpeer: async (peerId) => {
|
||||
const peers = await node.libp2p.peerStore.all();
|
||||
const circuitPeer = peers.filter(
|
||||
(p) =>
|
||||
p.protocols.includes("/libp2p/circuit/relay/0.2.0/hop") &&
|
||||
p.protocols.includes("/libp2p/circuit/relay/0.2.0/stop")
|
||||
)[0];
|
||||
|
||||
if (!circuitPeer) {
|
||||
throw Error("No Circuit peer is found");
|
||||
}
|
||||
|
||||
let multiaddr = circuitPeer.addresses.pop().multiaddr;
|
||||
multiaddr = `${multiaddr}/p2p/${circuitPeer.id.toString()}/p2p-circuit/webrtc/p2p/${peerId}`;
|
||||
|
||||
await node.dial(multiaddr);
|
||||
ui.setRelayMeshInfo(node.relay.gossipSub);
|
||||
},
|
||||
ensureWebRTCconnectionInRelayMesh: async () => {
|
||||
const promises = node.libp2p
|
||||
.getConnections()
|
||||
.filter((c) => c.stat.multiplexer === "/webrtc")
|
||||
.map(async (c) => {
|
||||
const outboundStream = node.relay.gossipSub.streamsOutbound.get(
|
||||
c.remotePeer.toString()
|
||||
);
|
||||
const isWebRTCOutbound =
|
||||
outboundStream.rawStream.constructor.name === "WebRTCStream";
|
||||
|
||||
if (isWebRTCOutbound) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.relay.gossipSub.streamsOutbound.delete(c.remotePeer.toString());
|
||||
await node.relay.gossipSub.createOutboundStream(
|
||||
c.remotePeer.toString(),
|
||||
c
|
||||
);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
ui.setRelayMeshInfo(node.relay.gossipSub);
|
||||
},
|
||||
dropNetworkConnections: async () => {
|
||||
const promises = node.libp2p
|
||||
.getConnections()
|
||||
.filter((c) => c.stat.multiplexer !== "/webrtc")
|
||||
.map(async (c) => {
|
||||
const peerId = c.remotePeer.toString();
|
||||
|
||||
node.relay.gossipSub.peers.delete(peerId);
|
||||
node.relay.gossipSub.streamsInbound.delete(peerId);
|
||||
node.relay.gossipSub.streamsOutbound.delete(peerId);
|
||||
|
||||
await node.libp2p.peerStore.delete(c.remotePeer);
|
||||
await c.close();
|
||||
});
|
||||
await Promise.all(promises);
|
||||
ui.setRelayMeshInfo(node.relay.gossipSub);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 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 contentTopicBlock = document.getElementById("contentTopic");
|
||||
|
||||
const messagesBlock = document.getElementById("messages");
|
||||
|
||||
const nickText = document.getElementById("nickText");
|
||||
const messageText = document.getElementById("messageText");
|
||||
|
||||
const remoteNode = document.getElementById("remoteNode");
|
||||
const connectRemoteNode = document.getElementById("connectRemoteNode");
|
||||
|
||||
const webrtcPeer = document.getElementById("webrtcPeer");
|
||||
const connectWebrtcPeer = document.getElementById("connectWebrtcPeer");
|
||||
|
||||
const relayWebRTCbutton = document.getElementById("relayWebRTC");
|
||||
const dropNonWebRTCbutton = document.getElementById("dropNonWebRTC");
|
||||
|
||||
const relayMeshInfo = document.getElementById("relayMeshInfo");
|
||||
|
||||
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 = `<span class=${className || ""}>${value}</span>`;
|
||||
},
|
||||
setLocalPeer: (id) => {
|
||||
localPeerBlock.innerText = id.toString();
|
||||
},
|
||||
setRemotePeer: (ids) => {
|
||||
remotePeerId.innerText = ids.join("\n");
|
||||
},
|
||||
setContentTopic: (topic) => {
|
||||
contentTopicBlock.innerText = topic.toString();
|
||||
},
|
||||
renderMessage: (messageObj) => {
|
||||
const { nick, text, timestamp } = messageObj;
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// WARNING: XSS vulnerable
|
||||
messagesBlock.innerHTML += `
|
||||
<div class="message">
|
||||
<p>${nick} <span>(${date.toDateString()})</span>:</p>
|
||||
<p>${text}</p>
|
||||
<div>
|
||||
`;
|
||||
},
|
||||
resetMessages: () => {
|
||||
messagesBlock.innerHTML = "";
|
||||
},
|
||||
setWebrtcPeer: (peerId) => {
|
||||
webrtcPeer.value = peerId;
|
||||
},
|
||||
onRemoteNodeConnect: (cb) => {
|
||||
connectRemoteNode.addEventListener("click", () => {
|
||||
const multiaddr = remoteNode.value;
|
||||
|
||||
if (!multiaddr) {
|
||||
throw Error("No multiaddr set to dial");
|
||||
}
|
||||
|
||||
cb(multiaddr);
|
||||
});
|
||||
},
|
||||
onWebrtcConnect: (cb) => {
|
||||
connectWebrtcPeer.addEventListener("click", () => {
|
||||
const multiaddr = webrtcPeer.value;
|
||||
|
||||
if (!multiaddr) {
|
||||
throw Error("No multiaddr to dial webrtc");
|
||||
}
|
||||
|
||||
cb(multiaddr);
|
||||
});
|
||||
},
|
||||
onRelayWebRTC: (cb) => {
|
||||
relayWebRTCbutton.addEventListener("click", cb);
|
||||
},
|
||||
onDropNonWebRTC: (cb) => {
|
||||
dropNonWebRTCbutton.addEventListener("click", cb);
|
||||
},
|
||||
setRelayMeshInfo: (gossipSub) => {
|
||||
relayMeshInfo.innerHTML = "";
|
||||
|
||||
Array.from(gossipSub.peers)
|
||||
.map((peerId) => {
|
||||
let inbound = gossipSub.streamsInbound.get(peerId);
|
||||
inbound = inbound ? inbound.rawStream.constructor.name : "none";
|
||||
|
||||
let outbound = gossipSub.streamsOutbound.get(peerId);
|
||||
outbound = outbound ? outbound.rawStream.constructor.name : "none";
|
||||
|
||||
return [peerId, inbound, outbound];
|
||||
})
|
||||
.map(([peerId, inbound, outbound]) => {
|
||||
relayMeshInfo.innerHTML += `${peerId}<br /><b>inbound</b>: ${inbound}\t<b>outbound</b>: ${outbound}`;
|
||||
relayMeshInfo.innerHTML += "<br /><br />";
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Relay direct chat",
|
||||
"description": "Send messages between several users (or just one) using Relay with direct WebRTC connection.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "light",
|
||||
"version": "1.0.0",
|
||||
"description": "**Demonstrates**:",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"start": "webpack-dev-server"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@libp2p/webrtc": "^2.0.11",
|
||||
"@libp2p/websockets": "^6.0.3",
|
||||
"@waku/dns-discovery": "^0.0.15",
|
||||
"@waku/sdk": "^0.0.17",
|
||||
"libp2p": "^0.45.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"webpack": "^5.74.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
* {
|
||||
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;
|
||||
}
|
||||
|
||||
h3 + h4,
|
||||
div + h4,
|
||||
div + details,
|
||||
div + div {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.header div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header div input {
|
||||
min-width: 300px;
|
||||
min-height: 30px;
|
||||
width: 80%;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.header div button {
|
||||
min-width: 50px;
|
||||
min-height: 30px;
|
||||
width: 10%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header div button + button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./index.js",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "build"),
|
||||
filename: "index.js",
|
||||
},
|
||||
experiments: {
|
||||
asyncWebAssembly: true,
|
||||
},
|
||||
mode: "development",
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
"index.html",
|
||||
"favicon.ico",
|
||||
"favicon.png",
|
||||
"manifest.json",
|
||||
"style.css",
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
# Using Waku Relay in JavaScript
|
||||
|
||||
**Demonstrates**:
|
||||
|
||||
- Waku Relay: Send and receive messages using Waku Relay.
|
||||
- Pure Javascript/HTML.
|
||||
- Use minified bundle of js from unpkg.com, no import, no package manager.
|
||||
|
||||
This example uses Waku Relay to send and receive simple text messages.
|
||||
|
||||
To test the example, simply download the `index.html` file from this folder and open it in a browser.
|
||||
|
||||
The `master` branch's HEAD is deployed at https://examples.waku.org/relay-js/.
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,131 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title>JS-Waku Chat</title>
|
||||
<link rel="apple-touch-icon" href="./favicon.png" />
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
<link rel="icon" href="./favicon.ico" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div><h1>Waku Node Status</h1></div>
|
||||
<div id="status"></div>
|
||||
|
||||
<label for="textInput">Message text</label>
|
||||
<input
|
||||
disabled
|
||||
id="textInput"
|
||||
placeholder="Type your message here"
|
||||
type="text"
|
||||
/>
|
||||
<button disabled id="sendButton" type="button">
|
||||
Send Message using Relay
|
||||
</button>
|
||||
|
||||
<div><h1>Messages</h1></div>
|
||||
<div id="messages"></div>
|
||||
|
||||
<script type="module">
|
||||
/**
|
||||
* Demonstrate usage of js-waku in the browser. Use relay, gossip sub protocol to send and receive messages.
|
||||
* Recommended payload is protobuf. Using simple utf-8 string for demo purposes only.
|
||||
*/
|
||||
|
||||
import {
|
||||
waitForRemotePeer,
|
||||
createDecoder,
|
||||
createEncoder,
|
||||
bytesToUtf8,
|
||||
utf8ToBytes,
|
||||
createRelayNode,
|
||||
} from "https://unpkg.com/@waku/sdk@0.0.18/bundle/index.js";
|
||||
|
||||
const statusDiv = document.getElementById("status");
|
||||
const messagesDiv = document.getElementById("messages");
|
||||
const textInput = document.getElementById("textInput");
|
||||
const sendButton = document.getElementById("sendButton");
|
||||
|
||||
// Every Waku Message has a content topic that categorizes it.
|
||||
// It is always encoded in clear text.
|
||||
// Recommendation: `/dapp-name/version/functionality/codec`
|
||||
// We recommend to use protobuf as codec (`proto`), this demo uses utf-8
|
||||
// for simplicity's sake.
|
||||
const contentTopic = "/js-waku-examples/1/chat/utf8";
|
||||
|
||||
// Prepare encoder and decoder, `V0` for clear text messages.
|
||||
|
||||
const encoder = createEncoder({ contentTopic });
|
||||
const decoder = createDecoder(contentTopic);
|
||||
|
||||
try {
|
||||
statusDiv.innerHTML = "<p>Starting</p>";
|
||||
|
||||
// Create and starts a Waku node.
|
||||
// `defaultBootstrap: true` bootstraps by connecting to pre-defined/hardcoded Waku nodes.
|
||||
// `emitSelf`: emits event of sent message to itself and invokes subscribers by it
|
||||
// We are currently working on migrating this method to DNS Discovery.
|
||||
//
|
||||
// https://js.waku.org/functions/lib_create_waku.createPrivacyNode.html
|
||||
const waku = await createRelayNode({
|
||||
emitSelf: true,
|
||||
defaultBootstrap: true,
|
||||
});
|
||||
await waku.start();
|
||||
|
||||
// Add a hook to process all incoming messages on a specified content topic.
|
||||
//
|
||||
// https://js.waku.org/classes/index.waku_relay.WakuRelay.html#addObserver
|
||||
waku.relay.subscribe(
|
||||
decoder,
|
||||
(message) => {
|
||||
// Checks there is a payload on the message.
|
||||
// Waku Message is encoded in protobuf, in proto v3 fields are always optional.
|
||||
//
|
||||
// https://js.waku.org/interfaces/index.proto_message.WakuMessage-1.html#payload
|
||||
if (!message.payload) return;
|
||||
|
||||
// Helper method to decode the payload to utf-8. A production dApp should
|
||||
// use `wakuMessage.payload` (Uint8Array) which enables encoding a data
|
||||
// structure of their choice.
|
||||
//
|
||||
// https://js.waku.org/functions/index.utils.bytesToUtf8.html
|
||||
const text = bytesToUtf8(message.payload);
|
||||
messagesDiv.innerHTML =
|
||||
`<p>${text}</p><br />` + messagesDiv.innerHTML;
|
||||
},
|
||||
[contentTopic]
|
||||
);
|
||||
|
||||
statusDiv.innerHTML = "<p>Connecting to a peer</p>";
|
||||
|
||||
// Best effort method that waits for the Waku node to be connected to remote
|
||||
// waku nodes (peers) and for appropriate handshakes to be done.
|
||||
//
|
||||
// https://js.waku.org/functions/lib_wait_for_remote_peer.waitForRemotePeer.html
|
||||
await waitForRemotePeer(waku);
|
||||
|
||||
// We are now connected to a remote peer, let's define the `sendMessage`
|
||||
// function that sends the text input over Waku Relay, the gossipsub
|
||||
// protocol.
|
||||
sendButton.onclick = async () => {
|
||||
const payload = utf8ToBytes(textInput.value);
|
||||
await waku.relay.send(encoder, { payload });
|
||||
console.log("Message sent!");
|
||||
|
||||
// Reset the text input.
|
||||
textInput.value = null;
|
||||
};
|
||||
|
||||
// Ready to send & receive messages, enable text input.
|
||||
textInput.disabled = false;
|
||||
sendButton.disabled = false;
|
||||
statusDiv.innerHTML = "<p>Ready!</p>";
|
||||
} catch (e) {
|
||||
statusDiv.innerHTML = "Failed to start application";
|
||||
console.log(e);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Relay JS",
|
||||
"description": "This example uses Waku Relay to send and receive simple text messages.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
|
@ -0,0 +1 @@
|
|||
auto-install-peers=true
|
|
@ -0,0 +1,22 @@
|
|||
# Minimal ReactJS Waku Relay App
|
||||
|
||||
**Demonstrates**:
|
||||
|
||||
- Group chat
|
||||
- React/JavaScript
|
||||
- `create-react-app`/`react-scripts` 5.0.0
|
||||
- Waku Relay
|
||||
- Protobuf using `protobufjs`.
|
||||
|
||||
A barebone chat app to illustrate the seamless integration of `js-waku` into ReactJS.
|
||||
|
||||
The `master` branch's HEAD is deployed at https://examples.waku.org/relay-reactjs-chat/.
|
||||
|
||||
To run a development version locally, do:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/waku-org/js-waku-examples
|
||||
cd js-waku-examples/examples/relay-reactjs-chat
|
||||
npm install
|
||||
npm run start
|
||||
```
|
|
@ -0,0 +1,28 @@
|
|||
const { getLoaders, loaderByName } = require("@craco/craco");
|
||||
|
||||
module.exports = {
|
||||
webpack: {
|
||||
configure: (webpackConfig) => {
|
||||
const { hasFoundAny, matches } = getLoaders(
|
||||
webpackConfig,
|
||||
loaderByName("babel-loader")
|
||||
);
|
||||
|
||||
if (hasFoundAny) {
|
||||
matches.forEach((c) => {
|
||||
// Modify test to include cjs for @chainsafe/libp2p-gossipsub rpc module
|
||||
if (c.loader.test.toString().includes("mjs")) {
|
||||
// If your project uses typescript then do not forget to include `ts`/`tsx`
|
||||
if (c.loader.test.toString().includes("jsx")) {
|
||||
c.loader.test = /\.(js|cjs|mjs|jsx)$/;
|
||||
} else {
|
||||
c.loader.test = /\.(js|cjs|mjs)$/;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return webpackConfig;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "relay-reactjs-chat",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "/relay-reactjs-chat",
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@waku/sdk": "^0.0.18",
|
||||
"protobufjs": "^7.1.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "GENERATE_SOURCEMAP=false PORT=3001 craco start",
|
||||
"build": "GENERATE_SOURCEMAP=false craco build",
|
||||
"test": "exit 0",
|
||||
"eject": "craco eject"
|
||||
},
|
||||
"browser": {
|
||||
"crypto": false
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not ie <= 99",
|
||||
"not android <= 4.4.4",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "7.0.0",
|
||||
"eslint": "^8.28.0"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A barebone chat app to illustrate the ReactJS Relay guide."
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React Relay</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Waku Relay",
|
||||
"description": "A barebone chat app to illustrate the ReactJS Relay guide.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -0,0 +1,38 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import * as React from "react";
|
||||
import protobuf from "protobufjs";
|
||||
import {
|
||||
createRelayNode,
|
||||
createDecoder,
|
||||
createEncoder,
|
||||
waitForRemotePeer,
|
||||
} from "@waku/sdk";
|
||||
|
||||
const ContentTopic = `/js-waku-examples/1/chat/proto`;
|
||||
const Encoder = createEncoder({ contentTopic: ContentTopic });
|
||||
const Decoder = createDecoder(ContentTopic);
|
||||
|
||||
const SimpleChatMessage = new protobuf.Type("SimpleChatMessage")
|
||||
.add(new protobuf.Field("timestamp", 1, "uint32"))
|
||||
.add(new protobuf.Field("text", 2, "string"));
|
||||
|
||||
function App() {
|
||||
const [waku, setWaku] = React.useState(undefined);
|
||||
const [wakuStatus, setWakuStatus] = React.useState("None");
|
||||
// Using a counter just for the messages to be different
|
||||
const [sendCounter, setSendCounter] = React.useState(0);
|
||||
const [messages, setMessages] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!!waku) return;
|
||||
if (wakuStatus !== "None") return;
|
||||
|
||||
setWakuStatus("Starting");
|
||||
(async () => {
|
||||
const waku = await createRelayNode({ defaultBootstrap: true });
|
||||
|
||||
setWaku(waku);
|
||||
await waku.start();
|
||||
setWakuStatus("Connecting");
|
||||
await waitForRemotePeer(waku, ["relay"]);
|
||||
setWakuStatus("Ready");
|
||||
})();
|
||||
}, [waku, wakuStatus]);
|
||||
|
||||
const processIncomingMessage = React.useCallback((wakuMessage) => {
|
||||
console.log("Message received", wakuMessage);
|
||||
if (!wakuMessage.payload) return;
|
||||
|
||||
const { text, timestamp } = SimpleChatMessage.decode(wakuMessage.payload);
|
||||
|
||||
const time = new Date();
|
||||
|
||||
time.setTime(timestamp);
|
||||
const message = { text, timestamp: time };
|
||||
|
||||
setMessages((messages) => {
|
||||
return [message].concat(messages);
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!waku) return;
|
||||
|
||||
// Pass the content topic to only process messages related to your dApp
|
||||
const deleteObserver = waku.relay.subscribe(
|
||||
Decoder,
|
||||
processIncomingMessage
|
||||
);
|
||||
|
||||
// Called when the component is unmounted, see ReactJS doc.
|
||||
return deleteObserver;
|
||||
}, [waku, wakuStatus, processIncomingMessage]);
|
||||
|
||||
const sendMessageOnClick = () => {
|
||||
// Check Waku is started and connected first.
|
||||
if (wakuStatus !== "Ready") return;
|
||||
|
||||
sendMessage(`Here is message #${sendCounter}`, waku, new Date()).then(() =>
|
||||
console.log("Message sent")
|
||||
);
|
||||
|
||||
// For demonstration purposes.
|
||||
setSendCounter(sendCounter + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<p>{wakuStatus}</p>
|
||||
<button onClick={sendMessageOnClick} disabled={wakuStatus !== "Ready"}>
|
||||
Send Message
|
||||
</button>
|
||||
<ul>
|
||||
{messages.map((msg) => {
|
||||
return (
|
||||
<li key={msg.timestamp.valueOf()}>
|
||||
<p>
|
||||
{msg.timestamp.toString()}: {msg.text}
|
||||
</p>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function sendMessage(message, waku, timestamp) {
|
||||
const time = timestamp.getTime();
|
||||
|
||||
// Encode to protobuf
|
||||
const protoMsg = SimpleChatMessage.create({
|
||||
timestamp: time,
|
||||
text: message,
|
||||
});
|
||||
const payload = SimpleChatMessage.encode(protoMsg).finish();
|
||||
|
||||
// Send over Waku Relay
|
||||
return waku.relay.send(Encoder, { payload });
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,8 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import App from "./App";
|
||||
|
||||
test("renders learn react link", () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom";
|