Compare commits
141 Commits
file-shari
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
0b1b2f1d90 | ||
|
89f903f856 | ||
|
8bd54b5f5f | ||
|
5911cc6efa | ||
|
f3491836ce | ||
|
dcabdb446c | ||
|
bce861bb44 | ||
|
716c37225c | ||
|
07e02fa07e | ||
|
9d76959374 | ||
|
7b676b4fa2 | ||
|
bd7e8df0e3 | ||
|
5418848687 | ||
|
4719dbae4d | ||
|
c265be4f4b | ||
|
0f7916cac5 | ||
|
5062817d50 | ||
|
ea2928ccf8 | ||
|
5203c86a48 | ||
|
c92ec13242 | ||
|
dc876e6589 | ||
|
1a94400af2 | ||
|
b647d72c85 | ||
|
41f3078f97 | ||
|
28bf6f124f | ||
|
49aedc0554 | ||
|
fefa8fa7ce | ||
|
cf58f6196e | ||
|
4274ec9fee | ||
|
7f7a497ba0 | ||
|
ff518bd3b9 | ||
|
66ffd16948 | ||
|
5d9af13637 | ||
|
6556039639 | ||
|
0cb5164753 | ||
|
a706ba1432 | ||
|
656c15bdc8 | ||
|
915e0a2aea | ||
|
5df3e36baa | ||
|
648b992f31 | ||
|
36a297b968 | ||
|
0ef49f1a80 | ||
|
79f62ab5e6 | ||
|
67dff70cca | ||
|
acb5d0c8ad | ||
|
5e3ca6c49f | ||
|
1660fcf4e2 | ||
|
d4213cec13 | ||
|
8ee2ebfbb5 | ||
|
db307829f4 | ||
|
7ab861e0ec | ||
|
62f0a7ec35 | ||
|
7bb626cba0 | ||
|
07ec45afdc | ||
|
e0e9c8d929 | ||
|
a15e76bc0c | ||
|
4c20d07f28 | ||
|
daa7a32627 | ||
|
36f6b23c18 | ||
|
fcf9fcea52 | ||
|
476657b7c0 | ||
|
9aaa84bfae | ||
|
dc78137702 | ||
|
56dbbf2665 | ||
|
da529efb1c | ||
|
ff03190bf5 | ||
|
501a6d8d11 | ||
|
dbf360c7b8 | ||
|
20b9e7fe4e | ||
|
a8dff459f3 | ||
|
73218f4eea | ||
|
47d8bf8f99 | ||
|
df842e9b2f | ||
|
f1f5927767 | ||
|
2d63f7c580 | ||
|
35947a559b | ||
|
677b13f47c | ||
|
3b24c5275d | ||
|
3b5c566807 | ||
|
f12510172b | ||
|
3616846c03 | ||
|
fcf45db228 | ||
|
3e185b98d5 | ||
|
f7fb092e67 | ||
|
80407f707d | ||
|
978dd74adc | ||
|
a029cb7385 | ||
|
a3e217e69b | ||
|
89830c2611 | ||
|
263cd8b4e4 | ||
|
ef514a6d1b | ||
|
456abaf799 | ||
|
2ac8c2f04e | ||
|
28e4953570 | ||
|
e36cca6f43 | ||
|
f64570d6b6 | ||
|
65c4c4890a | ||
|
7e40e0cd9f | ||
|
87f0a185d8 | ||
|
4718032415 | ||
|
b4c86515c0 | ||
|
367a4e3d62 | ||
|
91ef4ad32f | ||
|
24ac1f1bcd | ||
|
3adaf41ea0 | ||
|
f4d17d883e | ||
|
2dbd29a90a | ||
|
88e47c054f | ||
|
c03b4ad50b | ||
|
aaa0ed83ad | ||
|
ffc32fa2bf | ||
|
5c4202fa4c | ||
|
016e4c8161 | ||
|
bd2ecf202c | ||
|
401ec916bb | ||
|
aacfed853e | ||
|
6c434f84ab | ||
|
72526ebbbb | ||
|
7556d491f3 | ||
|
3a4a09ce69 | ||
|
df6d10868e | ||
|
3ce8dec639 | ||
|
533bff5f9f | ||
|
05b4615af9 | ||
|
89abe718db | ||
|
547564d921 | ||
|
915ea21316 | ||
|
042c447b55 | ||
|
7ecb17d0a1 | ||
|
0eb5b596dd | ||
|
d81e930da6 | ||
|
5d4619965c | ||
|
5f2444f9c9 | ||
|
f9768627ee | ||
|
e644b20f4e | ||
|
7c5c12178d | ||
|
520b863f2a | ||
|
ea34058fa7 | ||
|
72bc66a340 | ||
|
94a4b2fb2e | ||
|
4d6d1482f2 |
@ -1,5 +1,6 @@
|
||||
{
|
||||
"extends": ["react-app"],
|
||||
"extends": ["react-app", "plugin:prettier/recommended"],
|
||||
"plugins": ["prettier"],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
|
25
.github/workflows/ci.yml
vendored
Normal file
25
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
test_and_build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
- name: 'Build web app artifacts'
|
||||
run: npm run build
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-output
|
||||
path: dist
|
33
.github/workflows/deploy.yml
vendored
Normal file
33
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
name: Deploy to Github Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deployment:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
deploy_key: ${{ secrets.DEPLOY_KEY }}
|
||||
publish_dir: ./dist
|
||||
force_orphan: true
|
634
.gitignore
vendored
634
.gitignore
vendored
@ -9,7 +9,9 @@
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
/dist
|
||||
/dev-dist
|
||||
/src/config/rtcConfig.ts
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@ -25,3 +27,633 @@ yarn-error.log*
|
||||
|
||||
# Editors
|
||||
Session.vim
|
||||
build.zip
|
||||
package-lock.json
|
||||
build/404.html
|
||||
build/asset-manifest.json
|
||||
build/CNAME
|
||||
build/favicon.ico
|
||||
build/index.html
|
||||
build/logo192.png
|
||||
build/logo512.png
|
||||
build/manifest.json
|
||||
build/robots.txt
|
||||
build/sdk.js
|
||||
build/sdk.js.map
|
||||
build/service-worker.js
|
||||
build/service-worker.js.LICENSE.txt
|
||||
build/service-worker.js.map
|
||||
build/logo/favicon.svg
|
||||
build/logo/logo.svg
|
||||
build/sounds/new-message.aac
|
||||
build/static/css/3351.c06914f1.chunk.css
|
||||
build/static/css/3351.c06914f1.chunk.css.map
|
||||
build/static/css/main.845e3211.css
|
||||
build/static/css/main.845e3211.css.map
|
||||
build/static/js/787.101cda68.chunk.js
|
||||
build/static/js/787.101cda68.chunk.js.map
|
||||
build/static/js/1879.f9ed8391.chunk.js
|
||||
build/static/js/3254.292fba86.chunk.js
|
||||
build/static/js/3254.292fba86.chunk.js.LICENSE.txt
|
||||
build/static/js/3254.292fba86.chunk.js.map
|
||||
build/static/js/3351.bce00804.chunk.js
|
||||
build/static/js/3351.bce00804.chunk.js.map
|
||||
build/static/js/5801.d5bbfa98.chunk.js
|
||||
build/static/js/5801.d5bbfa98.chunk.js.map
|
||||
build/static/js/6197.a90915c3.chunk.js
|
||||
build/static/js/6197.a90915c3.chunk.js.map
|
||||
build/static/js/6926.fac58f8b.chunk.js
|
||||
build/static/js/6926.fac58f8b.chunk.js.map
|
||||
build/static/js/7918.d34e0d3c.chunk.js
|
||||
build/static/js/8221.f063fca8.chunk.js
|
||||
build/static/js/8221.f063fca8.chunk.js.map
|
||||
build/static/js/8599.bc071e1f.chunk.js
|
||||
build/static/js/8599.bc071e1f.chunk.js.map
|
||||
build/static/js/8778.5ba3e504.chunk.js
|
||||
build/static/js/9056.9898f814.chunk.js
|
||||
build/static/js/9910.7a65fbd4.chunk.js
|
||||
build/static/js/main.93e418b3.js
|
||||
build/static/js/main.93e418b3.js.LICENSE.txt
|
||||
build/static/js/main.93e418b3.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_abap.3770c4ae.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_abap.3770c4ae.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_abnf.ed2d35f9.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_abnf.ed2d35f9.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_actionscript.01709c28.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_actionscript.01709c28.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ada.a714ebfd.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ada.a714ebfd.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_agda.71bcb375.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_agda.71bcb375.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_al.8523274f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_al.8523274f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_antlr4.9bb977c6.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_antlr4.9bb977c6.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_apacheconf.ca0ca952.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_apacheconf.ca0ca952.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_apex.774f005c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_apex.774f005c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_apl.2e4ed6ff.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_apl.2e4ed6ff.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_applescript.8d6d6fdd.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_applescript.8d6d6fdd.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_aql.da991faa.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_aql.da991faa.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_arduino.18eeb2bb.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_arduino.18eeb2bb.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_arff.9ce4fbc1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_arff.9ce4fbc1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_asciidoc.30ff6b27.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_asciidoc.30ff6b27.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_asm6502.003753c1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_asm6502.003753c1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_asmatmel.50d5ea57.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_asmatmel.50d5ea57.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_aspnet.1eff1ccc.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_aspnet.1eff1ccc.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_autohotkey.9feeb630.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_autohotkey.9feeb630.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_autoit.134b2c7a.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_autoit.134b2c7a.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_avisynth.96691927.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_avisynth.96691927.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_avroIdl.aee3e4e1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_avroIdl.aee3e4e1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bash.3b60685e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bash.3b60685e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_basic.8c068269.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_basic.8c068269.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_batch.2bba26ce.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_batch.2bba26ce.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bbcode.3538ca7e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bbcode.3538ca7e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bicep.c92cc5e7.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bicep.c92cc5e7.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_birb.c7afa520.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_birb.c7afa520.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bison.c77284c9.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bison.c77284c9.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bnf.0da9203c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bnf.0da9203c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_brainfuck.190f2205.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_brainfuck.190f2205.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_brightscript.21bf1aa1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_brightscript.21bf1aa1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bro.e940f7f0.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bro.e940f7f0.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bsl.d5dfde96.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_bsl.d5dfde96.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_c.645e464b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_c.645e464b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cfscript.76838869.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cfscript.76838869.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_chaiscript.9f5cc3bc.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_chaiscript.9f5cc3bc.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cil.2c271c4f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cil.2c271c4f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_clike.9e99f5b1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_clike.9e99f5b1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_clojure.8ca7261f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_clojure.8ca7261f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cmake.f88c0327.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cmake.f88c0327.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cobol.248d0b73.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cobol.248d0b73.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_coffeescript.6ba75996.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_coffeescript.6ba75996.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_concurnas.50aab7b2.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_concurnas.50aab7b2.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_coq.00a6b435.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_coq.00a6b435.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cpp.441ea1d2.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cpp.441ea1d2.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_crystal.d9c98e8d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_crystal.d9c98e8d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_csharp.c5082886.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_csharp.c5082886.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cshtml.d484403e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cshtml.d484403e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_csp.e1cf0b6b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_csp.e1cf0b6b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_css.633090b4.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_css.633090b4.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cssExtras.d06dea38.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cssExtras.d06dea38.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_csv.aae72df3.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_csv.aae72df3.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cypher.4beb098b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_cypher.4beb098b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_d.4ef9a382.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_d.4ef9a382.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dart.ed0a0cd1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dart.ed0a0cd1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dataweave.b81624cc.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dataweave.b81624cc.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dax.d6ba78b6.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dax.d6ba78b6.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dhall.a5132a0b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dhall.a5132a0b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_diff.1d90c683.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_diff.1d90c683.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_django.484b4da1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_django.484b4da1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dnsZoneFile.bd8c712c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dnsZoneFile.bd8c712c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_docker.98f59ca7.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_docker.98f59ca7.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dot.2c3f15c2.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_dot.2c3f15c2.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ebnf.4f89a118.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ebnf.4f89a118.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_editorconfig.7f184e99.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_editorconfig.7f184e99.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_eiffel.87d2f7e5.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_eiffel.87d2f7e5.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ejs.72f83a1e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ejs.72f83a1e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_elixir.98b0c164.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_elixir.98b0c164.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_elm.854244b1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_elm.854244b1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_erb.c8ef9ac6.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_erb.c8ef9ac6.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_erlang.807b9c66.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_erlang.807b9c66.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_etlua.ada2074f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_etlua.ada2074f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_excelFormula.c62761a4.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_excelFormula.c62761a4.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_factor.e0818e95.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_factor.e0818e95.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_falselang.d1c18e87.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_falselang.d1c18e87.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_firestoreSecurityRules.233e2c30.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_firestoreSecurityRules.233e2c30.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_flow.75f21cba.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_flow.75f21cba.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_fortran.6ffc0fd0.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_fortran.6ffc0fd0.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_fsharp.a5e31859.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_fsharp.a5e31859.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ftl.57dc6317.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ftl.57dc6317.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gap.788b042a.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gap.788b042a.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gcode.c4c7cb2c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gcode.c4c7cb2c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gdscript.19e25055.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gdscript.19e25055.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gedcom.67a66a1d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gedcom.67a66a1d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gherkin.4396cba9.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gherkin.4396cba9.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_git.7ea5ef26.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_git.7ea5ef26.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_glsl.25a4951e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_glsl.25a4951e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gml.a9fedb1b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gml.a9fedb1b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gn.524f85a5.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_gn.524f85a5.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_go.d3145242.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_go.d3145242.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_goModule.bde00f1f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_goModule.bde00f1f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_graphql.2e97fd83.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_graphql.2e97fd83.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_groovy.2e708f5d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_groovy.2e708f5d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_haml.1557aecb.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_haml.1557aecb.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_handlebars.68cb4366.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_handlebars.68cb4366.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_haskell.1d8f2c81.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_haskell.1d8f2c81.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_haxe.3085b496.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_haxe.3085b496.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hcl.0578839a.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hcl.0578839a.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hlsl.697c95af.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hlsl.697c95af.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hoon.913023b8.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hoon.913023b8.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hpkp.1080a690.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hpkp.1080a690.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hsts.f53aeeb4.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_hsts.f53aeeb4.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_http.cd696ebf.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_http.cd696ebf.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ichigojam.7dbb8d08.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ichigojam.7dbb8d08.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_icon.a9155464.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_icon.a9155464.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_icuMessageFormat.4ecf3221.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_icuMessageFormat.4ecf3221.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_idris.4242114e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_idris.4242114e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_iecst.ae41b123.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_iecst.ae41b123.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ignore.9e0c8042.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ignore.9e0c8042.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_inform7.38e524b8.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_inform7.38e524b8.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ini.6692e4dc.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ini.6692e4dc.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_io.2f9b645e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_io.2f9b645e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_j.5a66e76b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_j.5a66e76b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_java.9ac20c75.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_java.9ac20c75.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_javadoc.42dc3422.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_javadoc.42dc3422.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_javadoclike.c9a887ad.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_javadoclike.c9a887ad.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_javascript.f300d60e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_javascript.f300d60e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_javastacktrace.eb5ff277.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_javastacktrace.eb5ff277.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jexl.91bcf086.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jexl.91bcf086.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jolie.a4202373.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jolie.a4202373.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jq.fcdd86e9.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jq.fcdd86e9.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsdoc.22e1f19d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsdoc.22e1f19d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsExtras.48cea1e5.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsExtras.48cea1e5.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_json.42f1caac.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_json.42f1caac.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_json5.decb5e70.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_json5.decb5e70.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsonp.863d5539.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsonp.863d5539.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsstacktrace.e1cc0564.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsstacktrace.e1cc0564.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsTemplates.6564756e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsTemplates.6564756e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsx.f2f054dd.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_jsx.f2f054dd.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_julia.95fbc7a3.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_julia.95fbc7a3.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_keepalived.ffa67ad7.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_keepalived.ffa67ad7.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_keyman.0b72908c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_keyman.0b72908c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_kotlin.691bd59a.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_kotlin.691bd59a.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_kumir.659105d6.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_kumir.659105d6.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_kusto.b2d7a11c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_kusto.b2d7a11c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_latex.4a07ef71.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_latex.4a07ef71.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_latte.96672677.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_latte.96672677.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_less.f9b4e88b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_less.f9b4e88b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_lilypond.e3e741c5.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_lilypond.e3e741c5.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_liquid.c602bc41.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_liquid.c602bc41.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_lisp.7ab0d89f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_lisp.7ab0d89f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_livescript.ce855174.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_livescript.ce855174.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_llvm.f2bc84d8.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_llvm.f2bc84d8.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_log.3fad41dd.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_log.3fad41dd.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_lolcode.84ecda30.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_lolcode.84ecda30.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_lua.52a6c33c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_lua.52a6c33c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_magma.0e9af4d7.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_magma.0e9af4d7.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_makefile.266fcc4d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_makefile.266fcc4d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_markdown.e59ba5fd.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_markdown.e59ba5fd.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_markup.84a87f7a.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_markup.84a87f7a.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_markupTemplating.a8c1c731.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_markupTemplating.a8c1c731.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_matlab.898a3780.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_matlab.898a3780.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_maxscript.ffc4775e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_maxscript.ffc4775e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_mel.d79aeb86.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_mel.d79aeb86.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_mermaid.f6ba0611.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_mermaid.f6ba0611.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_mizar.8388b307.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_mizar.8388b307.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_mongodb.3df51022.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_mongodb.3df51022.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_monkey.72f06aed.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_monkey.72f06aed.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_moonscript.37b0cd37.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_moonscript.37b0cd37.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_n1ql.4b14d687.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_n1ql.4b14d687.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_n4js.0c315394.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_n4js.0c315394.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nand2tetrisHdl.0d22ad45.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nand2tetrisHdl.0d22ad45.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_naniscript.2a3f597b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_naniscript.2a3f597b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nasm.3294a261.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nasm.3294a261.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_neon.d10e68a8.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_neon.d10e68a8.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nevod.e9b4a0d4.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nevod.e9b4a0d4.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nginx.7996cc1b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nginx.7996cc1b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nim.8d85c302.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nim.8d85c302.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nix.d3e20829.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nix.d3e20829.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nsis.43a7ee18.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_nsis.43a7ee18.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_objectivec.4d2e4bf2.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_objectivec.4d2e4bf2.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ocaml.1065cadd.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ocaml.1065cadd.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_opencl.3aa94984.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_opencl.3aa94984.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_openqasm.d6f8f6b6.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_openqasm.d6f8f6b6.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_oz.3a0cb911.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_oz.3a0cb911.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_parigp.f27c627f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_parigp.f27c627f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_parser.ab75055d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_parser.ab75055d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pascal.730cc923.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pascal.730cc923.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pascaligo.47bbefb6.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pascaligo.47bbefb6.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pcaxis.f5f06151.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pcaxis.f5f06151.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_peoplecode.57520fe4.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_peoplecode.57520fe4.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_perl.528c8b8e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_perl.528c8b8e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_php.7c3a39d1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_php.7c3a39d1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_phpdoc.47465e64.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_phpdoc.47465e64.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_phpExtras.3a545253.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_phpExtras.3a545253.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_plsql.c4805232.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_plsql.c4805232.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_powerquery.e89d0376.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_powerquery.e89d0376.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_powershell.77f12e72.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_powershell.77f12e72.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_processing.80772aab.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_processing.80772aab.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_prolog.97e5cc56.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_prolog.97e5cc56.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_promql.19574401.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_promql.19574401.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_properties.35522519.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_properties.35522519.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_protobuf.56ec3b7e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_protobuf.56ec3b7e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_psl.8ebe9791.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_psl.8ebe9791.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pug.d579f75c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pug.d579f75c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_puppet.d39a39ee.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_puppet.d39a39ee.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pure.94aa86b3.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_pure.94aa86b3.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_purebasic.edc05bbd.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_purebasic.edc05bbd.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_purescript.e0467141.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_purescript.e0467141.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_python.925670ec.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_python.925670ec.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_q.1efcbd38.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_q.1efcbd38.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_qml.60a3076c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_qml.60a3076c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_qore.3ba09070.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_qore.3ba09070.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_qsharp.2cc9d9f3.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_qsharp.2cc9d9f3.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_r.c67cd2ae.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_r.c67cd2ae.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_racket.753cacba.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_racket.753cacba.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_reason.aa4e59ac.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_reason.aa4e59ac.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_regex.af86145a.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_regex.af86145a.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_rego.f02a1247.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_rego.f02a1247.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_renpy.c30c8dcf.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_renpy.c30c8dcf.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_rest.61c86665.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_rest.61c86665.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_rip.a6c0e277.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_rip.a6c0e277.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_roboconf.ee276d9b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_roboconf.ee276d9b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_robotframework.6b544595.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_robotframework.6b544595.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ruby.23ca0941.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_ruby.23ca0941.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_rust.e71280da.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_rust.e71280da.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sas.387f813d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sas.387f813d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sass.c2fd25f2.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sass.c2fd25f2.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_scala.67511470.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_scala.67511470.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_scheme.3b176b5b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_scheme.3b176b5b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_scss.df549a2a.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_scss.df549a2a.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_shellSession.945d15cc.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_shellSession.945d15cc.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_smali.5211dec5.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_smali.5211dec5.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_smalltalk.d163e812.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_smalltalk.d163e812.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_smarty.39322ef4.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_smarty.39322ef4.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sml.28438f3c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sml.28438f3c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_solidity.59b31243.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_solidity.59b31243.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_solutionFile.5f18b4a2.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_solutionFile.5f18b4a2.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_soy.d3f3ff0b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_soy.d3f3ff0b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sparql.4e6dc021.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sparql.4e6dc021.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_splunkSpl.1138e8d9.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_splunkSpl.1138e8d9.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sqf.69cd4c8d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sqf.69cd4c8d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sql.08890780.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_sql.08890780.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_squirrel.e70e2648.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_squirrel.e70e2648.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_stan.c6e2c5cc.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_stan.c6e2c5cc.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_stylus.ba406826.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_stylus.ba406826.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_swift.1fbe97bc.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_swift.1fbe97bc.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_systemd.beadda10.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_systemd.beadda10.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_t4Cs.c71b771e.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_t4Cs.c71b771e.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_t4Templating.7932bd1c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_t4Templating.7932bd1c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_t4Vb.b5d48d85.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_t4Vb.b5d48d85.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tap.8485edd2.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tap.8485edd2.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tcl.8e60a29c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tcl.8e60a29c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_textile.94e36cdf.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_textile.94e36cdf.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_toml.d425b5b5.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_toml.d425b5b5.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tremor.4b29f41c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tremor.4b29f41c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tsx.a32466f3.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tsx.a32466f3.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tt2.381919a4.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_tt2.381919a4.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_turtle.34819f3c.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_turtle.34819f3c.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_twig.147c763f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_twig.147c763f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_typescript.fb01f157.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_typescript.fb01f157.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_typoscript.96ec3e92.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_typoscript.96ec3e92.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_unrealscript.14ef3dfa.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_unrealscript.14ef3dfa.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_uorazor.9a518174.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_uorazor.9a518174.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_uri.992356d2.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_uri.992356d2.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_v.5e01723d.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_v.5e01723d.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_vala.987292da.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_vala.987292da.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_vbnet.9ecb408b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_vbnet.9ecb408b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_velocity.7a76a0e3.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_velocity.7a76a0e3.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_verilog.5ff0845b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_verilog.5ff0845b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_vhdl.50fa563a.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_vhdl.50fa563a.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_vim.efb9f86b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_vim.efb9f86b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_visualBasic.79de8f6b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_visualBasic.79de8f6b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_warpscript.90bf10e8.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_warpscript.90bf10e8.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_wasm.89aeb2a1.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_wasm.89aeb2a1.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_webIdl.6d6a4c8f.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_webIdl.6d6a4c8f.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_wiki.98a5d7a3.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_wiki.98a5d7a3.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_wolfram.31374c06.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_wolfram.31374c06.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_wren.850b0d47.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_wren.850b0d47.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_xeora.1ea8b0c5.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_xeora.1ea8b0c5.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_xmlDoc.26ecedcb.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_xmlDoc.26ecedcb.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_xojo.8a6e77dd.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_xojo.8a6e77dd.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_xquery.6590358b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_xquery.6590358b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_yaml.144aac25.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_yaml.144aac25.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_yang.7905aac0.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_yang.7905aac0.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_zig.71aa2d4b.chunk.js
|
||||
build/static/js/react-syntax-highlighter_languages_refractor_zig.71aa2d4b.chunk.js.map
|
||||
build/static/js/react-syntax-highlighter/refractor-core-import.df24ca2e.chunk.js
|
||||
build/static/js/react-syntax-highlighter/refractor-core-import.df24ca2e.chunk.js.LICENSE.txt
|
||||
build/static/js/react-syntax-highlighter/refractor-core-import.df24ca2e.chunk.js.map
|
||||
build/static/media/logo.65c2273c08a6841a1dc427507f040724.svg
|
||||
build/static/media/roboto-latin-100.a45108d3b34af91f9113.woff
|
||||
build/static/media/roboto-latin-100.c2aa4ab115bf9c6057cb.woff2
|
||||
build/static/media/roboto-latin-100italic.7f839a8652da29745ce4.woff2
|
||||
build/static/media/roboto-latin-100italic.451d4e559d6f57cdf6a1.woff
|
||||
build/static/media/roboto-latin-300.37a7069dc30fc663c878.woff2
|
||||
build/static/media/roboto-latin-300.865f928cbabcc9f8f2b5.woff
|
||||
build/static/media/roboto-latin-300italic.bd5b7a13f2c52b531a2a.woff
|
||||
build/static/media/roboto-latin-300italic.c64e7e354c88e613c77c.woff2
|
||||
build/static/media/roboto-latin-400.49ae34d4cc6b98c00c69.woff
|
||||
build/static/media/roboto-latin-400.176f8f5bd5f02b3abfcf.woff2
|
||||
build/static/media/roboto-latin-400italic.b1d9d9904bfca8802a63.woff
|
||||
build/static/media/roboto-latin-400italic.d022bc70dc1bf7b3425d.woff2
|
||||
build/static/media/roboto-latin-500.cea99d3e3e13a3a599a0.woff
|
||||
build/static/media/roboto-latin-500.f5b74d7ffcdf85b9dd60.woff2
|
||||
build/static/media/roboto-latin-500italic.0d8bb5b3ee5f5dac9e44.woff2
|
||||
build/static/media/roboto-latin-500italic.18d00f739ff1e1c52db1.woff
|
||||
build/static/media/roboto-latin-700.2267169ee7270a22a963.woff
|
||||
build/static/media/roboto-latin-700.c18ee39fb002ad58b6dc.woff2
|
||||
build/static/media/roboto-latin-700italic.7d8125ff7f707231fd89.woff2
|
||||
build/static/media/roboto-latin-700italic.9360531f9bb817f917f0.woff
|
||||
build/static/media/roboto-latin-900.870c8c1486f76054301a.woff2
|
||||
build/static/media/roboto-latin-900.bac8362e7a6ea60b6983.woff
|
||||
build/static/media/roboto-latin-900italic.c20d916c1a1b094c1cec.woff
|
||||
build/static/media/roboto-latin-900italic.cb5ad999740e9d8a8bd1.woff2
|
||||
|
3
.vim/coc-settings.json
Normal file
3
.vim/coc-settings.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"eslint.experimental.useFlatConfig": false
|
||||
}
|
58
README.md
58
README.md
@ -125,30 +125,42 @@ As well as the following [standard `<iframe />` attributes](https://developer.mo
|
||||
- `referrerpolicy`
|
||||
- `sandbox`
|
||||
|
||||
## Available Scripts
|
||||
## Developing Chitchatter
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Presently Chitchatter can only be developed on \*NIX systems such as Linux and macOS. If you are using Windows, you can use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) to set up a Linux environment.
|
||||
|
||||
To make changes to Chitchatter, clone the source code from GitHub. Ensure you have [Node and NPM](https://nodejs.org) installed. Then in the project directory, run:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
This will install all of the dependencies.
|
||||
|
||||
### Available Scripts
|
||||
|
||||
Note: make sure to run 'npm install' after cloning the repo.
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm dev`
|
||||
#### `npm dev`
|
||||
|
||||
Runs the entire stack (client + WebTorrent tracker) locally.
|
||||
|
||||
### `npm start`
|
||||
#### `npm start`
|
||||
|
||||
Runs the front end app in the development mode. Uses public WebTorrent trackers.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
Runs the front end app in the development mode. Uses public WebTorrent trackers. Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes. You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
#### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode. See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
Launches the test runner in the interactive watch mode.
|
||||
|
||||
### `npm run build`
|
||||
#### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
Builds the app for production to the `dist` folder. It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.
|
||||
|
||||
@ -160,16 +172,36 @@ RemnantChat is designed to be forked and self-hosted. If you would like to chang
|
||||
|
||||
RemnantChat peer connections are bound to the instance's domain. So, a user of RemnantChat at https://remnant.chat/ would not be able to connect to a user of a RemnantChat instance on another domain.
|
||||
|
||||
#### Necessary steps after forking
|
||||
|
||||
Assuming you are hosting RemnantChat on [GitHub Pages](https://pages.github.com/):
|
||||
|
||||
1. Change the [`homepage` property in `package.json`](https://github.com/jeremyckahn/RemnantChat/blob/1ea67e2c3a45115e054ebfe3457f2c3572c6213b/package.json#L4) to whatever URL your RemnantChat instance will be hosted from. This will be something like `https://github_user_or_org_name.github.io/RemnantChat/`.
|
||||
2. Define a [`DEPLOY_KEY` GitHub Action secret](https://github.com/jeremyckahn/RemnantChat/blob/e2bac732cf1288f7b5d0bec151098f18e8b1d0d6/.github/workflows/deploy.yml#L28-L31) (at `https://github.com/github_user_or_org_name/RemnantChat/settings/secrets/actions`). See the docs for [`peaceiris/actions-gh-pages`](https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-set-ssh-private-key-deploy_key) for more information.
|
||||
> > > > > > > 6003270 (Main: Begin derived app rewrite into remnantchat.)
|
||||
|
||||
#### Deployment
|
||||
|
||||
##### On GitHub
|
||||
|
||||
# When hosted on GitHub Pages and the configuration above has been done, the Production environment is updated when the remote `main` branch is updated (once GitHub Actions are enabled).
|
||||
|
||||
#### Deployment After Forking
|
||||
|
||||
> > > > > > > 811c35b (main: prelim update to documention.)
|
||||
|
||||
##### On non-GitHub hosts
|
||||
|
||||
Build the app with `PUBLIC_URL="https://your-domain-here.com" npm run build`, and then serve the `build` directory. Any static file serving solution should work provided it is using a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).
|
||||
Build the app with `npm run build`, and then serve the `dist` directory. Any static file serving solution should work provided it is using a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).
|
||||
|
||||
#### Runtime configuration
|
||||
|
||||
Explore the files in `src/config` to modify pairing and relay server configuration.
|
||||
|
||||
#### Theme customization
|
||||
|
||||
Chitchatter utilizes the [MUI component library](https://mui.com/) which is [themeable](https://mui.com/material-ui/customization/theming/). You can customize Chitchatter's look and feel by modifying [the shell theme definition](https://github.com/jeremyckahn/chitchatter/blob/dc78137702bb9d6bf1be289e469e080cd7d5dc8b/src/components/Shell/useShellTheme.ts#L11-L18).
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you run into any issues with a custom RemnantChat installation, first ensure that you are using [the latest version of the code](https://githaven.org/shiloh/RemnantChat/tree/main).
|
||||
@ -190,7 +222,11 @@ RemnantChat works on iOS Safari, but browser-level bugs often prevent peers from
|
||||
|
||||
check your `about:config` settings and ensure that `media.peerconnection.enabled` is **enabled**.
|
||||
|
||||
##### Security
|
||||
#### Offered files can't be downloaded from peers
|
||||
|
||||
Chitchatter uses [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js) to facilitate large file transfers. Download managers such as [FDM](https://www.freedownloadmanager.org/) are [known to interfere with StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js/issues/325), so it is recommended to disable such download managers when trying to receive files.
|
||||
|
||||
### Security
|
||||
|
||||
### Contributors
|
||||
|
||||
|
@ -1,26 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="A peer-to-peer chat app that is serverless, decentralized, and ephemeral"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="apple-touch-icon" href="/logo192.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.
|
||||
Notice the use of 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
|
||||
Unlike "/favicon.ico" or "favicon.ico", "/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`.
|
||||
-->
|
||||
@ -69,5 +64,6 @@
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
91
manifest.ts
Normal file
91
manifest.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { ManifestOptions } from 'vite-plugin-pwa'
|
||||
|
||||
export const manifest: Partial<ManifestOptions> = {
|
||||
short_name: 'Chitchatter',
|
||||
name: 'Chitchatter',
|
||||
description:
|
||||
'This is a communication tool that is free, open source, and designed for simplicity and security. All communication between you and your online peers is encrypted. There is no trace of your conversation once you leave.',
|
||||
icons: [
|
||||
{
|
||||
src: 'favicon.ico',
|
||||
sizes: '64x64 32x32 24x24 16x16',
|
||||
type: 'image/x-icon',
|
||||
},
|
||||
{
|
||||
src: 'logo192.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192',
|
||||
},
|
||||
{
|
||||
src: 'logo512.png',
|
||||
type: 'image/png',
|
||||
sizes: '512x512',
|
||||
},
|
||||
],
|
||||
start_url: './',
|
||||
display: 'fullscreen',
|
||||
theme_color: '#000000',
|
||||
background_color: '#222222',
|
||||
screenshots: [
|
||||
{
|
||||
src: 'screenshots/home-desktop.png',
|
||||
sizes: '2160x1620',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/public-room-desktop.png',
|
||||
sizes: '2160x1620',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/public-room-desktop-with-video.png',
|
||||
sizes: '2160x1620',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/home-mobile-dark.png',
|
||||
sizes: '750x1334',
|
||||
type: 'image/png',
|
||||
form_factor: 'narrow',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/home-mobile-light.png',
|
||||
sizes: '750x1334',
|
||||
type: 'image/png',
|
||||
form_factor: 'narrow',
|
||||
},
|
||||
{
|
||||
src: 'screenshots/public-room-mobile.png',
|
||||
sizes: '750x1334',
|
||||
type: 'image/png',
|
||||
form_factor: 'narrow',
|
||||
},
|
||||
],
|
||||
|
||||
shortcuts: [
|
||||
{
|
||||
name: 'About',
|
||||
url: './about',
|
||||
icons: [
|
||||
{
|
||||
src: 'logo512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Disclaimer',
|
||||
url: './disclaimer',
|
||||
icons: [
|
||||
{
|
||||
src: 'logo512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
42350
package-lock.json
generated
42350
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
86
package.json
86
package.json
@ -4,78 +4,73 @@
|
||||
"homepage": "https://remnant.chat",
|
||||
"author": "Shiloh",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.0",
|
||||
"@emotion/styled": "^11.10.0",
|
||||
"@mui/icons-material": "^5.8.4",
|
||||
"@mui/material": "^5.14.12",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/icons-material": "^5.16.7",
|
||||
"@mui/material": "^5.16.7",
|
||||
"@react-hook/debounce": "^4.0.0",
|
||||
"@react-hook/window-size": "^3.1.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^28.1.6",
|
||||
"@types/node": "^18.18.4",
|
||||
"@types/react": "^18.2.25",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^18.19.55",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"detectincognitojs": "^1.1.2",
|
||||
"detectincognitojs": "^1.3.5",
|
||||
"fast-memoize": "^2.5.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"fun-animal-names": "^0.1.1",
|
||||
"idb-chunk-store": "^1.0.1",
|
||||
"localforage": "^1.10.0",
|
||||
"modern-normalize": "^2.0.0",
|
||||
"mui-markdown": "^0.5.5",
|
||||
"querystring": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-git-info": "^2.0.1",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-qrcode-logo": "^2.8.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-qrcode-logo": "^2.10.0",
|
||||
"react-router-dom": "^6.27.0",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-youtube": "^10.1.0",
|
||||
"readable-web-to-node-stream": "^3.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sass": "^1.54.3",
|
||||
"sdp": "^3.2.0",
|
||||
"secure-file-transfer": "^0.0.7",
|
||||
"secure-file-transfer": "^0.0.8",
|
||||
"streamsaver": "^2.0.6",
|
||||
"trystero": "^0.15.0",
|
||||
"trystero": "^0.20.0",
|
||||
"typeface-public-sans": "^1.1.13",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript": "^5.6.3",
|
||||
"uuid": "^8.3.2",
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"web-vitals": "^2.1.4",
|
||||
"webrtc-adapter": "^8.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"start": "cross-env REACT_APP_HOMEPAGE=$(npm pkg get homepage) react-scripts start",
|
||||
"start": "cross-env VITE_HOMEPAGE=$(npm pkg get homepage) vite --port 3000",
|
||||
"start:tracker": "bittorrent-tracker",
|
||||
"start:streamsaver": "serve -p 3015 node_modules/streamsaver",
|
||||
"dev": "mprocs \"npx cross-env REACT_APP_TRACKER_URL=\"ws://localhost:8000\" REACT_APP_STREAMSAVER_URL=\"http://localhost:3015/mitm.html\" npm run start\" \"npm run start:tracker\" \"npm run start:streamsaver\"",
|
||||
"dev": "mprocs \"npx cross-env VITE_TRACKER_URL=\"ws://localhost:8000\" VITE_STREAMSAVER_URL=\"http://localhost:3015/mitm.html\" npm run start\" \"npm run start:tracker\" \"npm run start:streamsaver\"",
|
||||
"build": "npm run build:app && npm run build:sdk",
|
||||
"build:app": "cross-env REACT_APP_HOMEPAGE=$(npm pkg get homepage) react-scripts build",
|
||||
"build:sdk": "parcel build sdk/sdk.ts --dist-dir build --no-content-hash",
|
||||
"build:app": "cross-env VITE_HOMEPAGE=$(npm pkg get homepage) vite build",
|
||||
"build:sdk": "parcel build sdk/sdk.ts --no-content-hash",
|
||||
"build:sdk:watch": "nodemon --exec \"npm run build:sdk\"",
|
||||
"test": "react-scripts test",
|
||||
"test": "vitest",
|
||||
"prepare": "husky install",
|
||||
"prettier": "prettier 'src/**/*.js' --write",
|
||||
"prettier": "prettier \"**/*.{ts,tsx}\" --write",
|
||||
"lint": "eslint src --max-warnings=0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": "18.12.1",
|
||||
"npm": "8.19.2"
|
||||
"node": "20.12.1",
|
||||
"npm": "10.5.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@ -92,6 +87,8 @@
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/react": "^18.2.72",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-file-reader-input": "^2.0.4",
|
||||
"@types/react-syntax-highlighter": "^15.5.5",
|
||||
"@types/streamsaver": "^2.0.1",
|
||||
@ -99,28 +96,34 @@
|
||||
"@types/webtorrent": "^0.109.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.33.0",
|
||||
"@typescript-eslint/parser": "^5.33.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.8",
|
||||
"bittorrent-tracker": "^9.19.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^8.0.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"mprocs": "^0.6.4",
|
||||
"nodemon": "^3.0.1",
|
||||
"parcel": "^2.10.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"prettier": "^3.2.5",
|
||||
"pretty-quick": "^4.0.0",
|
||||
"process": "^0.11.10",
|
||||
"serve": "^14.1.2",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"tailwindcss": "^3.1.8",
|
||||
"url": "^0.11.0",
|
||||
"util": "^0.12.5"
|
||||
"util": "^0.12.5",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-node-polyfills": "^0.19.0",
|
||||
"vite-plugin-pwa": "^0.19.2",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "^1.3.1"
|
||||
},
|
||||
"overrides": {
|
||||
"ipfs-core": "npm:dry-uninstall",
|
||||
@ -134,10 +137,5 @@
|
||||
"resolve-url-loader": {
|
||||
"postcss": "8.4.31"
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!trystero)/"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
// NOTE: This file is a shim to enable the Bootstrap component to be
|
||||
// lazy-loaded.
|
||||
import Bootstrap from './Bootstrap.tsx'
|
||||
export default Bootstrap
|
@ -1,9 +1,10 @@
|
||||
import { vi } from 'vitest'
|
||||
import { act, render } from '@testing-library/react'
|
||||
import localforage from 'localforage'
|
||||
import persistedStorage from 'localforage'
|
||||
|
||||
import { PersistedStorageKeys } from 'models/storage'
|
||||
import {
|
||||
mockSerializationService,
|
||||
mockSerialization,
|
||||
mockSerializedPrivateKey,
|
||||
mockSerializedPublicKey,
|
||||
} from 'test-utils/mocks/mockSerializationService'
|
||||
@ -11,30 +12,16 @@ import { userSettingsStubFactory } from 'test-utils/stubs/userSettings'
|
||||
|
||||
import Bootstrap, { BootstrapProps } from './Bootstrap'
|
||||
|
||||
const mockPersistedStorage =
|
||||
jest.createMockFromModule<jest.Mock<typeof localforage>>('localforage')
|
||||
|
||||
const mockGetItem = jest.fn()
|
||||
const mockSetItem = jest.fn()
|
||||
vi.mock('localforage')
|
||||
|
||||
const userSettingsStub = userSettingsStubFactory()
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetItem.mockImplementation(() => Promise.resolve(null))
|
||||
mockSetItem.mockImplementation((data: any) => Promise.resolve(data))
|
||||
})
|
||||
|
||||
const renderBootstrap = async (overrides: Partial<BootstrapProps> = {}) => {
|
||||
Object.assign(mockPersistedStorage, {
|
||||
getItem: mockGetItem,
|
||||
setItem: mockSetItem,
|
||||
})
|
||||
|
||||
render(
|
||||
<Bootstrap
|
||||
persistedStorage={mockPersistedStorage as any as typeof localforage}
|
||||
persistedStorage={persistedStorage}
|
||||
initialUserSettings={userSettingsStub}
|
||||
serializationService={mockSerializationService}
|
||||
serializationService={mockSerialization}
|
||||
{...overrides}
|
||||
/>
|
||||
)
|
||||
@ -51,7 +38,9 @@ test('renders', async () => {
|
||||
|
||||
test('checks persistedStorage for user settings', async () => {
|
||||
await renderBootstrap()
|
||||
expect(mockGetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS)
|
||||
expect(persistedStorage.getItem).toHaveBeenCalledWith(
|
||||
PersistedStorageKeys.USER_SETTINGS
|
||||
)
|
||||
})
|
||||
|
||||
test('updates persisted user settings', async () => {
|
||||
@ -59,7 +48,9 @@ test('updates persisted user settings', async () => {
|
||||
initialUserSettings: { ...userSettingsStub, userId: 'abc123' },
|
||||
})
|
||||
|
||||
expect(mockSetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS, {
|
||||
expect(persistedStorage.setItem).toHaveBeenCalledWith(
|
||||
PersistedStorageKeys.USER_SETTINGS,
|
||||
{
|
||||
colorMode: 'dark',
|
||||
userId: 'abc123',
|
||||
customUsername: '',
|
||||
@ -68,5 +59,6 @@ test('updates persisted user settings', async () => {
|
||||
showActiveTypingStatus: true,
|
||||
publicKey: mockSerializedPublicKey,
|
||||
privateKey: mockSerializedPrivateKey,
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
Navigate,
|
||||
} from 'react-router-dom'
|
||||
import localforage from 'localforage'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
|
||||
import * as serviceWorkerRegistration from 'serviceWorkerRegistration'
|
||||
import { StorageContext } from 'contexts/StorageContext'
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import { homepageUrl, routes } from 'config/routes'
|
||||
@ -27,15 +27,12 @@ import {
|
||||
PostMessageEvent,
|
||||
PostMessageEventName,
|
||||
} from 'models/sdk'
|
||||
import {
|
||||
serializationService as serializationServiceInstance,
|
||||
SerializedUserSettings,
|
||||
} from 'services/Serialization'
|
||||
import { serialization, SerializedUserSettings } from 'services/Serialization'
|
||||
|
||||
export interface BootstrapProps {
|
||||
persistedStorage?: typeof localforage
|
||||
initialUserSettings: UserSettings
|
||||
serializationService?: typeof serializationServiceInstance
|
||||
serializationService?: typeof serialization
|
||||
}
|
||||
|
||||
const configListenerTimeout = 3000
|
||||
@ -82,7 +79,7 @@ const Bootstrap = ({
|
||||
description: 'Persisted settings data for remnantchat',
|
||||
}),
|
||||
initialUserSettings,
|
||||
serializationService = serializationServiceInstance,
|
||||
serializationService = serialization,
|
||||
}: BootstrapProps) => {
|
||||
const queryParams = useMemo(
|
||||
() => new URLSearchParams(window.location.search),
|
||||
@ -90,16 +87,11 @@ const Bootstrap = ({
|
||||
)
|
||||
|
||||
const [persistedStorage] = useState(persistedStorageProp)
|
||||
const [appNeedsUpdate, setAppNeedsUpdate] = useState(false)
|
||||
const [hasLoadedSettings, setHasLoadedSettings] = useState(false)
|
||||
const [userSettings, setUserSettings] =
|
||||
useState<UserSettings>(initialUserSettings)
|
||||
const { userId } = userSettings
|
||||
|
||||
const handleServiceWorkerUpdate = () => {
|
||||
setAppNeedsUpdate(true)
|
||||
}
|
||||
|
||||
const persistUserSettings = useCallback(
|
||||
async (newUserSettings: UserSettings) => {
|
||||
if (queryParams.has(QueryParamKeys.IS_EMBEDDED)) {
|
||||
@ -117,9 +109,9 @@ const Bootstrap = ({
|
||||
[persistedStorageProp, queryParams, serializationService, userSettings]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
serviceWorkerRegistration.register({ onUpdate: handleServiceWorkerUpdate })
|
||||
}, [])
|
||||
const {
|
||||
needRefresh: [appNeedsUpdate],
|
||||
} = useRegisterSW()
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
|
@ -3,7 +3,7 @@ import Box from '@mui/material/Box'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import { encryption } from 'services/Encryption'
|
||||
import {
|
||||
EnvironmentUnsupportedDialog,
|
||||
isEnvironmentSupported,
|
||||
@ -13,8 +13,7 @@ import { ColorMode, UserSettings } from 'models/settings'
|
||||
|
||||
import type { BootstrapProps } from './Bootstrap'
|
||||
|
||||
// @ts-expect-error
|
||||
const Bootstrap = lazy(() => import('./Bootstrap.js'))
|
||||
const Bootstrap = lazy(() => import('./Bootstrap'))
|
||||
|
||||
export interface InitProps extends Omit<BootstrapProps, 'initialUserSettings'> {
|
||||
getUuid?: typeof uuid
|
||||
@ -32,8 +31,7 @@ const Init = ({ getUuid = uuid, ...props }: InitProps) => {
|
||||
if (userSettings !== null) return
|
||||
|
||||
try {
|
||||
const { publicKey, privateKey } =
|
||||
await encryptionService.generateKeyPair()
|
||||
const { publicKey, privateKey } = await encryption.generateKeyPair()
|
||||
|
||||
setUserSettings({
|
||||
userId: getUuid(),
|
||||
|
@ -1,16 +1,25 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import Slider from '@mui/material/Slider'
|
||||
import Box from '@mui/material/Box'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||
import VolumeUp from '@mui/icons-material/VolumeUp'
|
||||
import VolumeDown from '@mui/icons-material/VolumeDown'
|
||||
import VolumeMute from '@mui/icons-material/VolumeMute'
|
||||
import VolumeUpIcon from '@mui/icons-material/VolumeUp'
|
||||
import VolumeDownIcon from '@mui/icons-material/VolumeDown'
|
||||
import VolumeMuteIcon from '@mui/icons-material/VolumeMute'
|
||||
import MicIcon from '@mui/icons-material/Mic'
|
||||
import LaptopWindowsIcon from '@mui/icons-material/LaptopWindows'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import { AudioChannelName } from 'models/chat'
|
||||
|
||||
interface AudioVolumeProps {
|
||||
audioEl: HTMLAudioElement
|
||||
audioChannelName: AudioChannelName
|
||||
}
|
||||
|
||||
export const AudioVolume = ({ audioEl }: AudioVolumeProps) => {
|
||||
export const AudioVolume = ({
|
||||
audioEl,
|
||||
audioChannelName,
|
||||
}: AudioVolumeProps) => {
|
||||
const [audioVolume, setAudioVolume] = useState(audioEl.volume)
|
||||
|
||||
useEffect(() => {
|
||||
@ -32,19 +41,39 @@ export const AudioVolume = ({ audioEl }: AudioVolumeProps) => {
|
||||
|
||||
const formatLabelValue = () => `${Math.round(audioVolume * 100)}%`
|
||||
|
||||
let VolumeIcon = VolumeUp
|
||||
let VolumeIcon = VolumeUpIcon
|
||||
|
||||
if (audioVolume === 0) {
|
||||
VolumeIcon = VolumeMute
|
||||
VolumeIcon = VolumeMuteIcon
|
||||
} else if (audioVolume < 0.5) {
|
||||
VolumeIcon = VolumeDown
|
||||
VolumeIcon = VolumeDownIcon
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', pt: 1, pr: 3, alignItems: 'center' }}>
|
||||
<ListItemIcon>
|
||||
<VolumeIcon sx={{ cursor: 'pointer' }} onClick={handleIconClick} />
|
||||
<Paper
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
mt: 1.5,
|
||||
pl: 2,
|
||||
pr: 3,
|
||||
py: 1,
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ cursor: 'pointer' }} onClick={handleIconClick}>
|
||||
<VolumeIcon fontSize="small" />
|
||||
{audioChannelName === AudioChannelName.MICROPHONE && (
|
||||
<Tooltip title="Their microphone volume">
|
||||
<MicIcon fontSize="small" sx={{ ml: 1, mr: 2 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{audioChannelName === AudioChannelName.SCREEN_SHARE && (
|
||||
<Tooltip title="Their screen's volume">
|
||||
<LaptopWindowsIcon fontSize="small" sx={{ ml: 1, mr: 2 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<Box display="flex" width={1}>
|
||||
<Slider
|
||||
aria-label="Volume"
|
||||
getAriaValueText={formatLabelValue}
|
||||
@ -54,5 +83,6 @@ export const AudioVolume = ({ audioEl }: AudioVolumeProps) => {
|
||||
value={audioVolume * 100}
|
||||
></Slider>
|
||||
</Box>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { HTMLAttributes, useRef, useEffect, useState, useContext } from 'react'
|
||||
import cx from 'classnames'
|
||||
import Box from '@mui/material/Box'
|
||||
import useTheme from '@mui/material/styles/useTheme'
|
||||
|
||||
@ -12,11 +11,7 @@ export interface ChatTranscriptProps extends HTMLAttributes<HTMLDivElement> {
|
||||
userId: string
|
||||
}
|
||||
|
||||
export const ChatTranscript = ({
|
||||
className,
|
||||
messageLog,
|
||||
userId,
|
||||
}: ChatTranscriptProps) => {
|
||||
export const ChatTranscript = ({ messageLog, userId }: ChatTranscriptProps) => {
|
||||
const { showRoomControls } = useContext(ShellContext)
|
||||
const theme = useTheme()
|
||||
const boxRef = useRef<HTMLDivElement>(null)
|
||||
@ -66,11 +61,13 @@ export const ChatTranscript = ({
|
||||
return (
|
||||
<Box
|
||||
ref={boxRef}
|
||||
className={cx('ChatTranscript', className)}
|
||||
className="ChatTranscript"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
py: transcriptMinPadding,
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
pb: transcriptMinPadding,
|
||||
pt: showRoomControls ? theme.spacing(10) : theme.spacing(2),
|
||||
px: `max(${transcriptPaddingX}, ${transcriptMinPadding})`,
|
||||
transition: `padding-top ${theme.transitions.duration.short}ms ${theme.transitions.easing.easeInOut}`,
|
||||
|
9
src/components/Elements/index.tsx
Normal file
9
src/components/Elements/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import styled from '@mui/material/styles/styled'
|
||||
|
||||
// NOTE: These components are defined to enable raw DOM elements to be styled
|
||||
// with MUI's sx prop.
|
||||
// @see https://mui.com/system/styled/
|
||||
// @see https://mui.com/system/getting-started/the-sx-prop/
|
||||
export const Form = styled('form')({})
|
||||
export const Input = styled('input')({})
|
||||
export const Main = styled('main')({})
|
@ -1,9 +1,10 @@
|
||||
import { useContext, useEffect, useRef, useState } from 'react'
|
||||
import { TorrentFile } from 'webtorrent'
|
||||
import Box from '@mui/material/Box'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
import { fileTransfer } from 'services/FileTransfer'
|
||||
import { fileTransfer } from 'lib/FileTransfer'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
|
||||
type TorrentFiles = Awaited<ReturnType<typeof fileTransfer.download>>
|
||||
@ -16,34 +17,65 @@ interface InlineFileProps {
|
||||
file: TorrentFile
|
||||
}
|
||||
|
||||
// NOTE: These filename extensions are copied from render-media, the upstream
|
||||
// library used to embed media files:
|
||||
// https://github.com/feross/render-media/blob/a445b2ab90fcd4a248552d32027b2bc6a02600c8/index.js#L15-L72
|
||||
const supportedImageExtensions = [
|
||||
'.bmp',
|
||||
'.gif',
|
||||
'.jpeg',
|
||||
'.jpg',
|
||||
'.png',
|
||||
'.svg',
|
||||
]
|
||||
|
||||
const supportedAudioExtensions = ['.aac', '.oga', '.ogg', '.wav', '.flac']
|
||||
|
||||
const supportedMediaExtensions = [
|
||||
...supportedImageExtensions,
|
||||
...supportedAudioExtensions,
|
||||
]
|
||||
|
||||
export const InlineFile = ({ file }: InlineFileProps) => {
|
||||
const containerRef = useRef(null)
|
||||
const [didRenderingMediaFail, setDidRenderingMediaFail] = useState(false)
|
||||
const [isMediaSupported, setIsMediaSupported] = useState(true)
|
||||
const shellContext = useContext(ShellContext)
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
const { current: container } = containerRef
|
||||
|
||||
if (!container) return
|
||||
|
||||
const { name } = file
|
||||
const fileNameExtension = name.split('.').pop() ?? ''
|
||||
|
||||
if (!supportedMediaExtensions.includes(`.${fileNameExtension}`)) {
|
||||
setIsMediaSupported(false)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
file.appendTo(container)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setDidRenderingMediaFail(true)
|
||||
}
|
||||
})()
|
||||
}, [file, containerRef, shellContext.roomId])
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<Box ref={containerRef} sx={{ '& img': { maxWidth: '100%' } }}>
|
||||
{!isMediaSupported && (
|
||||
<Typography sx={{ fontStyle: 'italic' }}>
|
||||
Media preview not supported
|
||||
</Typography>
|
||||
)}
|
||||
{didRenderingMediaFail && (
|
||||
<Typography sx={{ fontStyle: 'italic' }}>
|
||||
Media failed to render
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
.Message
|
||||
pre
|
||||
overflow: auto
|
@ -5,20 +5,10 @@ import Box from '@mui/material/Box'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Typography, { TypographyProps } from '@mui/material/Typography'
|
||||
import Link, { LinkProps } from '@mui/material/Link'
|
||||
import styled from '@mui/material/styles/styled'
|
||||
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
|
||||
// These imports need to be ts-ignored to prevent spurious errors that look
|
||||
// like this:
|
||||
//
|
||||
// Module 'react-markdown' cannot be imported using this construct. The
|
||||
// specifier only resolves to an ES module, which cannot be imported
|
||||
// synchronously. Use dynamic import instead. (tsserver 1471)
|
||||
//
|
||||
// @ts-ignore
|
||||
import Markdown from 'react-markdown'
|
||||
// @ts-ignore
|
||||
import { CodeProps } from 'react-markdown/lib/ast-to-react'
|
||||
// @ts-ignore
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
import {
|
||||
@ -32,7 +22,7 @@ import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock'
|
||||
|
||||
import { InlineMedia } from './InlineMedia'
|
||||
|
||||
import './Message.sass'
|
||||
const StyledMarkdown = styled(Markdown)({})
|
||||
|
||||
export interface MessageProps {
|
||||
message: IMessage | I_InlineMedia
|
||||
@ -154,13 +144,26 @@ export const Message = ({ message, showAuthor, userId }: MessageProps) => {
|
||||
) : isYouTubeLink(message) ? (
|
||||
<YouTube videoId={getYouTubeVideoId(message.text)} />
|
||||
) : (
|
||||
<Markdown
|
||||
<StyledMarkdown
|
||||
components={componentMap}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
linkTarget="_blank"
|
||||
sx={{
|
||||
'& pre': {
|
||||
overflow: 'auto',
|
||||
},
|
||||
'& ol': {
|
||||
pl: 2,
|
||||
listStyleType: 'decimal',
|
||||
},
|
||||
'& ul': {
|
||||
pl: 2,
|
||||
listStyleType: 'disc',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{message.text}
|
||||
</Markdown>
|
||||
</StyledMarkdown>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
@ -14,7 +14,7 @@ import ArrowUpward from '@mui/icons-material/ArrowUpward'
|
||||
|
||||
import { messageCharacterSizeLimit } from 'config/messaging'
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import classNames from 'classnames'
|
||||
import { Form } from 'components/Elements'
|
||||
|
||||
interface MessageFormProps {
|
||||
onMessageSubmit: (message: string) => void
|
||||
@ -76,12 +76,17 @@ export const MessageForm = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
<Form
|
||||
onSubmit={handleMessageSubmit}
|
||||
className={classNames({
|
||||
'pt-4 px-4': showActiveTypingStatus,
|
||||
'p-4': !showActiveTypingStatus,
|
||||
})}
|
||||
sx={{
|
||||
...(showActiveTypingStatus && {
|
||||
pt: 2,
|
||||
px: 2,
|
||||
}),
|
||||
...(!showActiveTypingStatus && {
|
||||
p: 2,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={2}>
|
||||
<FormControl fullWidth>
|
||||
@ -110,6 +115,6 @@ export const MessageForm = ({
|
||||
<ArrowUpward />
|
||||
</Fab>
|
||||
</Stack>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
@ -1,11 +1,19 @@
|
||||
import { ChangeEvent, useState, SyntheticEvent } from 'react'
|
||||
import { ChangeEvent, useState, SyntheticEvent, useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Button from '@mui/material/Button'
|
||||
import InputAdornment from '@mui/material/InputAdornment'
|
||||
import IconButton from '@mui/material/IconButton'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Dialog from '@mui/material/Dialog'
|
||||
import DialogActions from '@mui/material/DialogActions'
|
||||
import DialogContent from '@mui/material/DialogContent'
|
||||
import DialogContentText from '@mui/material/DialogContentText'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
import Visibility from '@mui/icons-material/Visibility'
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
||||
|
||||
import { QueryParamKeys } from 'models/shell'
|
||||
|
||||
interface PasswordPromptProps {
|
||||
isOpen: boolean
|
||||
@ -17,16 +25,36 @@ export const PasswordPrompt = ({
|
||||
onPasswordEntered,
|
||||
}: PasswordPromptProps) => {
|
||||
const [password, setPassword] = useState('')
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => new URLSearchParams(window.location.search),
|
||||
[]
|
||||
)
|
||||
|
||||
const isEmbedded = queryParams.has(QueryParamKeys.IS_EMBEDDED)
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleFormSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
onPasswordEntered(password)
|
||||
}
|
||||
|
||||
const handleClickShowPassword = () => {
|
||||
setShowPassword(!showPassword)
|
||||
}
|
||||
|
||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setPassword(e.target.value)
|
||||
}
|
||||
|
||||
const handleGoBackClick = () => {
|
||||
navigate(-1)
|
||||
}
|
||||
|
||||
const passwordToggleLabel = showPassword ? 'Hide password' : 'Show password'
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen}>
|
||||
<form onSubmit={handleFormSubmit}>
|
||||
@ -38,7 +66,7 @@ export const PasswordPrompt = ({
|
||||
impossible to know if the password you enter will match the password
|
||||
entered by other peers.
|
||||
</DialogContentText>
|
||||
<DialogContentText>
|
||||
<DialogContentText sx={{ mb: 2 }}>
|
||||
If there is a mismatch, you will be in the room but be unable to
|
||||
connect to others. An error will not be shown.
|
||||
</DialogContentText>
|
||||
@ -47,14 +75,33 @@ export const PasswordPrompt = ({
|
||||
margin="dense"
|
||||
id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
fullWidth
|
||||
variant="standard"
|
||||
variant="outlined"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Tooltip title={passwordToggleLabel}>
|
||||
<IconButton
|
||||
aria-label={passwordToggleLabel}
|
||||
onClick={handleClickShowPassword}
|
||||
>
|
||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!isEmbedded && (
|
||||
<Button color="secondary" onClick={handleGoBackClick}>
|
||||
Go back
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" disabled={password.length === 0}>
|
||||
Submit
|
||||
</Button>
|
||||
|
@ -19,7 +19,7 @@ export const usePeerNameDisplay = () => {
|
||||
const getCustomUsername = (userId: string) =>
|
||||
isPeerSelf(userId)
|
||||
? selfCustomUsername
|
||||
: getPeer(userId)?.customUsername ?? ''
|
||||
: (getPeer(userId)?.customUsername ?? '')
|
||||
|
||||
const getFriendlyName = (userId: string) => {
|
||||
const customUsername = getCustomUsername(userId)
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
||||
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock'
|
||||
import { encryptionService } from 'services/Encryption/Encryption'
|
||||
import { encryption } from 'services/Encryption/Encryption'
|
||||
|
||||
interface PeerPublicKeyProps {
|
||||
publicKey: CryptoKey
|
||||
@ -13,7 +13,7 @@ export const PublicKey = ({ publicKey }: PeerPublicKeyProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
setPublicKeyString(await encryptionService.stringifyCryptoKey(publicKey))
|
||||
setPublicKeyString(await encryption.stringifyCryptoKey(publicKey))
|
||||
})()
|
||||
}, [publicKey])
|
||||
|
||||
|
@ -3,7 +3,7 @@ import Paper from '@mui/material/Paper'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
|
||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
import { VideoStreamType } from 'models/chat'
|
||||
import { StreamType } from 'models/chat'
|
||||
|
||||
import { SelectedPeerStream } from './RoomVideoDisplay'
|
||||
|
||||
@ -13,13 +13,13 @@ interface PeerVideoProps {
|
||||
numberOfVideos: number
|
||||
onVideoClick?: (
|
||||
userId: string,
|
||||
videoStreamType: VideoStreamType,
|
||||
streamType: StreamType,
|
||||
videoStream: MediaStream
|
||||
) => void
|
||||
selectedPeerStream: SelectedPeerStream | null
|
||||
userId: string
|
||||
videoStream: MediaStream
|
||||
videoStreamType: VideoStreamType
|
||||
streamType: StreamType
|
||||
}
|
||||
|
||||
// Adapted from https://www.geeksforgeeks.org/find-the-next-perfect-square-greater-than-a-given-number/
|
||||
@ -37,7 +37,7 @@ export const PeerVideo = ({
|
||||
userId,
|
||||
selectedPeerStream,
|
||||
videoStream,
|
||||
videoStreamType,
|
||||
streamType,
|
||||
}: PeerVideoProps) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
|
||||
@ -47,13 +47,14 @@ export const PeerVideo = ({
|
||||
|
||||
video.autoplay = true
|
||||
video.srcObject = videoStream
|
||||
video.muted = true
|
||||
}, [videoRef, videoStream])
|
||||
|
||||
const cols = Math.sqrt(nextPerfectSquare(numberOfVideos - 1))
|
||||
const rows = Math.ceil(numberOfVideos / cols)
|
||||
|
||||
const handleVideoClick = () => {
|
||||
onVideoClick?.(userId, videoStreamType, videoStream)
|
||||
onVideoClick?.(userId, streamType, videoStream)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { vi } from 'vitest'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { waitFor, render, screen } from '@testing-library/react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { MemoryRouter as Router, Route, Routes } from 'react-router-dom'
|
||||
|
||||
import { userSettingsContextStubFactory } from 'test-utils/stubs/settingsContext'
|
||||
import { mockEncryptionService } from 'test-utils/mocks/mockEncryptionService'
|
||||
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import { Time } from 'lib/Time'
|
||||
|
||||
import { Room, RoomProps } from './'
|
||||
|
||||
@ -17,13 +18,17 @@ const userSettingsStub = userSettingsContextStubFactory({
|
||||
userId: mockUserId,
|
||||
})
|
||||
|
||||
window.AudioContext = jest.fn().mockImplementation()
|
||||
const mockGetUuid = jest.fn()
|
||||
const mockMessagedSender = jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve([]))
|
||||
window.AudioContext = vi.fn().mockImplementation(() => {})
|
||||
const mockGetUuid = vi.fn()
|
||||
const mockMessagedSender = vi.fn().mockImplementation(() => Promise.resolve([]))
|
||||
|
||||
jest.mock('trystero', () => ({
|
||||
const mockTimeService = new Time()
|
||||
const mockNowTime = 1234
|
||||
mockTimeService.now = () => mockNowTime
|
||||
|
||||
vi.mock('../../lib/Audio')
|
||||
|
||||
vi.mock('trystero/torrent', () => ({
|
||||
joinRoom: () => ({
|
||||
makeAction: () => [mockMessagedSender, () => {}, () => {}],
|
||||
ping: () => Promise.resolve(0),
|
||||
@ -53,10 +58,14 @@ const RouteStub = ({ children }: PropsWithChildren) => {
|
||||
)
|
||||
}
|
||||
|
||||
jest.useFakeTimers().setSystemTime(100)
|
||||
|
||||
const RoomStub = (props: RoomProps) => {
|
||||
return <Room encryptionService={mockEncryptionService} {...props} />
|
||||
return (
|
||||
<Room
|
||||
encryptionService={mockEncryptionService}
|
||||
timeService={mockTimeService}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('Room', () => {
|
||||
@ -89,9 +98,7 @@ describe('Room', () => {
|
||||
const sendButton = screen.getByLabelText('Send')
|
||||
const textInput = screen.getByPlaceholderText('Your message')
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(textInput, 'hello')
|
||||
})
|
||||
await userEvent.type(textInput, 'hello')
|
||||
|
||||
expect(sendButton).not.toBeDisabled()
|
||||
})
|
||||
@ -106,13 +113,8 @@ describe('Room', () => {
|
||||
const sendButton = screen.getByLabelText('Send')
|
||||
const textInput = screen.getByPlaceholderText('Your message')
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(textInput, 'hello')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(sendButton)
|
||||
})
|
||||
await userEvent.type(textInput, 'hello')
|
||||
await userEvent.click(sendButton)
|
||||
|
||||
expect(textInput).toHaveValue('')
|
||||
})
|
||||
@ -131,18 +133,13 @@ describe('Room', () => {
|
||||
const sendButton = screen.getByLabelText('Send')
|
||||
const textInput = screen.getByPlaceholderText('Your message')
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.type(textInput, 'hello')
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
userEvent.click(sendButton)
|
||||
})
|
||||
await userEvent.type(textInput, 'hello')
|
||||
await userEvent.click(sendButton)
|
||||
|
||||
expect(mockMessagedSender).toHaveBeenCalledWith({
|
||||
authorId: mockUserId,
|
||||
text: 'hello',
|
||||
timeSent: 100,
|
||||
timeSent: mockNowTime,
|
||||
id: 'abc123',
|
||||
})
|
||||
})
|
||||
|
@ -8,11 +8,12 @@ import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { rtcConfig } from 'config/rtcConfig'
|
||||
import { trackerUrls } from 'config/trackerUrls'
|
||||
import { time } from 'lib/Time'
|
||||
import { RoomContext } from 'contexts/RoomContext'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { MessageForm } from 'components/MessageForm'
|
||||
import { ChatTranscript } from 'components/ChatTranscript'
|
||||
import { encryptionService as encryptionServiceInstance } from 'services/Encryption'
|
||||
import { encryption } from 'services/Encryption'
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
|
||||
import { useRoom } from './useRoom'
|
||||
@ -30,13 +31,15 @@ export interface RoomProps {
|
||||
password?: string
|
||||
roomId: string
|
||||
userId: string
|
||||
encryptionService?: typeof encryptionServiceInstance
|
||||
encryptionService?: typeof encryption
|
||||
timeService?: typeof time
|
||||
}
|
||||
|
||||
export function Room({
|
||||
appId = `${encodeURI(window.location.origin)}_${process.env.REACT_APP_NAME}`,
|
||||
appId = `${encodeURI(window.location.origin)}_${process.env.VITE_NAME}`,
|
||||
getUuid = uuid,
|
||||
encryptionService = encryptionServiceInstance,
|
||||
encryptionService = encryption,
|
||||
timeService = time,
|
||||
roomId,
|
||||
password,
|
||||
userId,
|
||||
@ -57,10 +60,10 @@ export function Room({
|
||||
} = useRoom(
|
||||
{
|
||||
appId,
|
||||
trackerUrls,
|
||||
relayUrls: trackerUrls,
|
||||
rtcConfig,
|
||||
password,
|
||||
trackerRedundancy: 4,
|
||||
relayRedundancy: 4,
|
||||
},
|
||||
{
|
||||
roomId,
|
||||
@ -68,6 +71,7 @@ export function Room({
|
||||
getUuid,
|
||||
publicKey,
|
||||
encryptionService,
|
||||
timeService,
|
||||
}
|
||||
)
|
||||
|
||||
@ -153,11 +157,7 @@ export function Room({
|
||||
height: landscape ? '100%' : '40%',
|
||||
}}
|
||||
>
|
||||
<ChatTranscript
|
||||
messageLog={messageLog}
|
||||
userId={userId}
|
||||
className="grow overflow-auto"
|
||||
/>
|
||||
<ChatTranscript messageLog={messageLog} userId={userId} />
|
||||
<Divider />
|
||||
<Box>
|
||||
<MessageForm
|
||||
|
@ -9,7 +9,7 @@ import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
|
||||
import { PeerRoom } from 'services/PeerRoom/PeerRoom'
|
||||
import { PeerRoom } from 'lib/PeerRoom'
|
||||
|
||||
import { useRoomAudio } from './useRoomAudio'
|
||||
import { MediaButton } from './MediaButton'
|
||||
|
@ -6,7 +6,9 @@ import Tooltip from '@mui/material/Tooltip'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
|
||||
import { RoomContext } from 'contexts/RoomContext'
|
||||
import { PeerRoom } from 'services/PeerRoom/PeerRoom'
|
||||
import { PeerRoom } from 'lib/PeerRoom'
|
||||
|
||||
import { Input } from 'components/Elements'
|
||||
|
||||
import { useRoomFileShare } from './useRoomFileShare'
|
||||
import { MediaButton } from './MediaButton'
|
||||
@ -73,12 +75,12 @@ export function RoomFileUploadControls({
|
||||
px: 1,
|
||||
}}
|
||||
>
|
||||
<input
|
||||
<Input
|
||||
multiple
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
id="file-upload"
|
||||
className="hidden"
|
||||
sx={{ display: 'none' }}
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<Tooltip
|
||||
|
@ -3,7 +3,7 @@ import ScreenShare from '@mui/icons-material/ScreenShare'
|
||||
import StopScreenShare from '@mui/icons-material/StopScreenShare'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
|
||||
import { PeerRoom } from 'services/PeerRoom/PeerRoom'
|
||||
import { PeerRoom } from 'lib/PeerRoom'
|
||||
|
||||
import { useRoomScreenShare } from './useRoomScreenShare'
|
||||
import { MediaButton } from './MediaButton'
|
||||
|
@ -9,7 +9,7 @@ import Menu from '@mui/material/Menu'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Tooltip from '@mui/material/Tooltip'
|
||||
|
||||
import { PeerRoom } from 'services/PeerRoom/PeerRoom'
|
||||
import { PeerRoom } from 'lib/PeerRoom'
|
||||
|
||||
import { useRoomVideo } from './useRoomVideo'
|
||||
import { MediaButton } from './MediaButton'
|
||||
|
@ -4,7 +4,7 @@ import Paper from '@mui/material/Paper'
|
||||
|
||||
import { RoomContext } from 'contexts/RoomContext'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { Peer, VideoStreamType } from 'models/chat'
|
||||
import { Peer, StreamType } from 'models/chat'
|
||||
|
||||
import { PeerVideo } from './PeerVideo'
|
||||
|
||||
@ -16,7 +16,7 @@ interface PeerWithVideo {
|
||||
|
||||
export interface SelectedPeerStream {
|
||||
peerId: string
|
||||
videoStreamType: VideoStreamType
|
||||
streamType: StreamType
|
||||
videoStream: MediaStream
|
||||
}
|
||||
|
||||
@ -105,13 +105,13 @@ export const RoomVideoDisplay = ({
|
||||
|
||||
const handleVideoClick = (
|
||||
peerId: string,
|
||||
videoStreamType: VideoStreamType,
|
||||
streamType: StreamType,
|
||||
videoStream: MediaStream
|
||||
) => {
|
||||
if (selectedPeerStream?.videoStream === videoStream) {
|
||||
setSelectedPeerStream(null)
|
||||
} else if (numberOfVideos > 1) {
|
||||
setSelectedPeerStream({ peerId, videoStreamType, videoStream })
|
||||
setSelectedPeerStream({ peerId, streamType, videoStream })
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ export const RoomVideoDisplay = ({
|
||||
userId={selectedPeerStream.peerId}
|
||||
selectedPeerStream={selectedPeerStream}
|
||||
videoStream={selectedPeerStream.videoStream}
|
||||
videoStreamType={selectedPeerStream.videoStreamType}
|
||||
streamType={selectedPeerStream.streamType}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@ -168,7 +168,7 @@ export const RoomVideoDisplay = ({
|
||||
userId={userId}
|
||||
selectedPeerStream={selectedPeerStream}
|
||||
videoStream={selfVideoStream}
|
||||
videoStreamType={VideoStreamType.WEBCAM}
|
||||
streamType={StreamType.WEBCAM}
|
||||
/>
|
||||
)}
|
||||
{selfScreenStream && (
|
||||
@ -179,7 +179,7 @@ export const RoomVideoDisplay = ({
|
||||
userId={userId}
|
||||
selectedPeerStream={selectedPeerStream}
|
||||
videoStream={selfScreenStream}
|
||||
videoStreamType={VideoStreamType.SCREEN_SHARE}
|
||||
streamType={StreamType.SCREEN_SHARE}
|
||||
/>
|
||||
)}
|
||||
{peersWithVideo.map(peerWithVideo => (
|
||||
@ -191,7 +191,7 @@ export const RoomVideoDisplay = ({
|
||||
userId={peerWithVideo.peer.userId}
|
||||
selectedPeerStream={selectedPeerStream}
|
||||
videoStream={peerWithVideo.videoStream}
|
||||
videoStreamType={VideoStreamType.WEBCAM}
|
||||
streamType={StreamType.WEBCAM}
|
||||
/>
|
||||
)}
|
||||
{peerWithVideo.screenStream && (
|
||||
@ -201,7 +201,7 @@ export const RoomVideoDisplay = ({
|
||||
userId={peerWithVideo.peer.userId}
|
||||
selectedPeerStream={selectedPeerStream}
|
||||
videoStream={peerWithVideo.screenStream}
|
||||
videoStreamType={VideoStreamType.SCREEN_SHARE}
|
||||
streamType={StreamType.SCREEN_SHARE}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { Peer, PeerVerificationState } from 'models/chat'
|
||||
import { encryptionService as encryptionServiceInstance } from 'services/Encryption'
|
||||
import { PeerRoom } from 'services/PeerRoom'
|
||||
import { encryption } from 'services/Encryption'
|
||||
import { PeerRoom } from 'lib/PeerRoom'
|
||||
import { PeerActions } from 'models/network'
|
||||
import { verificationTimeout } from 'config/messaging'
|
||||
import { usePeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
@ -10,13 +10,13 @@ import { usePeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
interface UserPeerVerificationProps {
|
||||
peerRoom: PeerRoom
|
||||
privateKey: CryptoKey
|
||||
encryptionService?: typeof encryptionServiceInstance
|
||||
encryptionService?: typeof encryption
|
||||
}
|
||||
|
||||
export const usePeerVerification = ({
|
||||
peerRoom,
|
||||
privateKey,
|
||||
encryptionService = encryptionServiceInstance,
|
||||
encryptionService = encryption,
|
||||
}: UserPeerVerificationProps) => {
|
||||
const { updatePeer, peerList, showAlert } = useContext(ShellContext)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { BaseRoomConfig } from 'trystero'
|
||||
import { TorrentRoomConfig } from 'trystero/torrent'
|
||||
import { RelayConfig } from 'trystero/torrent'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { useDebounce } from '@react-hook/debounce'
|
||||
|
||||
@ -23,16 +23,15 @@ import {
|
||||
TypingStatus,
|
||||
Peer,
|
||||
PeerVerificationState,
|
||||
AudioChannelName,
|
||||
} from 'models/chat'
|
||||
import { getPeerName, usePeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
import { NotificationService } from 'services/Notification'
|
||||
import { Audio as AudioService } from 'services/Audio'
|
||||
import { PeerRoom, PeerHookType } from 'services/PeerRoom'
|
||||
import { fileTransfer } from 'services/FileTransfer'
|
||||
import {
|
||||
AllowedKeyType,
|
||||
encryptionService as encryptionServiceInstance,
|
||||
} from 'services/Encryption'
|
||||
import { Audio } from 'lib/Audio'
|
||||
import { time } from 'lib/Time'
|
||||
import { PeerRoom, PeerHookType } from 'lib/PeerRoom'
|
||||
import { notification } from 'services/Notification'
|
||||
import { fileTransfer } from 'lib/FileTransfer'
|
||||
import { AllowedKeyType, encryption } from 'services/Encryption'
|
||||
|
||||
import { messageTranscriptSizeLimit } from 'config/messaging'
|
||||
|
||||
@ -43,23 +42,25 @@ interface UseRoomConfig {
|
||||
userId: string
|
||||
publicKey: CryptoKey
|
||||
getUuid?: typeof uuid
|
||||
encryptionService?: typeof encryptionServiceInstance
|
||||
encryptionService?: typeof encryption
|
||||
timeService?: typeof time
|
||||
}
|
||||
|
||||
interface UserMetadata {
|
||||
interface UserMetadata extends Record<string, any> {
|
||||
userId: string
|
||||
customUsername: string
|
||||
publicKeyString: string
|
||||
}
|
||||
|
||||
export function useRoom(
|
||||
{ password, ...roomConfig }: BaseRoomConfig & TorrentRoomConfig,
|
||||
{ password, ...roomConfig }: BaseRoomConfig & RelayConfig,
|
||||
{
|
||||
roomId,
|
||||
userId,
|
||||
publicKey,
|
||||
getUuid = uuid,
|
||||
encryptionService = encryptionServiceInstance,
|
||||
encryptionService = encryption,
|
||||
timeService = time,
|
||||
}: UseRoomConfig
|
||||
) {
|
||||
const isPrivate = password !== undefined
|
||||
@ -86,9 +87,7 @@ export function useRoom(
|
||||
const [messageLog, _setMessageLog] = useState<Array<Message | InlineMedia>>(
|
||||
[]
|
||||
)
|
||||
const [newMessageAudio] = useState(
|
||||
() => new AudioService(process.env.PUBLIC_URL + '/sounds/new-message.aac')
|
||||
)
|
||||
const [newMessageAudio] = useState(() => new Audio('/sounds/new-message.aac'))
|
||||
|
||||
const { getDisplayUsername } = usePeerNameDisplay()
|
||||
|
||||
@ -240,7 +239,7 @@ export function useRoom(
|
||||
const unsentMessage: UnsentMessage = {
|
||||
authorId: userId,
|
||||
text: message,
|
||||
timeSent: Date.now(),
|
||||
timeSent: timeService.now(),
|
||||
id: getUuid(),
|
||||
}
|
||||
|
||||
@ -251,7 +250,7 @@ export function useRoom(
|
||||
|
||||
setMessageLog([
|
||||
...messageLog,
|
||||
{ ...unsentMessage, timeReceived: Date.now() },
|
||||
{ ...unsentMessage, timeReceived: timeService.now() },
|
||||
])
|
||||
setIsMessageSending(false)
|
||||
}
|
||||
@ -271,7 +270,10 @@ export function useRoom(
|
||||
userId,
|
||||
publicKey,
|
||||
customUsername,
|
||||
audioState: AudioState.STOPPED,
|
||||
audioChannelState: {
|
||||
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
|
||||
[AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED,
|
||||
},
|
||||
videoState: VideoState.STOPPED,
|
||||
screenShareState: ScreenShareState.NOT_SHARING,
|
||||
offeredFileId: null,
|
||||
@ -323,13 +325,14 @@ export function useRoom(
|
||||
if (userSettings.showNotificationOnNewMessage) {
|
||||
const displayUsername = getDisplayUsername(message.authorId)
|
||||
|
||||
NotificationService.showNotification(
|
||||
`${displayUsername}: ${message.text}`
|
||||
)
|
||||
notification.showNotification(`${displayUsername}: ${message.text}`)
|
||||
}
|
||||
}
|
||||
|
||||
setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }])
|
||||
setMessageLog([
|
||||
...messageLog,
|
||||
{ ...message, timeReceived: timeService.now() },
|
||||
])
|
||||
updatePeer(peerId, { isTyping: false })
|
||||
})
|
||||
|
||||
@ -339,9 +342,8 @@ export function useRoom(
|
||||
})
|
||||
;(async () => {
|
||||
try {
|
||||
const publicKeyString = await encryptionService.stringifyCryptoKey(
|
||||
publicKey
|
||||
)
|
||||
const publicKeyString =
|
||||
await encryptionService.stringifyCryptoKey(publicKey)
|
||||
|
||||
const promises: Promise<any>[] = [
|
||||
sendPeerMetadata({ userId, customUsername, publicKeyString }, peerId),
|
||||
@ -396,7 +398,7 @@ export function useRoom(
|
||||
const unsentInlineMedia: UnsentInlineMedia = {
|
||||
authorId: userId,
|
||||
magnetURI: fileOfferId,
|
||||
timeSent: Date.now(),
|
||||
timeSent: timeService.now(),
|
||||
id: getUuid(),
|
||||
}
|
||||
|
||||
@ -407,7 +409,7 @@ export function useRoom(
|
||||
|
||||
setMessageLog([
|
||||
...messageLog,
|
||||
{ ...unsentInlineMedia, timeReceived: Date.now() },
|
||||
{ ...unsentInlineMedia, timeReceived: timeService.now() },
|
||||
])
|
||||
setIsMessageSending(false)
|
||||
}
|
||||
@ -433,13 +435,16 @@ export function useRoom(
|
||||
}
|
||||
|
||||
if (userSettings.showNotificationOnNewMessage) {
|
||||
NotificationService.showNotification(
|
||||
notification.showNotification(
|
||||
`${getDisplayUsername(inlineMedia.authorId)} shared media`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setMessageLog([...messageLog, { ...inlineMedia, timeReceived: Date.now() }])
|
||||
setMessageLog([
|
||||
...messageLog,
|
||||
{ ...inlineMedia, timeReceived: timeService.now() },
|
||||
])
|
||||
})
|
||||
|
||||
receiveTypingStatusChange((typingStatus, peerId) => {
|
||||
@ -449,9 +454,8 @@ export function useRoom(
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
const publicKeyString = await encryptionService.stringifyCryptoKey(
|
||||
publicKey
|
||||
)
|
||||
const publicKeyString =
|
||||
await encryptionService.stringifyCryptoKey(publicKey)
|
||||
|
||||
sendPeerMetadata({
|
||||
customUsername,
|
||||
|
@ -2,8 +2,14 @@ import { useContext, useEffect, useCallback, useState } from 'react'
|
||||
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { PeerActions } from 'models/network'
|
||||
import { AudioState, Peer } from 'models/chat'
|
||||
import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom'
|
||||
import {
|
||||
AudioState,
|
||||
Peer,
|
||||
AudioChannelName,
|
||||
PeerAudioChannelState,
|
||||
StreamType,
|
||||
} from 'models/chat'
|
||||
import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom'
|
||||
|
||||
interface UseRoomAudioConfig {
|
||||
peerRoom: PeerRoom
|
||||
@ -19,7 +25,7 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
|
||||
string | null
|
||||
>(null)
|
||||
|
||||
const { peerList, setPeerList, setAudioState, peerAudios, setPeerAudios } =
|
||||
const { setPeerList, setAudioChannelState, setPeerAudioChannels } =
|
||||
shellContext
|
||||
|
||||
useEffect(() => {
|
||||
@ -32,29 +38,46 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
|
||||
})()
|
||||
}, [audioStream])
|
||||
|
||||
const [sendAudioChange, receiveAudioChange] = peerRoom.makeAction<AudioState>(
|
||||
PeerActions.AUDIO_CHANGE
|
||||
)
|
||||
const [sendAudioChange, receiveAudioChange] = peerRoom.makeAction<
|
||||
Partial<PeerAudioChannelState>
|
||||
>(PeerActions.AUDIO_CHANGE)
|
||||
|
||||
receiveAudioChange((audioState, peerId) => {
|
||||
const newPeerList = peerList.map(peer => {
|
||||
receiveAudioChange((peerAudioChannelState, peerId) => {
|
||||
setPeerList(peerList => {
|
||||
return peerList.map(peer => {
|
||||
const newPeer: Peer = { ...peer }
|
||||
|
||||
if (peer.peerId === peerId) {
|
||||
newPeer.audioState = audioState
|
||||
const microphoneAudioChannel =
|
||||
peerAudioChannelState[AudioChannelName.MICROPHONE]
|
||||
|
||||
if (audioState === AudioState.STOPPED) {
|
||||
if (microphoneAudioChannel) {
|
||||
if (peer.peerId === peerId) {
|
||||
newPeer.audioChannelState = {
|
||||
...newPeer.audioChannelState,
|
||||
...peerAudioChannelState,
|
||||
}
|
||||
|
||||
if (microphoneAudioChannel === AudioState.STOPPED) {
|
||||
deletePeerAudio(peerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newPeer
|
||||
})
|
||||
|
||||
setPeerList(newPeerList)
|
||||
})
|
||||
})
|
||||
|
||||
peerRoom.onPeerStream(PeerStreamType.AUDIO, (stream, peerId) => {
|
||||
peerRoom.onPeerStream(PeerStreamType.AUDIO, (stream, peerId, metadata) => {
|
||||
if (
|
||||
typeof metadata === 'object' &&
|
||||
metadata !== null &&
|
||||
'type' in metadata &&
|
||||
metadata.type !== StreamType.MICROPHONE
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const audioTracks = stream.getAudioTracks()
|
||||
|
||||
if (audioTracks.length === 0) return
|
||||
@ -63,7 +86,13 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
|
||||
audio.srcObject = stream
|
||||
audio.autoplay = true
|
||||
|
||||
setPeerAudios({ ...peerAudios, [peerId]: audio })
|
||||
setPeerAudioChannels(peerAudioChannels => ({
|
||||
...peerAudioChannels,
|
||||
[peerId]: {
|
||||
...peerAudioChannels[peerId],
|
||||
[AudioChannelName.MICROPHONE]: audio,
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
const cleanupAudio = useCallback(() => {
|
||||
@ -86,9 +115,19 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
|
||||
video: false,
|
||||
})
|
||||
|
||||
peerRoom.addStream(newSelfStream)
|
||||
sendAudioChange(AudioState.PLAYING)
|
||||
setAudioState(AudioState.PLAYING)
|
||||
peerRoom.addStream(newSelfStream, null, {
|
||||
type: StreamType.MICROPHONE,
|
||||
})
|
||||
|
||||
sendAudioChange({
|
||||
[AudioChannelName.MICROPHONE]: AudioState.PLAYING,
|
||||
})
|
||||
|
||||
setAudioChannelState(prevState => ({
|
||||
...prevState,
|
||||
[AudioChannelName.MICROPHONE]: AudioState.PLAYING,
|
||||
}))
|
||||
|
||||
setAudioStream(newSelfStream)
|
||||
}
|
||||
} else {
|
||||
@ -96,8 +135,16 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
|
||||
cleanupAudio()
|
||||
|
||||
peerRoom.removeStream(audioStream, peerRoom.getPeers())
|
||||
sendAudioChange(AudioState.STOPPED)
|
||||
setAudioState(AudioState.STOPPED)
|
||||
|
||||
sendAudioChange({
|
||||
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
|
||||
})
|
||||
|
||||
setAudioChannelState(prevState => ({
|
||||
...prevState,
|
||||
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
|
||||
}))
|
||||
|
||||
setAudioStream(null)
|
||||
}
|
||||
}
|
||||
@ -106,11 +153,10 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
|
||||
audioStream,
|
||||
cleanupAudio,
|
||||
isSpeakingToRoom,
|
||||
peerAudios,
|
||||
peerRoom,
|
||||
selectedAudioDeviceId,
|
||||
sendAudioChange,
|
||||
setAudioState,
|
||||
setAudioChannelState,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
@ -139,27 +185,45 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
|
||||
video: false,
|
||||
})
|
||||
|
||||
peerRoom.addStream(newSelfStream)
|
||||
peerRoom.addStream(newSelfStream, null, {
|
||||
type: StreamType.MICROPHONE,
|
||||
})
|
||||
|
||||
setAudioStream(newSelfStream)
|
||||
}
|
||||
|
||||
const deletePeerAudio = (peerId: string) => {
|
||||
const newPeerAudios = { ...peerAudios }
|
||||
delete newPeerAudios[peerId]
|
||||
setPeerAudios(newPeerAudios)
|
||||
setPeerAudioChannels(({ ...newPeerAudios }) => {
|
||||
if (!newPeerAudios[peerId]) {
|
||||
return newPeerAudios
|
||||
}
|
||||
|
||||
const microphoneAudio = newPeerAudios[peerId][AudioChannelName.MICROPHONE]
|
||||
microphoneAudio?.pause()
|
||||
|
||||
const { [AudioChannelName.MICROPHONE]: _, ...newPeerAudioChannels } =
|
||||
newPeerAudios[peerId]
|
||||
|
||||
newPeerAudios[peerId] = newPeerAudioChannels
|
||||
|
||||
return newPeerAudios
|
||||
})
|
||||
}
|
||||
|
||||
const handleAudioForNewPeer = (peerId: string) => {
|
||||
if (audioStream) {
|
||||
peerRoom.addStream(audioStream, peerId)
|
||||
peerRoom.addStream(audioStream, peerId, {
|
||||
type: StreamType.MICROPHONE,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleAudioForLeavingPeer = (peerId: string) => {
|
||||
if (audioStream) {
|
||||
peerRoom.removeStream(audioStream, peerId)
|
||||
deletePeerAudio(peerId)
|
||||
}
|
||||
|
||||
deletePeerAudio(peerId)
|
||||
}
|
||||
|
||||
peerRoom.onPeerJoin(PeerHookType.AUDIO, (peerId: string) => {
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { sleep } from 'utils'
|
||||
import { sleep } from 'lib/sleep'
|
||||
import { RoomContext } from 'contexts/RoomContext'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { PeerActions } from 'models/network'
|
||||
import { FileOfferMetadata, Peer } from 'models/chat'
|
||||
import { PeerRoom, PeerHookType } from 'services/PeerRoom'
|
||||
|
||||
import { fileTransfer } from 'services/FileTransfer/index'
|
||||
import { PeerRoom, PeerHookType } from 'lib/PeerRoom'
|
||||
import { fileTransfer } from 'lib/FileTransfer'
|
||||
|
||||
interface UseRoomFileShareConfig {
|
||||
onInlineMediaUpload: (files: File[]) => void
|
||||
|
@ -1,11 +1,17 @@
|
||||
import { useContext, useEffect, useCallback, useState } from 'react'
|
||||
|
||||
import { isRecord } from 'utils'
|
||||
import { isRecord } from 'lib/type-guards'
|
||||
import { RoomContext } from 'contexts/RoomContext'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { PeerActions } from 'models/network'
|
||||
import { ScreenShareState, Peer, VideoStreamType } from 'models/chat'
|
||||
import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom'
|
||||
import {
|
||||
ScreenShareState,
|
||||
Peer,
|
||||
StreamType,
|
||||
AudioChannelName,
|
||||
AudioState,
|
||||
} from 'models/chat'
|
||||
import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom'
|
||||
|
||||
interface UseRoomScreenShareConfig {
|
||||
peerRoom: PeerRoom
|
||||
@ -16,7 +22,13 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
|
||||
const roomContext = useContext(RoomContext)
|
||||
const [isSharingScreen, setIsSharingScreen] = useState(false)
|
||||
|
||||
const { peerList, setPeerList, setScreenState } = shellContext
|
||||
const {
|
||||
peerList,
|
||||
setPeerList,
|
||||
setScreenState,
|
||||
setAudioChannelState,
|
||||
setPeerAudioChannels,
|
||||
} = shellContext
|
||||
|
||||
const {
|
||||
peerScreenStreams,
|
||||
@ -50,7 +62,7 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
|
||||
const isScreenShareStream =
|
||||
isRecord(metadata) &&
|
||||
'type' in metadata &&
|
||||
metadata.type === VideoStreamType.SCREEN_SHARE
|
||||
metadata.type === StreamType.SCREEN_SHARE
|
||||
|
||||
if (!isScreenShareStream) return
|
||||
|
||||
@ -58,6 +70,33 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
|
||||
...peerScreenStreams,
|
||||
[peerId]: stream,
|
||||
})
|
||||
|
||||
const [audioStream] = stream.getAudioTracks()
|
||||
|
||||
if (audioStream) {
|
||||
setAudioChannelState(prevState => ({
|
||||
...prevState,
|
||||
[AudioChannelName.SCREEN_SHARE]: AudioState.PLAYING,
|
||||
}))
|
||||
|
||||
const audioTracks = stream.getAudioTracks()
|
||||
|
||||
if (audioTracks.length > 0) {
|
||||
const audio = new Audio()
|
||||
audio.srcObject = stream
|
||||
audio.autoplay = true
|
||||
|
||||
setPeerAudioChannels(peerAudioChannels => {
|
||||
return {
|
||||
...peerAudioChannels,
|
||||
[peerId]: {
|
||||
...peerAudioChannels[peerId],
|
||||
[AudioChannelName.SCREEN_SHARE]: audio,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const cleanupScreenStream = useCallback(() => {
|
||||
@ -78,8 +117,9 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
|
||||
})
|
||||
|
||||
peerRoom.addStream(displayMedia, null, {
|
||||
type: VideoStreamType.SCREEN_SHARE,
|
||||
type: StreamType.SCREEN_SHARE,
|
||||
})
|
||||
|
||||
setSelfScreenStream(displayMedia)
|
||||
sendScreenShare(ScreenShareState.SHARING)
|
||||
setScreenState(ScreenShareState.SHARING)
|
||||
@ -119,15 +159,33 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
|
||||
}, [setPeerScreenStreams])
|
||||
|
||||
const deletePeerScreen = (peerId: string) => {
|
||||
const newPeerScreens = { ...peerScreenStreams }
|
||||
delete newPeerScreens[peerId]
|
||||
setPeerScreenStreams(newPeerScreens)
|
||||
setPeerScreenStreams(({ [peerId]: _, ...newPeerScreens }) => {
|
||||
return newPeerScreens
|
||||
})
|
||||
|
||||
setPeerAudioChannels(({ ...newPeerAudios }) => {
|
||||
if (!newPeerAudios[peerId]) {
|
||||
return newPeerAudios
|
||||
}
|
||||
|
||||
const screenShareAudio =
|
||||
newPeerAudios[peerId][AudioChannelName.SCREEN_SHARE]
|
||||
|
||||
screenShareAudio?.pause()
|
||||
|
||||
const { [AudioChannelName.SCREEN_SHARE]: _, ...newPeerAudioChannels } =
|
||||
newPeerAudios[peerId]
|
||||
|
||||
newPeerAudios[peerId] = newPeerAudioChannels
|
||||
|
||||
return newPeerAudios
|
||||
})
|
||||
}
|
||||
|
||||
const handleScreenForNewPeer = (peerId: string) => {
|
||||
if (selfScreenStream) {
|
||||
peerRoom.addStream(selfScreenStream, peerId, {
|
||||
type: VideoStreamType.SCREEN_SHARE,
|
||||
type: StreamType.SCREEN_SHARE,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,9 @@ import { useContext, useEffect, useCallback, useState } from 'react'
|
||||
import { RoomContext } from 'contexts/RoomContext'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { PeerActions } from 'models/network'
|
||||
import { VideoState, Peer, VideoStreamType } from 'models/chat'
|
||||
import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom'
|
||||
|
||||
import { isRecord } from 'utils'
|
||||
import { VideoState, Peer, StreamType } from 'models/chat'
|
||||
import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom'
|
||||
import { isRecord } from 'lib/type-guards'
|
||||
|
||||
interface UseRoomVideoConfig {
|
||||
peerRoom: PeerRoom
|
||||
@ -61,8 +60,9 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
|
||||
})
|
||||
|
||||
peerRoom.addStream(newSelfStream, null, {
|
||||
type: VideoStreamType.WEBCAM,
|
||||
type: StreamType.WEBCAM,
|
||||
})
|
||||
|
||||
setSelfVideoStream(newSelfStream)
|
||||
}
|
||||
})()
|
||||
@ -94,7 +94,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
|
||||
const isWebcamStream =
|
||||
isRecord(metadata) &&
|
||||
'type' in metadata &&
|
||||
metadata.type === VideoStreamType.WEBCAM
|
||||
metadata.type === StreamType.WEBCAM
|
||||
|
||||
if (!isWebcamStream) return
|
||||
|
||||
@ -125,8 +125,9 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
|
||||
})
|
||||
|
||||
peerRoom.addStream(newSelfStream, null, {
|
||||
type: VideoStreamType.WEBCAM,
|
||||
type: StreamType.WEBCAM,
|
||||
})
|
||||
|
||||
sendVideoChange(VideoState.PLAYING)
|
||||
setVideoState(VideoState.PLAYING)
|
||||
setSelfVideoStream(newSelfStream)
|
||||
@ -194,7 +195,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
|
||||
},
|
||||
})
|
||||
|
||||
peerRoom.addStream(newSelfStream, null, { type: VideoStreamType.WEBCAM })
|
||||
peerRoom.addStream(newSelfStream, null, { type: StreamType.WEBCAM })
|
||||
setSelfVideoStream(newSelfStream)
|
||||
}
|
||||
|
||||
@ -207,7 +208,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
|
||||
const handleVideoForNewPeer = (peerId: string) => {
|
||||
if (selfVideoStream) {
|
||||
peerRoom.addStream(selfVideoStream, peerId, {
|
||||
type: VideoStreamType.WEBCAM,
|
||||
type: StreamType.WEBCAM,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import Circle from '@mui/icons-material/FiberManualRecord'
|
||||
import { Box } from '@mui/system'
|
||||
import ReportIcon from '@mui/icons-material/Report'
|
||||
|
||||
import { TrackerConnection } from 'services/ConnectionTest/ConnectionTest'
|
||||
import { TrackerConnection } from 'lib/ConnectionTest'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
|
||||
import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest'
|
||||
|
@ -145,7 +145,9 @@ export const Drawer = ({ isDrawerOpen, onDrawerClose, theme }: DrawerProps) => {
|
||||
<MuiLink
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href={`${process.env.REACT_APP_GITHUB_REPO}/commit/${commit.hash}`}
|
||||
href={`${import.meta.env.VITE_GITHUB_REPO}/commit/${
|
||||
commit.hash
|
||||
}`}
|
||||
>
|
||||
{commit.shortHash}
|
||||
</MuiLink>
|
||||
|
@ -12,7 +12,7 @@ const { isSecureContext, RTCDataChannel } = window
|
||||
const doesSupportWebRtc = RTCDataChannel !== undefined
|
||||
|
||||
export const isEnvironmentSupported =
|
||||
(isSecureContext && doesSupportWebRtc) || process.env.NODE_ENV === 'test'
|
||||
(isSecureContext && doesSupportWebRtc) || import.meta.env.MODE === 'test'
|
||||
|
||||
export const EnvironmentUnsupportedDialog = () => {
|
||||
const theme = useTheme()
|
||||
|
@ -2,12 +2,11 @@ import { forwardRef } from 'react'
|
||||
import Snackbar from '@mui/material/Snackbar'
|
||||
import MuiAlert, { AlertProps, AlertColor } from '@mui/material/Alert'
|
||||
|
||||
const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
||||
function Alert(props, ref) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
interface NotificationAreaProps {
|
||||
alertSeverity: AlertColor
|
||||
|
@ -1,3 +0,0 @@
|
||||
.PeerDownloadFileButton
|
||||
.MuiCircularProgress-circle
|
||||
transition: none !important
|
@ -5,12 +5,11 @@ import Tooltip from '@mui/material/Tooltip'
|
||||
import Download from '@mui/icons-material/Download'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
|
||||
import { isError } from 'utils'
|
||||
import { fileTransfer } from 'services/FileTransfer/index'
|
||||
import { isError } from 'lib/type-guards'
|
||||
import { fileTransfer } from 'lib/FileTransfer'
|
||||
import { Peer } from 'models/chat'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
|
||||
import './PeerDownloadFileButton.sass'
|
||||
import { usePeerNameDisplay } from 'components/PeerNameDisplay/usePeerNameDisplay'
|
||||
|
||||
interface PeerDownloadFileButtonProps {
|
||||
@ -65,6 +64,9 @@ export const PeerDownloadFileButton = ({
|
||||
<CircularProgress
|
||||
variant={downloadProgress === null ? 'indeterminate' : 'determinate'}
|
||||
value={downloadProgress === null ? undefined : downloadProgress}
|
||||
sx={{
|
||||
transition: 'none',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip
|
||||
|
@ -9,9 +9,15 @@ import Box from '@mui/material/Box'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
|
||||
import { UserInfo } from 'components/UserInfo'
|
||||
import { AudioState, Peer } from 'models/chat'
|
||||
import { PeerConnectionType } from 'services/PeerRoom'
|
||||
import { TrackerConnection } from 'services/ConnectionTest'
|
||||
import {
|
||||
AudioState,
|
||||
Peer,
|
||||
AudioChannel,
|
||||
AudioChannelName,
|
||||
PeerAudioChannelState,
|
||||
} from 'models/chat'
|
||||
import { PeerConnectionType } from 'lib/PeerRoom'
|
||||
import { TrackerConnection } from 'lib/ConnectionTest'
|
||||
|
||||
import { PeerListHeader } from './PeerListHeader'
|
||||
import { PeerListItem } from './PeerListItem'
|
||||
@ -25,8 +31,8 @@ export interface PeerListProps extends PropsWithChildren {
|
||||
onPeerListClose: () => void
|
||||
peerList: Peer[]
|
||||
peerConnectionTypes: Record<string, PeerConnectionType>
|
||||
audioState: AudioState
|
||||
peerAudios: Record<string, HTMLAudioElement>
|
||||
peerAudioChannelState: PeerAudioChannelState
|
||||
peerAudioChannels: Record<string, AudioChannel>
|
||||
connectionTestResults: IConnectionTestResults
|
||||
}
|
||||
|
||||
@ -36,8 +42,8 @@ export const PeerList = ({
|
||||
onPeerListClose,
|
||||
peerList,
|
||||
peerConnectionTypes,
|
||||
audioState,
|
||||
peerAudios,
|
||||
peerAudioChannelState,
|
||||
peerAudioChannels,
|
||||
connectionTestResults,
|
||||
}: PeerListProps) => {
|
||||
return (
|
||||
@ -49,7 +55,8 @@ export const PeerList = ({
|
||||
<Divider />
|
||||
<List>
|
||||
<ListItem divider={true}>
|
||||
{audioState === AudioState.PLAYING && (
|
||||
{peerAudioChannelState[AudioChannelName.MICROPHONE] ===
|
||||
AudioState.PLAYING && (
|
||||
<ListItemIcon>
|
||||
<VolumeUp />
|
||||
</ListItemIcon>
|
||||
@ -63,7 +70,7 @@ export const PeerList = ({
|
||||
key={peer.peerId}
|
||||
peer={peer}
|
||||
peerConnectionTypes={peerConnectionTypes}
|
||||
peerAudios={peerAudios}
|
||||
peerAudioChannels={peerAudioChannels}
|
||||
/>
|
||||
))}
|
||||
{peerList.length === 0 &&
|
||||
|
@ -18,15 +18,20 @@ import EnhancedEncryptionIcon from '@mui/icons-material/EnhancedEncryption'
|
||||
import { AudioVolume } from 'components/AudioVolume'
|
||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
import { PublicKey } from 'components/PublicKey'
|
||||
import { Peer, PeerVerificationState } from 'models/chat'
|
||||
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
|
||||
import {
|
||||
Peer,
|
||||
AudioChannel,
|
||||
AudioChannelName,
|
||||
PeerVerificationState,
|
||||
} from 'models/chat'
|
||||
import { PeerConnectionType } from 'lib/PeerRoom'
|
||||
|
||||
import { PeerDownloadFileButton } from './PeerDownloadFileButton'
|
||||
|
||||
interface PeerListItemProps {
|
||||
peer: Peer
|
||||
peerConnectionTypes: Record<string, PeerConnectionType>
|
||||
peerAudios: Record<string, HTMLAudioElement>
|
||||
peerAudioChannels: Record<string, AudioChannel>
|
||||
}
|
||||
|
||||
const verificationStateDisplayMap = {
|
||||
@ -52,8 +57,8 @@ const iconRightPadding = 1
|
||||
export const PeerListItem = ({
|
||||
peer,
|
||||
peerConnectionTypes,
|
||||
peerAudios,
|
||||
}: PeerListItemProps): JSX.Element => {
|
||||
peerAudioChannels,
|
||||
}: PeerListItemProps) => {
|
||||
const [showPeerDialog, setShowPeerDialog] = useState(false)
|
||||
|
||||
const hasPeerConnection = peer.peerId in peerConnectionTypes
|
||||
@ -69,6 +74,11 @@ export const PeerListItem = ({
|
||||
setShowPeerDialog(false)
|
||||
}
|
||||
|
||||
const microphoneAudio =
|
||||
peerAudioChannels[peer.peerId]?.[AudioChannelName.MICROPHONE]
|
||||
const screenShareAudio =
|
||||
peerAudioChannels[peer.peerId]?.[AudioChannelName.SCREEN_SHARE]
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem key={peer.peerId} divider={true}>
|
||||
@ -124,8 +134,17 @@ export const PeerListItem = ({
|
||||
</Box>
|
||||
<PeerNameDisplay>{peer.userId}</PeerNameDisplay>
|
||||
</Box>
|
||||
{peer.peerId in peerAudios && (
|
||||
<AudioVolume audioEl={peerAudios[peer.peerId]} />
|
||||
{microphoneAudio && (
|
||||
<AudioVolume
|
||||
audioEl={microphoneAudio}
|
||||
audioChannelName={AudioChannelName.MICROPHONE}
|
||||
/>
|
||||
)}
|
||||
{screenShareAudio && (
|
||||
<AudioVolume
|
||||
audioEl={screenShareAudio}
|
||||
audioChannelName={AudioChannelName.SCREEN_SHARE}
|
||||
/>
|
||||
)}
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
@ -14,8 +14,8 @@ import CloseIcon from '@mui/icons-material/Close'
|
||||
|
||||
import { AlertOptions } from 'models/shell'
|
||||
import { useEffect, useState, SyntheticEvent } from 'react'
|
||||
import { sleep } from 'utils'
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import { sleep } from 'lib/sleep'
|
||||
import { encryption } from 'services/Encryption'
|
||||
|
||||
export interface RoomShareDialogProps {
|
||||
isOpen: boolean
|
||||
@ -51,10 +51,7 @@ export function RoomShareDialog(props: RoomShareDialogProps) {
|
||||
const url = window.location.href.split('#')[0]
|
||||
|
||||
const copyWithPass = async () => {
|
||||
const encoded = await encryptionService.encodePassword(
|
||||
props.roomId,
|
||||
password
|
||||
)
|
||||
const encoded = await encryption.encodePassword(props.roomId, password)
|
||||
|
||||
if (encoded === props.password) {
|
||||
const params = new URLSearchParams()
|
||||
|
@ -35,7 +35,7 @@ describe('Shell', () => {
|
||||
userEvent.click(menuButton)
|
||||
})
|
||||
|
||||
const navigation = screen.getByRole('navigation')
|
||||
const navigation = screen.getByLabelText('Navigation menu')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigation).toBeVisible()
|
||||
@ -56,7 +56,7 @@ describe('Shell', () => {
|
||||
userEvent.click(closeMenu)
|
||||
})
|
||||
|
||||
const navigation = screen.getByRole('navigation')
|
||||
const navigation = screen.getByLabelText('Navigation menu')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigation).not.toBeVisible()
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
||||
import { ThemeProvider } from '@mui/material/styles'
|
||||
import Box from '@mui/material/Box'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { AlertColor } from '@mui/material/Alert'
|
||||
@ -19,10 +19,17 @@ import { useWindowSize } from '@react-hook/window-size'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import { AlertOptions, QueryParamKeys } from 'models/shell'
|
||||
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
|
||||
import {
|
||||
AudioState,
|
||||
ScreenShareState,
|
||||
VideoState,
|
||||
Peer,
|
||||
AudioChannel,
|
||||
PeerAudioChannelState,
|
||||
AudioChannelName,
|
||||
} from 'models/chat'
|
||||
import { ErrorBoundary } from 'components/ErrorBoundary'
|
||||
|
||||
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
|
||||
import { PeerConnectionType } from 'lib/PeerRoom'
|
||||
|
||||
import { Drawer } from './Drawer'
|
||||
import { UpgradeDialog } from './UpgradeDialog'
|
||||
@ -38,6 +45,7 @@ import {
|
||||
EnvironmentUnsupportedDialog,
|
||||
isEnvironmentSupported,
|
||||
} from './EnvironmentUnsupportedDialog'
|
||||
import { useShellTheme } from './useShellTheme'
|
||||
|
||||
export interface ShellProps extends PropsWithChildren {
|
||||
userPeerId: string
|
||||
@ -50,17 +58,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
const { getUserSettings, updateUserSettings } = useContext(SettingsContext)
|
||||
const isEmbedded = queryParams.get(QueryParamKeys.IS_EMBEDDED) !== null
|
||||
|
||||
const { colorMode } = getUserSettings()
|
||||
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
createTheme({
|
||||
palette: {
|
||||
mode: colorMode,
|
||||
},
|
||||
}),
|
||||
[colorMode]
|
||||
)
|
||||
const theme = useShellTheme()
|
||||
|
||||
const [windowWidth] = useWindowSize()
|
||||
const defaultSidebarsOpen = windowWidth >= theme.breakpoints.values.lg
|
||||
@ -87,7 +85,11 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
Record<string, PeerConnectionType>
|
||||
>({})
|
||||
const [tabHasFocus, setTabHasFocus] = useState(true)
|
||||
const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED)
|
||||
const [audioChannelState, setAudioChannelState] =
|
||||
useState<PeerAudioChannelState>({
|
||||
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
|
||||
[AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED,
|
||||
})
|
||||
const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED)
|
||||
const [screenState, setScreenState] = useState<ScreenShareState>(
|
||||
ScreenShareState.NOT_SHARING
|
||||
@ -95,8 +97,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
const [customUsername, setCustomUsername] = useState(
|
||||
getUserSettings().customUsername
|
||||
)
|
||||
const [peerAudios, setPeerAudios] = useState<
|
||||
Record<string, HTMLAudioElement>
|
||||
const [peerAudioChannels, setPeerAudioChannels] = useState<
|
||||
Record<string, AudioChannel>
|
||||
>({})
|
||||
|
||||
const showAlert = useCallback((message: string, options?: AlertOptions) => {
|
||||
@ -145,14 +147,14 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
setIsServerConnectionFailureDialogOpen,
|
||||
peerConnectionTypes,
|
||||
setPeerConnectionTypes,
|
||||
audioState,
|
||||
setAudioState,
|
||||
audioChannelState,
|
||||
setAudioChannelState,
|
||||
videoState,
|
||||
setVideoState,
|
||||
screenState,
|
||||
setScreenState,
|
||||
peerAudios,
|
||||
setPeerAudios,
|
||||
peerAudioChannels,
|
||||
setPeerAudioChannels,
|
||||
customUsername,
|
||||
setCustomUsername,
|
||||
connectionTestResults,
|
||||
@ -175,14 +177,14 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
setShowRoomControls,
|
||||
setTitle,
|
||||
showAlert,
|
||||
audioState,
|
||||
setAudioState,
|
||||
audioChannelState,
|
||||
setAudioChannelState,
|
||||
videoState,
|
||||
setVideoState,
|
||||
screenState,
|
||||
setScreenState,
|
||||
peerAudios,
|
||||
setPeerAudios,
|
||||
peerAudioChannels,
|
||||
setPeerAudioChannels,
|
||||
customUsername,
|
||||
setCustomUsername,
|
||||
connectionTestResults,
|
||||
@ -394,8 +396,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
||||
onPeerListClose={handlePeerListClick}
|
||||
peerList={peerList}
|
||||
peerConnectionTypes={peerConnectionTypes}
|
||||
audioState={audioState}
|
||||
peerAudios={peerAudios}
|
||||
peerAudioChannelState={audioChannelState}
|
||||
peerAudioChannels={peerAudioChannels}
|
||||
connectionTestResults={connectionTestResults}
|
||||
/>
|
||||
{isEmbedded ? (
|
||||
|
@ -6,14 +6,17 @@ import DialogContent from '@mui/material/DialogContent'
|
||||
import DialogContentText from '@mui/material/DialogContentText'
|
||||
import DialogTitle from '@mui/material/DialogTitle'
|
||||
import WarningIcon from '@mui/icons-material/Warning'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
|
||||
interface UpgradeDialogProps {
|
||||
appNeedsUpdate: boolean
|
||||
}
|
||||
|
||||
export const UpgradeDialog = ({ appNeedsUpdate }: UpgradeDialogProps) => {
|
||||
const { updateServiceWorker } = useRegisterSW()
|
||||
|
||||
const handleRestartClick = () => {
|
||||
window.location.reload()
|
||||
updateServiceWorker(true)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { sleep } from 'utils'
|
||||
|
||||
import { sleep } from 'lib/sleep'
|
||||
import {
|
||||
ConnectionTest,
|
||||
ConnectionTestEvent,
|
||||
ConnectionTestEvents,
|
||||
TrackerConnection,
|
||||
} from 'services/ConnectionTest/ConnectionTest'
|
||||
} from 'lib/ConnectionTest'
|
||||
|
||||
export interface ConnectionTestResults {
|
||||
hasHost: boolean
|
||||
|
23
src/components/Shell/useShellTheme.ts
Normal file
23
src/components/Shell/useShellTheme.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import { createTheme } from '@mui/material/styles'
|
||||
|
||||
export const useShellTheme = () => {
|
||||
const { getUserSettings } = useContext(SettingsContext)
|
||||
const { colorMode } = getUserSettings()
|
||||
|
||||
const theme = useMemo(
|
||||
() =>
|
||||
// NOTE: You can make theme customizations here. It is recommended to use
|
||||
// the default theme viewer as a reference:
|
||||
// https://mui.com/material-ui/customization/default-theme/
|
||||
createTheme({
|
||||
palette: {
|
||||
mode: colorMode,
|
||||
},
|
||||
}),
|
||||
[colorMode]
|
||||
)
|
||||
|
||||
return theme
|
||||
}
|
1
src/config/communityRooms.ts
Normal file
1
src/config/communityRooms.ts
Normal file
@ -0,0 +1 @@
|
||||
export const communityRoomNames = ['General', 'Prayer']
|
@ -11,22 +11,32 @@ export const rtcConfig: RTCConfiguration = {
|
||||
// CONNECTED TO THE INTERNET.
|
||||
iceServers: [
|
||||
{
|
||||
urls: 'stun:188.148.133.173:3478',
|
||||
urls: 'turn:relay1.expressturn.com:3478',
|
||||
username: 'efQUQ79N77B5BNVVKF',
|
||||
credential: 'N4EAUgpjMzPLrxSS',
|
||||
},
|
||||
{
|
||||
urls: 'turn:188.148.133.173:3478',
|
||||
username: 'c386d75b5633456cb3bc13812858098d',
|
||||
credential: '58fd06d85fe14c0f9f46220748b0f565',
|
||||
urls: 'stun:stun.relay.metered.ca:80',
|
||||
},
|
||||
{
|
||||
urls: 'turn:188.148.133.173:3478',
|
||||
username: '0e2f563eacfd4c4a82ea239b04d1d494',
|
||||
credential: '8179b4b533f240ad9fe590663bef1bc9',
|
||||
urls: 'turn:global.relay.metered.ca:80',
|
||||
username: 'cfd3dccc1b9162a136a80482',
|
||||
credential: '1TgyKEd7B6iUxIn9',
|
||||
},
|
||||
{
|
||||
urls: 'turn:188.148.133.173:3478',
|
||||
username: 'feab95c3fcd147a2a96a3d3590bf9cda',
|
||||
credential: '654cafd885424b7fb974e65f631f25f9',
|
||||
urls: 'turn:global.relay.metered.ca:80?transport=tcp',
|
||||
username: 'cfd3dccc1b9162a136a80482',
|
||||
credential: '1TgyKEd7B6iUxIn9',
|
||||
},
|
||||
{
|
||||
urls: 'turn:global.relay.metered.ca:443',
|
||||
username: 'cfd3dccc1b9162a136a80482',
|
||||
credential: '1TgyKEd7B6iUxIn9',
|
||||
},
|
||||
{
|
||||
urls: 'turns:global.relay.metered.ca:443?transport=tcp',
|
||||
username: 'cfd3dccc1b9162a136a80482',
|
||||
credential: '1TgyKEd7B6iUxIn9',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
export const streamSaverUrl =
|
||||
process.env.REACT_APP_STREAMSAVER_URL ??
|
||||
// If you would like to host your own RemnantChat instance with an
|
||||
import.meta.env.VITE_STREAMSAVER_URL ??
|
||||
// If you would like to host your own Chitchatter instance with an
|
||||
// alternative StreamSaver fork to facilitate file sharing, change this
|
||||
// string to its respective .mitm.html URL.
|
||||
//
|
||||
|
@ -8,12 +8,12 @@ let trackerUrls: string[] | undefined = [
|
||||
// https://github.com/dmotz/trystero/blob/694f49974974cc9df8b621db09215d6df10fad09/src/torrent.js#L27-L33
|
||||
]
|
||||
|
||||
// If a tracker URL has been provided via the REACT_APP_TRACKER_URL environment
|
||||
// If a tracker URL has been provided via the VITE_TRACKER_URL environment
|
||||
// variable, prioritize using it. This is mainly relevant for local development
|
||||
// when using the `npm run dev` script. If you are hosting your own RemnantChat
|
||||
// instance, consider populating the trackerUrls above instead.
|
||||
if (process.env.REACT_APP_TRACKER_URL) {
|
||||
trackerUrls.unshift(process.env.REACT_APP_TRACKER_URL)
|
||||
if (import.meta.env.VITE_TRACKER_URL) {
|
||||
trackerUrls.unshift(import.meta.env.VITE_TRACKER_URL)
|
||||
}
|
||||
|
||||
// If no tracker URL overrides have been provided, set trackerUrls to undefined
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
import { ColorMode, UserSettings } from 'models/settings'
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import { encryption } from 'services/Encryption'
|
||||
|
||||
export interface SettingsContextProps {
|
||||
updateUserSettings: (settings: Partial<UserSettings>) => Promise<void>
|
||||
@ -17,7 +17,7 @@ export const SettingsContext = createContext<SettingsContextProps>({
|
||||
playSoundOnNewMessage: true,
|
||||
showNotificationOnNewMessage: true,
|
||||
showActiveTypingStatus: true,
|
||||
publicKey: encryptionService.cryptoKeyStub,
|
||||
privateKey: encryptionService.cryptoKeyStub,
|
||||
publicKey: encryption.cryptoKeyStub,
|
||||
privateKey: encryption.cryptoKeyStub,
|
||||
}),
|
||||
})
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { createContext, Dispatch, SetStateAction } from 'react'
|
||||
|
||||
import { AlertOptions } from 'models/shell'
|
||||
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
|
||||
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
|
||||
import {
|
||||
AudioState,
|
||||
ScreenShareState,
|
||||
VideoState,
|
||||
Peer,
|
||||
AudioChannel,
|
||||
PeerAudioChannelState,
|
||||
AudioChannelName,
|
||||
} from 'models/chat'
|
||||
import { PeerConnectionType } from 'lib/PeerRoom'
|
||||
import { ConnectionTestResults } from 'components/Shell/useConnectionTest'
|
||||
import { TrackerConnection } from 'services/ConnectionTest/ConnectionTest'
|
||||
import { TrackerConnection } from 'lib/ConnectionTest'
|
||||
|
||||
interface ShellContextProps {
|
||||
isEmbedded: boolean
|
||||
@ -27,14 +35,14 @@ interface ShellContextProps {
|
||||
setPeerConnectionTypes: Dispatch<
|
||||
SetStateAction<Record<string, PeerConnectionType>>
|
||||
>
|
||||
audioState: AudioState
|
||||
setAudioState: Dispatch<SetStateAction<AudioState>>
|
||||
audioChannelState: PeerAudioChannelState
|
||||
setAudioChannelState: Dispatch<SetStateAction<PeerAudioChannelState>>
|
||||
videoState: VideoState
|
||||
setVideoState: Dispatch<SetStateAction<VideoState>>
|
||||
screenState: ScreenShareState
|
||||
setScreenState: Dispatch<SetStateAction<ScreenShareState>>
|
||||
peerAudios: Record<string, HTMLAudioElement>
|
||||
setPeerAudios: Dispatch<SetStateAction<Record<string, HTMLAudioElement>>>
|
||||
peerAudioChannels: Record<string, AudioChannel>
|
||||
setPeerAudioChannels: Dispatch<SetStateAction<Record<string, AudioChannel>>>
|
||||
customUsername: string
|
||||
setCustomUsername: Dispatch<SetStateAction<string>>
|
||||
connectionTestResults: ConnectionTestResults
|
||||
@ -60,14 +68,17 @@ export const ShellContext = createContext<ShellContextProps>({
|
||||
setIsServerConnectionFailureDialogOpen: () => {},
|
||||
peerConnectionTypes: {},
|
||||
setPeerConnectionTypes: () => {},
|
||||
audioState: AudioState.STOPPED,
|
||||
setAudioState: () => {},
|
||||
audioChannelState: {
|
||||
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
|
||||
[AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED,
|
||||
},
|
||||
setAudioChannelState: () => {},
|
||||
videoState: VideoState.STOPPED,
|
||||
setVideoState: () => {},
|
||||
screenState: ScreenShareState.NOT_SHARING,
|
||||
setScreenState: () => {},
|
||||
peerAudios: {},
|
||||
setPeerAudios: () => {},
|
||||
peerAudioChannels: {},
|
||||
setPeerAudioChannels: () => {},
|
||||
customUsername: '',
|
||||
setCustomUsername: () => {},
|
||||
connectionTestResults: {
|
||||
|
20
src/index.css
Normal file
20
src/index.css
Normal file
@ -0,0 +1,20 @@
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
@tailwind base
|
||||
@tailwind components
|
||||
@tailwind utilities
|
||||
|
||||
ol
|
||||
@apply pl-4 list-decimal
|
||||
|
||||
ul
|
||||
@apply pl-4 list-disc
|
@ -2,10 +2,29 @@ import './polyfills'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import 'typeface-roboto'
|
||||
|
||||
import './index.sass'
|
||||
import 'modern-normalize/modern-normalize.css'
|
||||
import './index.css'
|
||||
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
|
||||
import Init from './Init'
|
||||
import reportWebVitals from './reportWebVitals'
|
||||
|
||||
// NOTE: This is a workaround for MUI components attempting to access theme code
|
||||
// before it has loaded.
|
||||
// See: https://stackoverflow.com/a/76017295/470685
|
||||
;<ThemeProvider theme={createTheme()} />
|
||||
|
||||
// NOTE: This is a workaround for SyntaxHighlighter not working reliably in the
|
||||
// EmbedCodeDialog component. It seems to have the effect of warming some
|
||||
// sort of internal cache that avoids a race condition within
|
||||
// SyntaxHighlighter.
|
||||
// See: https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/513
|
||||
ReactDOM.createRoot(document.createElement('div')).render(
|
||||
<SyntaxHighlighter language="" children={''} />
|
||||
)
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(<Init />)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getTrackers } from 'trystero/torrent'
|
||||
import { getRelaySockets } from 'trystero/torrent'
|
||||
import { rtcConfig } from 'config/rtcConfig'
|
||||
import { parseCandidate } from 'sdp'
|
||||
|
||||
@ -104,17 +104,15 @@ export class ConnectionTest extends EventTarget {
|
||||
}
|
||||
|
||||
testTrackerConnection() {
|
||||
const trackers = getTrackers()
|
||||
const relaySockets = Object.values(getRelaySockets())
|
||||
|
||||
const trackerSockets = Object.values(trackers)
|
||||
|
||||
if (trackerSockets.length === 0) {
|
||||
if (relaySockets.length === 0) {
|
||||
// Trystero has not yet initialized tracker sockets
|
||||
this.trackerConnection = TrackerConnection.SEARCHING
|
||||
return this.trackerConnection
|
||||
}
|
||||
|
||||
const readyStates = trackerSockets.map(({ readyState }) => readyState)
|
||||
const readyStates = relaySockets.map(({ readyState }) => readyState)
|
||||
|
||||
const haveAllTrackerConnectionsFailed = readyStates.every(
|
||||
readyState => readyState === WebSocket.CLOSED
|
@ -1,7 +1,8 @@
|
||||
import { joinRoom, Room, BaseRoomConfig } from 'trystero'
|
||||
import { TorrentRoomConfig } from 'trystero/torrent'
|
||||
import { joinRoom, Room, BaseRoomConfig, DataPayload } from 'trystero/torrent'
|
||||
import { RelayConfig } from 'trystero/torrent'
|
||||
|
||||
import { sleep } from 'utils'
|
||||
import { sleep } from 'lib/sleep'
|
||||
import { StreamType } from 'models/chat'
|
||||
|
||||
export enum PeerHookType {
|
||||
NEW_PEER = 'NEW_PEER',
|
||||
@ -27,7 +28,7 @@ const streamQueueAddDelay = 1000
|
||||
export class PeerRoom {
|
||||
private room: Room
|
||||
|
||||
private roomConfig: TorrentRoomConfig & BaseRoomConfig
|
||||
private roomConfig: RelayConfig & BaseRoomConfig
|
||||
|
||||
private peerJoinHandlers: Map<
|
||||
PeerHookType,
|
||||
@ -48,7 +49,19 @@ export class PeerRoom {
|
||||
|
||||
private isProcessingPendingStreams = false
|
||||
|
||||
constructor(config: TorrentRoomConfig & BaseRoomConfig, roomId: string) {
|
||||
private processPendingStreams = async () => {
|
||||
if (this.isProcessingPendingStreams) return
|
||||
|
||||
this.isProcessingPendingStreams = true
|
||||
|
||||
while (this.streamQueue.length > 0) {
|
||||
await this.streamQueue.shift()?.()
|
||||
}
|
||||
|
||||
this.isProcessingPendingStreams = false
|
||||
}
|
||||
|
||||
constructor(config: RelayConfig & BaseRoomConfig, roomId: string) {
|
||||
this.roomConfig = config
|
||||
this.room = joinRoom(this.roomConfig, roomId)
|
||||
|
||||
@ -155,34 +168,26 @@ export class PeerRoom {
|
||||
return peerConnections
|
||||
}
|
||||
|
||||
makeAction = <T>(namespace: string) => {
|
||||
makeAction = <T extends DataPayload>(namespace: string) => {
|
||||
return this.room.makeAction<T>(namespace)
|
||||
}
|
||||
|
||||
addStream = (...args: Parameters<Room['addStream']>) => {
|
||||
addStream = (
|
||||
stream: Parameters<Room['addStream']>[0],
|
||||
targetPeers: Parameters<Room['addStream']>[1],
|
||||
metadata: { type: StreamType }
|
||||
) => {
|
||||
// New streams need to be added as a delayed queue to prevent race
|
||||
// conditions on the receiver's end where streams and their metadata get
|
||||
// mixed up.
|
||||
this.streamQueue.push(
|
||||
() => Promise.all(this.room.addStream(...args)),
|
||||
() => Promise.all(this.room.addStream(stream, targetPeers, metadata)),
|
||||
() => sleep(streamQueueAddDelay)
|
||||
)
|
||||
|
||||
this.processPendingStreams()
|
||||
}
|
||||
|
||||
private processPendingStreams = async () => {
|
||||
if (this.isProcessingPendingStreams) return
|
||||
|
||||
this.isProcessingPendingStreams = true
|
||||
|
||||
while (this.streamQueue.length > 0) {
|
||||
await this.streamQueue.shift()?.()
|
||||
}
|
||||
|
||||
this.isProcessingPendingStreams = false
|
||||
}
|
||||
|
||||
removeStream: Room['removeStream'] = (stream, targetPeers) => {
|
||||
return this.room.removeStream(stream, targetPeers)
|
||||
}
|
7
src/lib/Time/Time.ts
Normal file
7
src/lib/Time/Time.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class Time {
|
||||
now = () => {
|
||||
return Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
export const time = new Time()
|
1
src/lib/Time/index.ts
Normal file
1
src/lib/Time/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './Time'
|
4
src/lib/sleep.ts
Normal file
4
src/lib/sleep.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const sleep = (milliseconds: number): Promise<void> =>
|
||||
new Promise<void>(res => {
|
||||
setTimeout(res, milliseconds)
|
||||
})
|
@ -1,8 +1,3 @@
|
||||
export const sleep = (milliseconds: number): Promise<void> =>
|
||||
new Promise<void>(res => {
|
||||
setTimeout(res, milliseconds)
|
||||
})
|
||||
|
||||
export const isRecord = (variable: any): variable is Record<string, any> => {
|
||||
return (
|
||||
typeof variable === 'object' &&
|
@ -1,4 +1,4 @@
|
||||
export interface UnsentMessage {
|
||||
export interface UnsentMessage extends Record<string, any> {
|
||||
id: string
|
||||
text: string
|
||||
timeSent: number
|
||||
@ -31,9 +31,10 @@ export enum VideoState {
|
||||
STOPPED = 'STOPPED',
|
||||
}
|
||||
|
||||
export enum VideoStreamType {
|
||||
export enum StreamType {
|
||||
WEBCAM = 'WEBCAM',
|
||||
SCREEN_SHARE = 'SCREEN_SHARE',
|
||||
MICROPHONE = 'MICROPHONE',
|
||||
}
|
||||
|
||||
export enum ScreenShareState {
|
||||
@ -47,12 +48,21 @@ export enum PeerVerificationState {
|
||||
VERIFIED,
|
||||
}
|
||||
|
||||
export enum AudioChannelName {
|
||||
MICROPHONE = 'microphone',
|
||||
SCREEN_SHARE = 'screen-share',
|
||||
}
|
||||
|
||||
export type AudioChannel = Partial<Record<AudioChannelName, HTMLAudioElement>>
|
||||
|
||||
export type PeerAudioChannelState = Record<AudioChannelName, AudioState>
|
||||
|
||||
export interface Peer {
|
||||
peerId: string
|
||||
userId: string
|
||||
publicKey: CryptoKey
|
||||
customUsername: string
|
||||
audioState: AudioState
|
||||
audioChannelState: PeerAudioChannelState
|
||||
videoState: VideoState
|
||||
screenShareState: ScreenShareState
|
||||
offeredFileId: string | null
|
||||
@ -73,11 +83,11 @@ export const isInlineMedia = (
|
||||
return 'magnetURI' in message
|
||||
}
|
||||
|
||||
export interface FileOfferMetadata {
|
||||
export interface FileOfferMetadata extends Record<string, any> {
|
||||
magnetURI: string
|
||||
isAllInlineMedia: boolean
|
||||
}
|
||||
|
||||
export interface TypingStatus {
|
||||
export interface TypingStatus extends Record<string, any> {
|
||||
isTyping: boolean
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useContext, useEffect } from 'react'
|
||||
import MuiMarkdown from 'mui-markdown'
|
||||
import Box from '@mui/material/Box'
|
||||
import useTheme from '@mui/material/styles/useTheme'
|
||||
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import {
|
||||
@ -8,8 +9,6 @@ import {
|
||||
messageCharacterSizeLimit,
|
||||
} from 'config/messaging'
|
||||
|
||||
import './index.sass'
|
||||
|
||||
const messageTranscriptSizeLimitFormatted = Intl.NumberFormat().format(
|
||||
messageTranscriptSizeLimit
|
||||
)
|
||||
@ -20,13 +19,24 @@ const messageCharacterSizeLimitFormatted = Intl.NumberFormat().format(
|
||||
|
||||
export const About = () => {
|
||||
const { setTitle } = useContext(ShellContext)
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('About')
|
||||
}, [setTitle])
|
||||
|
||||
return (
|
||||
<Box className="About max-w-3xl mx-auto p-4">
|
||||
<Box
|
||||
className="About"
|
||||
sx={{
|
||||
p: 2,
|
||||
mx: 'auto',
|
||||
maxWidth: theme.breakpoints.values.md,
|
||||
'& p': {
|
||||
mb: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MuiMarkdown>
|
||||
{`
|
||||
### User Guide
|
||||
|
@ -1,3 +0,0 @@
|
||||
.About
|
||||
p
|
||||
@apply mb-4
|
@ -1,20 +1,30 @@
|
||||
import { useContext, useEffect } from 'react'
|
||||
import Box from '@mui/material/Box'
|
||||
import MuiMarkdown from 'mui-markdown'
|
||||
import useTheme from '@mui/material/styles/useTheme'
|
||||
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
|
||||
import './index.sass'
|
||||
|
||||
export const Disclaimer = () => {
|
||||
const { setTitle } = useContext(ShellContext)
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
setTitle('Disclaimer')
|
||||
}, [setTitle])
|
||||
|
||||
return (
|
||||
<Box className="Disclaimer max-w-3xl mx-auto p-4">
|
||||
<Box
|
||||
className="Disclaimer"
|
||||
sx={{
|
||||
p: 2,
|
||||
mx: 'auto',
|
||||
maxWidth: theme.breakpoints.values.md,
|
||||
'& p': {
|
||||
mb: 2,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MuiMarkdown>
|
||||
{`
|
||||
### Interpretation and Definitions
|
||||
|
@ -1,6 +0,0 @@
|
||||
.Disclaimer
|
||||
ul
|
||||
@apply my-4
|
||||
|
||||
p
|
||||
@apply mb-4
|
67
src/pages/Home/CommunityRoomSelector.tsx
Normal file
67
src/pages/Home/CommunityRoomSelector.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { useState, SyntheticEvent } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Button from '@mui/material/Button'
|
||||
import Box from '@mui/material/Box'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import TextField from '@mui/material/TextField'
|
||||
import Autocomplete from '@mui/material/Autocomplete'
|
||||
import Accordion from '@mui/material/Accordion'
|
||||
import AccordionSummary from '@mui/material/AccordionSummary'
|
||||
import AccordionDetails from '@mui/material/AccordionDetails'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
|
||||
import { communityRoomNames } from 'config/communityRooms'
|
||||
|
||||
export const CommunityRoomSelector = () => {
|
||||
const navigate = useNavigate()
|
||||
const [selectedRoom, setSelectedRoom] = useState<string | null>(null)
|
||||
|
||||
const handleRoomNameChange = (
|
||||
_event: SyntheticEvent<Element, Event>,
|
||||
roomName: string | null
|
||||
) => {
|
||||
setSelectedRoom(roomName)
|
||||
}
|
||||
|
||||
const handleJoinClick = () => {
|
||||
navigate(`/public/${selectedRoom}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel1-content"
|
||||
id="panel1-header"
|
||||
sx={{
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
Community rooms
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body1">
|
||||
You can also chat in a public community room. You'll be anonymous, but
|
||||
be careful what information you choose to share.
|
||||
</Typography>
|
||||
<Box display="flex" mt={2} gap={1}>
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
options={communityRoomNames}
|
||||
value={selectedRoom}
|
||||
renderInput={params => <TextField {...params} label="Room" />}
|
||||
onChange={handleRoomNameChange}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={selectedRoom === null}
|
||||
onClick={handleJoinClick}
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
)
|
||||
}
|
@ -10,15 +10,21 @@ import IconButton from '@mui/material/IconButton'
|
||||
import MuiLink from '@mui/material/Link'
|
||||
|
||||
import Cached from '@mui/icons-material/Cached'
|
||||
import useTheme from '@mui/material/styles/useTheme'
|
||||
import styled from '@mui/material/styles/styled'
|
||||
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
import { routes } from 'config/routes'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
import { ReactComponent as Logo } from 'img/logo.svg'
|
||||
import { Form, Main } from 'components/Elements'
|
||||
import Logo from 'img/logo.svg?react'
|
||||
|
||||
import { EmbedCodeDialog } from './EmbedCodeDialog'
|
||||
import { CommunityRoomSelector } from './CommunityRoomSelector'
|
||||
|
||||
const StyledLogo = styled(Logo)({})
|
||||
|
||||
interface HomeProps {
|
||||
userId: string
|
||||
@ -26,6 +32,7 @@ interface HomeProps {
|
||||
|
||||
export function Home({ userId }: HomeProps) {
|
||||
const { setTitle } = useContext(ShellContext)
|
||||
const theme = useTheme()
|
||||
const [roomName, setRoomName] = useState(uuid())
|
||||
const [showEmbedCode, setShowEmbedCode] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
@ -63,11 +70,29 @@ export function Home({ userId }: HomeProps) {
|
||||
|
||||
return (
|
||||
<Box className="Home">
|
||||
<main className="mt-6 px-4 max-w-3xl text-center mx-auto">
|
||||
<Main
|
||||
sx={{
|
||||
maxWidth: theme.breakpoints.values.md,
|
||||
mt: 3,
|
||||
mx: 'auto',
|
||||
px: 2,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Link to={routes.ABOUT}>
|
||||
<Logo className="px-1 pb-4 mx-auto max-w-md" />
|
||||
<StyledLogo
|
||||
sx={{
|
||||
px: 0.5,
|
||||
pb: 2,
|
||||
mx: 'auto',
|
||||
maxWidth: theme.breakpoints.values.sm,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
<form onSubmit={handleFormSubmit} className="max-w-xl mx-auto">
|
||||
<Form
|
||||
onSubmit={handleFormSubmit}
|
||||
sx={{ maxWidth: theme.breakpoints.values.sm, mx: 'auto' }}
|
||||
>
|
||||
<Typography sx={{ mb: 2 }}>
|
||||
Your username:{' '}
|
||||
<PeerNameDisplay paragraph={false} sx={{ fontWeight: 'bold' }}>
|
||||
@ -76,7 +101,7 @@ export function Home({ userId }: HomeProps) {
|
||||
</Typography>
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
label="Room name (generated client-side)"
|
||||
label="Room name (generated on your device)"
|
||||
variant="outlined"
|
||||
value={roomName}
|
||||
onChange={handleRoomNameChange}
|
||||
@ -135,14 +160,28 @@ export function Home({ userId }: HomeProps) {
|
||||
Get embed code
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</main>
|
||||
</Form>
|
||||
</Main>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Box className="max-w-3xl text-center mx-auto px-4">
|
||||
<Box maxWidth={theme.breakpoints.values.sm} mx="auto" px={2}>
|
||||
<CommunityRoomSelector />
|
||||
</Box>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Box
|
||||
sx={{
|
||||
maxWidth: theme.breakpoints.values.sm,
|
||||
mx: 'auto',
|
||||
textAlign: 'center',
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">
|
||||
The secure communication tool that is designed for simplicity,
|
||||
privacy. All interaction between you and your online peers is
|
||||
encrypted. Conversation records are dumped once everyone leaves.
|
||||
WEBRTC settings must be enabled in your browser. Is webrtc{' '}
|
||||
<MuiLink href="https://webrtc-security.github.io/" target="_blank">
|
||||
secure
|
||||
</MuiLink>
|
||||
? All interaction between you and your online peers is encrypted.
|
||||
Conversation records are dumped once everyone leaves.
|
||||
</Typography>
|
||||
<Box className="max-w-3xl text-center mx-auto my-4 px-4">
|
||||
{' '}
|
||||
@ -150,19 +189,7 @@ export function Home({ userId }: HomeProps) {
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Box className="max-w-3xl text-center mx-auto my-4 px-4">
|
||||
<Typography variant="body1">Official Public Rooms to Join:</Typography>
|
||||
<MuiLink
|
||||
href="/public/general"
|
||||
target="_blank"
|
||||
sx={theme => ({
|
||||
color: theme.palette.text.primary,
|
||||
})}
|
||||
>
|
||||
<Typography variant="body1">• General Chat</Typography>
|
||||
</MuiLink>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="body1" sx={{ textAlign: 'center', mb: 1 }}>
|
||||
Licensed under{' '}
|
||||
<MuiLink
|
||||
href="https://githaven.org/Shiloh/remnantchat/src/branch/main/LICENSE"
|
||||
@ -175,9 +202,8 @@ export function Home({ userId }: HomeProps) {
|
||||
href="https://githaven.org/Shiloh/remnantchat/src/branch/main/README.md"
|
||||
target="_blank"
|
||||
>
|
||||
read the docs
|
||||
read the docs.
|
||||
</MuiLink>
|
||||
.
|
||||
</Typography>
|
||||
<EmbedCodeDialog
|
||||
showEmbedCode={showEmbedCode}
|
||||
|
@ -3,9 +3,9 @@ import { Room } from 'components/Room'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { NotificationService } from 'services/Notification'
|
||||
import { notification } from 'services/Notification'
|
||||
import { PasswordPrompt } from 'components/PasswordPrompt'
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import { encryption } from 'services/Encryption'
|
||||
|
||||
interface PublicRoomProps {
|
||||
userId: string
|
||||
@ -22,7 +22,7 @@ export function PrivateRoom({ userId }: PublicRoomProps) {
|
||||
const [secret, setSecret] = useState(urlParams.get('secret') ?? '')
|
||||
|
||||
useEffect(() => {
|
||||
NotificationService.requestPermission()
|
||||
notification.requestPermission()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@ -31,7 +31,7 @@ export function PrivateRoom({ userId }: PublicRoomProps) {
|
||||
|
||||
const handlePasswordEntered = async (password: string) => {
|
||||
if (password.length !== 0)
|
||||
setSecret(await encryptionService.encodePassword(roomId, password))
|
||||
setSecret(await encryption.encodePassword(roomId, password))
|
||||
}
|
||||
|
||||
if (urlParams.has('pwd') && !urlParams.has('secret'))
|
||||
|
@ -3,7 +3,7 @@ import { Room } from 'components/Room'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { NotificationService } from 'services/Notification'
|
||||
import { notification } from 'services/Notification'
|
||||
|
||||
interface PublicRoomProps {
|
||||
userId: string
|
||||
@ -14,7 +14,7 @@ export function PublicRoom({ userId }: PublicRoomProps) {
|
||||
const { setTitle } = useContext(ShellContext)
|
||||
|
||||
useEffect(() => {
|
||||
NotificationService.requestPermission()
|
||||
notification.requestPermission()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -10,15 +10,15 @@ import FormControlLabel from '@mui/material/FormControlLabel'
|
||||
import Paper from '@mui/material/Paper'
|
||||
import useTheme from '@mui/material/styles/useTheme'
|
||||
|
||||
import { settingsService } from 'services/Settings'
|
||||
import { NotificationService } from 'services/Notification'
|
||||
import { settings } from 'services/Settings'
|
||||
import { notification } from 'services/Notification'
|
||||
import { ShellContext } from 'contexts/ShellContext'
|
||||
import { StorageContext } from 'contexts/StorageContext'
|
||||
import { SettingsContext } from 'contexts/SettingsContext'
|
||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
||||
import { ConfirmDialog } from 'components/ConfirmDialog'
|
||||
|
||||
import { isErrorWithMessage } from '../../utils'
|
||||
import { isErrorWithMessage } from '../../lib/type-guards'
|
||||
|
||||
interface SettingsProps {
|
||||
userId: string
|
||||
@ -45,7 +45,7 @@ export const Settings = ({ userId }: SettingsProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
;(async () => {
|
||||
await NotificationService.requestPermission()
|
||||
await notification.requestPermission()
|
||||
|
||||
// This state needs to be set to cause a rerender so that
|
||||
// areNotificationsAvailable is up-to-date.
|
||||
@ -93,7 +93,7 @@ export const Settings = ({ userId }: SettingsProps) => {
|
||||
|
||||
const handleExportSettingsClick = async () => {
|
||||
try {
|
||||
await settingsService.exportSettings(getUserSettings())
|
||||
await settings.exportSettings(getUserSettings())
|
||||
} catch (e) {
|
||||
if (isErrorWithMessage(e)) {
|
||||
showAlert(e.message, { severity: 'error' })
|
||||
@ -103,7 +103,7 @@ export const Settings = ({ userId }: SettingsProps) => {
|
||||
|
||||
const handleImportSettingsClick = async ([[, file]]: Result[]) => {
|
||||
try {
|
||||
const userSettings = await settingsService.importSettings(file)
|
||||
const userSettings = await settings.importSettings(file)
|
||||
|
||||
updateUserSettings(userSettings)
|
||||
|
||||
@ -115,10 +115,10 @@ export const Settings = ({ userId }: SettingsProps) => {
|
||||
}
|
||||
}
|
||||
|
||||
const areNotificationsAvailable = NotificationService.permission === 'granted'
|
||||
const areNotificationsAvailable = notification.permission === 'granted'
|
||||
|
||||
return (
|
||||
<Box className="max-w-3xl mx-auto p-4">
|
||||
<Box sx={{ p: 2, mx: 'auto', maxWidth: theme.breakpoints.values.md }}>
|
||||
<Typography
|
||||
variant="h2"
|
||||
sx={{
|
||||
|
@ -1,9 +1 @@
|
||||
import 'webrtc-adapter'
|
||||
import { Buffer } from 'buffer'
|
||||
|
||||
// @ts-ignore
|
||||
import process from 'process/browser'
|
||||
|
||||
// Polyfill
|
||||
window.Buffer = Buffer
|
||||
window.process = process
|
||||
|
2
src/react-app-env.d.ts
vendored
2
src/react-app-env.d.ts
vendored
@ -1 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
|
@ -1,73 +0,0 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
|
||||
// This service worker can be customized!
|
||||
// See https://developers.google.com/web/tools/workbox/modules
|
||||
// for the list of available Workbox modules, or add any other
|
||||
// code you'd like.
|
||||
// You can also remove this file if you'd prefer not to use a
|
||||
// service worker, and the Workbox build step will be skipped.
|
||||
|
||||
import { clientsClaim } from 'workbox-core'
|
||||
import { ExpirationPlugin } from 'workbox-expiration'
|
||||
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'
|
||||
import { registerRoute } from 'workbox-routing'
|
||||
import { StaleWhileRevalidate } from 'workbox-strategies'
|
||||
|
||||
clientsClaim()
|
||||
|
||||
// Precache all of the assets generated by your build process.
|
||||
// Their URLs are injected into the manifest variable below.
|
||||
// This variable must be present somewhere in your service worker file,
|
||||
// even if you decide not to use precaching. See https://cra.link/PWA
|
||||
precacheAndRoute(self.__WB_MANIFEST)
|
||||
|
||||
// Set up App Shell-style routing, so that all navigation requests
|
||||
// are fulfilled with your index.html shell. Learn more at
|
||||
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
|
||||
registerRoute(
|
||||
// Return false to exempt requests from being fulfilled by index.html.
|
||||
({ request, url }) => {
|
||||
// If this isn't a navigation, skip.
|
||||
if (request.mode !== 'navigate') {
|
||||
return false
|
||||
} // If this is a URL that starts with /_, skip.
|
||||
|
||||
if (url.pathname.startsWith('/_')) {
|
||||
return false
|
||||
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
|
||||
|
||||
if (url.pathname.match(fileExtensionRegexp)) {
|
||||
return false
|
||||
} // Return true to signal that we want to use the handler.
|
||||
|
||||
return true
|
||||
},
|
||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||
)
|
||||
|
||||
// An example runtime caching route for requests that aren't handled by the
|
||||
// precache, in this case same-origin .png requests like those from in public/
|
||||
registerRoute(
|
||||
// Add in any other file extensions or routing criteria as needed.
|
||||
({ url }) =>
|
||||
url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'images',
|
||||
plugins: [
|
||||
// Ensure that once this runtime cache reaches a maximum size the
|
||||
// least-recently used images are removed.
|
||||
new ExpirationPlugin({ maxEntries: 50 }),
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
// This allows the web app to trigger skipWaiting via
|
||||
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||
self.addEventListener('message', event => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting()
|
||||
}
|
||||
})
|
||||
|
||||
// Any other custom service worker logic can go here.
|
@ -1,141 +0,0 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// This lets the app load faster on subsequent visits in production, and gives
|
||||
// it offline capabilities. However, it also means that developers (and users)
|
||||
// will only see deployed updates on subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://cra.link/PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
)
|
||||
|
||||
export function register(config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
return
|
||||
}
|
||||
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config)
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://cra.link/PWA'
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing
|
||||
if (installingWorker == null) {
|
||||
return
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||
)
|
||||
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration)
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.')
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then(registration => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then(registration => {
|
||||
registration.unregister()
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.message)
|
||||
})
|
||||
}
|
||||
}
|
@ -110,4 +110,4 @@ export class EncryptionService {
|
||||
}
|
||||
}
|
||||
|
||||
export const encryptionService = new EncryptionService()
|
||||
export const encryption = new EncryptionService()
|
||||
|
@ -1,18 +1,17 @@
|
||||
export class NotificationService {
|
||||
static permission: NotificationPermission
|
||||
permission: NotificationPermission = 'default'
|
||||
|
||||
static requestPermission = async () => {
|
||||
if (NotificationService.permission === 'granted') return
|
||||
requestPermission = async () => {
|
||||
if (this.permission === 'granted') return
|
||||
|
||||
NotificationService.permission = await Notification.requestPermission()
|
||||
this.permission = await Notification.requestPermission()
|
||||
}
|
||||
|
||||
static showNotification = (
|
||||
message: string,
|
||||
options?: NotificationOptions
|
||||
) => {
|
||||
if (NotificationService.permission !== 'granted') return
|
||||
showNotification = (message: string, options?: NotificationOptions) => {
|
||||
if (this.permission !== 'granted') return
|
||||
|
||||
new Notification(message, options)
|
||||
}
|
||||
}
|
||||
|
||||
export const notification = new NotificationService()
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ColorMode, UserSettings } from 'models/settings'
|
||||
import { AllowedKeyType, encryptionService } from 'services/Encryption'
|
||||
import { AllowedKeyType, encryption } from 'services/Encryption'
|
||||
|
||||
export interface SerializedUserSettings
|
||||
extends Omit<UserSettings, 'publicKey' | 'privateKey'> {
|
||||
@ -42,13 +42,9 @@ export class SerializationService {
|
||||
...userSettingsRest
|
||||
} = userSettings
|
||||
|
||||
const publicKey = await encryptionService.stringifyCryptoKey(
|
||||
publicCryptoKey
|
||||
)
|
||||
const publicKey = await encryption.stringifyCryptoKey(publicCryptoKey)
|
||||
|
||||
const privateKey = await encryptionService.stringifyCryptoKey(
|
||||
privateCryptoKey
|
||||
)
|
||||
const privateKey = await encryption.stringifyCryptoKey(privateCryptoKey)
|
||||
|
||||
return {
|
||||
...userSettingsRest,
|
||||
@ -66,11 +62,11 @@ export class SerializationService {
|
||||
...userSettingsForIndexedDbRest
|
||||
} = serializedUserSettings
|
||||
|
||||
const publicKey = await encryptionService.parseCryptoKeyString(
|
||||
const publicKey = await encryption.parseCryptoKeyString(
|
||||
publicCryptoKeyString,
|
||||
AllowedKeyType.PUBLIC
|
||||
)
|
||||
const privateKey = await encryptionService.parseCryptoKeyString(
|
||||
const privateKey = await encryption.parseCryptoKeyString(
|
||||
privateCryptoKeyString,
|
||||
AllowedKeyType.PRIVATE
|
||||
)
|
||||
@ -83,4 +79,4 @@ export class SerializationService {
|
||||
}
|
||||
}
|
||||
|
||||
export const serializationService = new SerializationService()
|
||||
export const serialization = new SerializationService()
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import { UserSettings } from 'models/settings'
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import { encryption } from 'services/Encryption'
|
||||
import {
|
||||
isSerializedUserSettings,
|
||||
serializationService,
|
||||
serialization,
|
||||
} from 'services/Serialization/Serialization'
|
||||
|
||||
class InvalidFileError extends Error {
|
||||
@ -16,7 +16,7 @@ const encryptionTestTarget = 'remnantchat'
|
||||
export class SettingsService {
|
||||
exportSettings = async (userSettings: UserSettings) => {
|
||||
const serializedUserSettings =
|
||||
await serializationService.serializeUserSettings(userSettings)
|
||||
await serialization.serializeUserSettings(userSettings)
|
||||
|
||||
const blob = new Blob([JSON.stringify(serializedUserSettings)], {
|
||||
type: 'application/json;charset=utf-8',
|
||||
@ -44,14 +44,14 @@ export class SettingsService {
|
||||
}
|
||||
|
||||
const deserializedUserSettings =
|
||||
await serializationService.deserializeUserSettings(parsedFileResult)
|
||||
await serialization.deserializeUserSettings(parsedFileResult)
|
||||
|
||||
const encryptedString = await encryptionService.encryptString(
|
||||
const encryptedString = await encryption.encryptString(
|
||||
deserializedUserSettings.publicKey,
|
||||
encryptionTestTarget
|
||||
)
|
||||
|
||||
const decryptedString = await encryptionService.decryptString(
|
||||
const decryptedString = await encryption.decryptString(
|
||||
deserializedUserSettings.privateKey,
|
||||
encryptedString
|
||||
)
|
||||
@ -77,4 +77,4 @@ export class SettingsService {
|
||||
}
|
||||
}
|
||||
|
||||
export const settingsService = new SettingsService()
|
||||
export const settings = new SettingsService()
|
||||
|
@ -3,12 +3,16 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
jest.mock('secure-file-transfer', () => ({
|
||||
vi.mock('trystero')
|
||||
vi.mock('trystero/torrent')
|
||||
|
||||
vi.mock('secure-file-transfer', () => ({
|
||||
__esModule: true,
|
||||
FileTransfer: class FileTransfer {
|
||||
rescindAll() {}
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import { vi } from 'vitest'
|
||||
import { encryption } from 'services/Encryption'
|
||||
|
||||
export const mockEncryptionService = encryptionService
|
||||
export const mockEncryptionService = encryption
|
||||
|
||||
mockEncryptionService.generateKeyPair = jest.fn(async () => ({
|
||||
publicKey: encryptionService.cryptoKeyStub,
|
||||
privateKey: encryptionService.cryptoKeyStub,
|
||||
mockEncryptionService.generateKeyPair = vi.fn(async () => ({
|
||||
publicKey: encryption.cryptoKeyStub,
|
||||
privateKey: encryption.cryptoKeyStub,
|
||||
}))
|
||||
|
||||
mockEncryptionService.encodePassword = async () => ''
|
||||
@ -12,4 +13,4 @@ mockEncryptionService.encodePassword = async () => ''
|
||||
mockEncryptionService.stringifyCryptoKey = async () => ''
|
||||
|
||||
mockEncryptionService.parseCryptoKeyString = async () =>
|
||||
encryptionService.cryptoKeyStub
|
||||
encryption.cryptoKeyStub
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { UserSettings } from 'models/settings'
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import {
|
||||
serializationService,
|
||||
SerializedUserSettings,
|
||||
} from 'services/Serialization'
|
||||
import { encryption } from 'services/Encryption'
|
||||
import { serialization, SerializedUserSettings } from 'services/Serialization'
|
||||
|
||||
export const mockSerializedPublicKey = 'public key'
|
||||
export const mockSerializedPrivateKey = 'private key'
|
||||
|
||||
export const mockSerializationService = serializationService
|
||||
export const mockSerialization = serialization
|
||||
|
||||
mockSerializationService.serializeUserSettings = async (
|
||||
mockSerialization.serializeUserSettings = async (
|
||||
userSettings: UserSettings
|
||||
) => {
|
||||
const { publicKey, privateKey, ...userSettingsRest } = userSettings
|
||||
@ -22,14 +19,14 @@ mockSerializationService.serializeUserSettings = async (
|
||||
}
|
||||
}
|
||||
|
||||
mockSerializationService.deserializeUserSettings = async (
|
||||
mockSerialization.deserializeUserSettings = async (
|
||||
serializedUserSettings: SerializedUserSettings
|
||||
) => {
|
||||
const { publicKey, privateKey, ...userSettingsRest } = serializedUserSettings
|
||||
|
||||
return {
|
||||
publicKey: encryptionService.cryptoKeyStub,
|
||||
privateKey: encryptionService.cryptoKeyStub,
|
||||
publicKey: encryption.cryptoKeyStub,
|
||||
privateKey: encryption.cryptoKeyStub,
|
||||
...userSettingsRest,
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SettingsContextProps } from 'contexts/SettingsContext'
|
||||
import { ColorMode, UserSettings } from 'models/settings'
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import { encryption } from 'services/Encryption'
|
||||
|
||||
export const userSettingsContextStubFactory = (
|
||||
userSettingsOverrides: Partial<UserSettings> = {}
|
||||
@ -14,8 +14,8 @@ export const userSettingsContextStubFactory = (
|
||||
playSoundOnNewMessage: true,
|
||||
showNotificationOnNewMessage: true,
|
||||
showActiveTypingStatus: true,
|
||||
publicKey: encryptionService.cryptoKeyStub,
|
||||
privateKey: encryptionService.cryptoKeyStub,
|
||||
publicKey: encryption.cryptoKeyStub,
|
||||
privateKey: encryption.cryptoKeyStub,
|
||||
...userSettingsOverrides,
|
||||
}),
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ColorMode, UserSettings } from 'models/settings'
|
||||
import { encryptionService } from 'services/Encryption'
|
||||
import { encryption } from 'services/Encryption'
|
||||
|
||||
export const userSettingsStubFactory = (
|
||||
overrides: Partial<UserSettings> = {}
|
||||
@ -11,8 +11,8 @@ export const userSettingsStubFactory = (
|
||||
playSoundOnNewMessage: true,
|
||||
showNotificationOnNewMessage: true,
|
||||
showActiveTypingStatus: true,
|
||||
publicKey: encryptionService.cryptoKeyStub,
|
||||
privateKey: encryptionService.cryptoKeyStub,
|
||||
publicKey: encryption.cryptoKeyStub,
|
||||
privateKey: encryption.cryptoKeyStub,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user