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";
|