Compare commits

..

7 Commits

Author SHA1 Message Date
Gary
d612d0fa11 About page: Fixed typos and links. 2024-01-10 10:36:44 -08:00
Gary
4baff5dcf4 main: added public pages section to Home page. 2024-01-09 16:52:27 -08:00
Gary
fec3f4a535 main: update to README 2024-01-09 15:14:37 -08:00
Gary
38bb81d758 main: change to favicon. 2024-01-09 14:51:44 -08:00
Gary
29aede18f9 main: created docker files 2024-01-03 19:02:30 -08:00
Gary
811c35bbd5 main: prelim update to documention. 2024-01-03 18:41:17 -08:00
Gary
6003270216 Main: Begin derived app rewrite into remnantchat. 2024-01-03 18:09:51 -08:00
105 changed files with 24807 additions and 20810 deletions

View File

@ -1,6 +1,5 @@
{ {
"extends": ["react-app", "plugin:prettier/recommended"], "extends": ["react-app"],
"plugins": ["prettier"],
"rules": { "rules": {
"import/order": [ "import/order": [
"error", "error",

View File

@ -1,25 +0,0 @@
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

View File

@ -1,33 +0,0 @@
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
View File

@ -9,9 +9,7 @@
/coverage /coverage
# production # production
/dist /build
/dev-dist
/src/config/rtcConfig.ts
# misc # misc
.DS_Store .DS_Store
@ -27,633 +25,3 @@ yarn-error.log*
# Editors # Editors
Session.vim 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

2
.nvmrc
View File

@ -1 +1 @@
v20.12.1 v18.12.1

View File

@ -1,3 +0,0 @@
{
"eslint.experimental.useFlatConfig": false
}

View File

@ -125,42 +125,30 @@ As well as the following [standard `<iframe />` attributes](https://developer.mo
- `referrerpolicy` - `referrerpolicy`
- `sandbox` - `sandbox`
## Developing Chitchatter ## Available Scripts
> [!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. Note: make sure to run 'npm install' after cloning the repo.
In the project directory, you can run: In the project directory, you can run:
#### `npm dev` ### `npm dev`
Runs the entire stack (client + WebTorrent tracker) locally. 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. 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. 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.
#### `npm run build` ### `npm run build`
Builds the app for production to the `dist` folder. It correctly bundles React in production mode and optimizes the build for the best performance. Builds the app for production to the `build` 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. The build is minified and the filenames include the hashes.
@ -172,36 +160,16 @@ 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. 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 #### Deployment After Forking
> > > > > > > 811c35b (main: prelim update to documention.)
##### On non-GitHub hosts ##### On non-GitHub hosts
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). 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).
#### Runtime configuration #### Runtime configuration
Explore the files in `src/config` to modify pairing and relay server 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 ### 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). 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).
@ -222,11 +190,7 @@ 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**. check your `about:config` settings and ensure that `media.peerconnection.enabled` is **enabled**.
#### Offered files can't be downloaded from peers ##### Security
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 ### Contributors

View File

@ -1,91 +0,0 @@
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',
},
],
},
],
}

42792
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,73 +4,78 @@
"homepage": "https://remnant.chat", "homepage": "https://remnant.chat",
"author": "Shiloh", "author": "Shiloh",
"license": "GPL-2.0-or-later", "license": "GPL-2.0-or-later",
"type": "module",
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.3", "@emotion/react": "^11.10.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.10.0",
"@mui/icons-material": "^5.16.7", "@mui/icons-material": "^5.8.4",
"@mui/material": "^5.16.7", "@mui/material": "^5.14.12",
"@react-hook/debounce": "^4.0.0", "@react-hook/debounce": "^4.0.0",
"@react-hook/window-size": "^3.1.1", "@react-hook/window-size": "^3.1.1",
"@testing-library/jest-dom": "^6.5.0", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^15.0.7", "@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^13.5.0",
"@types/node": "^18.19.55", "@types/jest": "^28.1.6",
"@types/node": "^18.18.4",
"@types/react": "^18.2.25",
"@types/react-dom": "^18.0.6",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"detectincognitojs": "^1.3.5", "classnames": "^2.3.1",
"detectincognitojs": "^1.1.2",
"fast-memoize": "^2.5.2", "fast-memoize": "^2.5.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"fun-animal-names": "^0.1.1", "fun-animal-names": "^0.1.1",
"idb-chunk-store": "^1.0.1", "idb-chunk-store": "^1.0.1",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"modern-normalize": "^2.0.0",
"mui-markdown": "^0.5.5", "mui-markdown": "^0.5.5",
"querystring": "^0.2.1", "querystring": "^0.2.1",
"react": "^18.3.1", "react": "^18.2.0",
"react-dom": "^18.3.1", "react-dom": "^18.2.0",
"react-file-reader-input": "^2.0.0", "react-file-reader-input": "^2.0.0",
"react-git-info": "^2.0.1", "react-git-info": "^2.0.1",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-qrcode-logo": "^2.10.0", "react-qrcode-logo": "^2.8.0",
"react-router-dom": "^6.27.0", "react-router-dom": "^6.16.0",
"react-scripts": "5.0.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-youtube": "^10.1.0", "react-youtube": "^10.1.0",
"readable-web-to-node-stream": "^3.0.2", "readable-web-to-node-stream": "^3.0.2",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"sass": "^1.54.3",
"sdp": "^3.2.0", "sdp": "^3.2.0",
"secure-file-transfer": "^0.0.8", "secure-file-transfer": "^0.0.7",
"streamsaver": "^2.0.6", "streamsaver": "^2.0.6",
"trystero": "^0.20.0", "trystero": "^0.15.0",
"typeface-public-sans": "^1.1.13", "typeface-public-sans": "^1.1.13",
"typeface-roboto": "^1.1.13", "typeface-roboto": "^1.1.13",
"typescript": "^5.6.3", "typescript": "^4.9.5",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"vite-plugin-babel-macros": "^1.0.6",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"webrtc-adapter": "^8.2.2" "webrtc-adapter": "^8.2.2"
}, },
"scripts": { "scripts": {
"start": "cross-env VITE_HOMEPAGE=$(npm pkg get homepage) vite --port 3000", "analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "cross-env REACT_APP_HOMEPAGE=$(npm pkg get homepage) react-scripts start",
"start:tracker": "bittorrent-tracker", "start:tracker": "bittorrent-tracker",
"start:streamsaver": "serve -p 3015 node_modules/streamsaver", "start:streamsaver": "serve -p 3015 node_modules/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\"", "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\"",
"build": "npm run build:app && npm run build:sdk", "build": "npm run build:app && npm run build:sdk",
"build:app": "cross-env VITE_HOMEPAGE=$(npm pkg get homepage) vite build", "build:app": "cross-env REACT_APP_HOMEPAGE=$(npm pkg get homepage) react-scripts build",
"build:sdk": "parcel build sdk/sdk.ts --no-content-hash", "build:sdk": "parcel build sdk/sdk.ts --dist-dir build --no-content-hash",
"build:sdk:watch": "nodemon --exec \"npm run build:sdk\"", "build:sdk:watch": "nodemon --exec \"npm run build:sdk\"",
"test": "vitest", "test": "react-scripts test",
"prepare": "husky install", "prepare": "husky install",
"prettier": "prettier \"**/*.{ts,tsx}\" --write", "prettier": "prettier 'src/**/*.js' --write",
"lint": "eslint src --max-warnings=0" "lint": "eslint src --max-warnings=0"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app" "react-app",
"react-app/jest"
] ]
}, },
"engines": { "engines": {
"node": "20.12.1", "node": "18.12.1",
"npm": "10.5.0" "npm": "8.19.2"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -87,8 +92,6 @@
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@types/file-saver": "^2.0.7", "@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-file-reader-input": "^2.0.4",
"@types/react-syntax-highlighter": "^15.5.5", "@types/react-syntax-highlighter": "^15.5.5",
"@types/streamsaver": "^2.0.1", "@types/streamsaver": "^2.0.1",
@ -96,34 +99,28 @@
"@types/webtorrent": "^0.109.3", "@types/webtorrent": "^0.109.3",
"@typescript-eslint/eslint-plugin": "^5.33.0", "@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0", "@typescript-eslint/parser": "^5.33.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"bittorrent-tracker": "^9.19.0", "bittorrent-tracker": "^9.19.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.21.0", "eslint": "^8.21.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "^7.0.1", "eslint-config-react-app": "^7.0.1",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"husky": "^8.0.1", "husky": "^8.0.1",
"jsdom": "^24.0.0",
"mprocs": "^0.6.4", "mprocs": "^0.6.4",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"parcel": "^2.10.0", "parcel": "^2.10.0",
"prettier": "^3.2.5", "postcss": "^8.4.31",
"pretty-quick": "^4.0.0", "prettier": "^2.7.1",
"pretty-quick": "^3.1.3",
"process": "^0.11.10", "process": "^0.11.10",
"serve": "^14.1.2", "serve": "^14.1.2",
"source-map-explorer": "^2.5.3",
"tailwindcss": "^3.1.8",
"url": "^0.11.0", "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": { "overrides": {
"ipfs-core": "npm:dry-uninstall", "ipfs-core": "npm:dry-uninstall",
@ -137,5 +134,10 @@
"resolve-url-loader": { "resolve-url-loader": {
"postcss": "8.4.31" "postcss": "8.4.31"
} }
},
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!trystero)/"
]
} }
} }

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,21 +1,26 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="A peer-to-peer chat app that is serverless, decentralized, and ephemeral" content="A peer-to-peer chat app that is serverless, decentralized, and ephemeral"
/> />
<link rel="apple-touch-icon" href="/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
Notice the use of in the tags above. manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build. 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. Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "/favicon.ico" will Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL. 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`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
@ -64,6 +69,5 @@
To begin the development, run `npm start` or `yarn start`. To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`. To create a production bundle, use `npm run build` or `yarn build`.
--> -->
<script type="module" src="/src/index.tsx"></script>
</body> </body>
</html> </html>

4
src/Bootstrap.js Normal file
View File

@ -0,0 +1,4 @@
// NOTE: This file is a shim to enable the Bootstrap component to be
// lazy-loaded.
import Bootstrap from './Bootstrap.tsx'
export default Bootstrap

View File

@ -1,10 +1,9 @@
import { vi } from 'vitest'
import { act, render } from '@testing-library/react' import { act, render } from '@testing-library/react'
import persistedStorage from 'localforage' import localforage from 'localforage'
import { PersistedStorageKeys } from 'models/storage' import { PersistedStorageKeys } from 'models/storage'
import { import {
mockSerialization, mockSerializationService,
mockSerializedPrivateKey, mockSerializedPrivateKey,
mockSerializedPublicKey, mockSerializedPublicKey,
} from 'test-utils/mocks/mockSerializationService' } from 'test-utils/mocks/mockSerializationService'
@ -12,16 +11,30 @@ import { userSettingsStubFactory } from 'test-utils/stubs/userSettings'
import Bootstrap, { BootstrapProps } from './Bootstrap' import Bootstrap, { BootstrapProps } from './Bootstrap'
vi.mock('localforage') const mockPersistedStorage =
jest.createMockFromModule<jest.Mock<typeof localforage>>('localforage')
const mockGetItem = jest.fn()
const mockSetItem = jest.fn()
const userSettingsStub = userSettingsStubFactory() const userSettingsStub = userSettingsStubFactory()
beforeEach(() => {
mockGetItem.mockImplementation(() => Promise.resolve(null))
mockSetItem.mockImplementation((data: any) => Promise.resolve(data))
})
const renderBootstrap = async (overrides: Partial<BootstrapProps> = {}) => { const renderBootstrap = async (overrides: Partial<BootstrapProps> = {}) => {
Object.assign(mockPersistedStorage, {
getItem: mockGetItem,
setItem: mockSetItem,
})
render( render(
<Bootstrap <Bootstrap
persistedStorage={persistedStorage} persistedStorage={mockPersistedStorage as any as typeof localforage}
initialUserSettings={userSettingsStub} initialUserSettings={userSettingsStub}
serializationService={mockSerialization} serializationService={mockSerializationService}
{...overrides} {...overrides}
/> />
) )
@ -38,9 +51,7 @@ test('renders', async () => {
test('checks persistedStorage for user settings', async () => { test('checks persistedStorage for user settings', async () => {
await renderBootstrap() await renderBootstrap()
expect(persistedStorage.getItem).toHaveBeenCalledWith( expect(mockGetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS)
PersistedStorageKeys.USER_SETTINGS
)
}) })
test('updates persisted user settings', async () => { test('updates persisted user settings', async () => {
@ -48,9 +59,7 @@ test('updates persisted user settings', async () => {
initialUserSettings: { ...userSettingsStub, userId: 'abc123' }, initialUserSettings: { ...userSettingsStub, userId: 'abc123' },
}) })
expect(persistedStorage.setItem).toHaveBeenCalledWith( expect(mockSetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS, {
PersistedStorageKeys.USER_SETTINGS,
{
colorMode: 'dark', colorMode: 'dark',
userId: 'abc123', userId: 'abc123',
customUsername: '', customUsername: '',
@ -59,6 +68,5 @@ test('updates persisted user settings', async () => {
showActiveTypingStatus: true, showActiveTypingStatus: true,
publicKey: mockSerializedPublicKey, publicKey: mockSerializedPublicKey,
privateKey: mockSerializedPrivateKey, privateKey: mockSerializedPrivateKey,
} })
)
}) })

View File

@ -6,8 +6,8 @@ import {
Navigate, Navigate,
} from 'react-router-dom' } from 'react-router-dom'
import localforage from 'localforage' import localforage from 'localforage'
import { useRegisterSW } from 'virtual:pwa-register/react'
import * as serviceWorkerRegistration from 'serviceWorkerRegistration'
import { StorageContext } from 'contexts/StorageContext' import { StorageContext } from 'contexts/StorageContext'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { homepageUrl, routes } from 'config/routes' import { homepageUrl, routes } from 'config/routes'
@ -27,12 +27,15 @@ import {
PostMessageEvent, PostMessageEvent,
PostMessageEventName, PostMessageEventName,
} from 'models/sdk' } from 'models/sdk'
import { serialization, SerializedUserSettings } from 'services/Serialization' import {
serializationService as serializationServiceInstance,
SerializedUserSettings,
} from 'services/Serialization'
export interface BootstrapProps { export interface BootstrapProps {
persistedStorage?: typeof localforage persistedStorage?: typeof localforage
initialUserSettings: UserSettings initialUserSettings: UserSettings
serializationService?: typeof serialization serializationService?: typeof serializationServiceInstance
} }
const configListenerTimeout = 3000 const configListenerTimeout = 3000
@ -79,7 +82,7 @@ const Bootstrap = ({
description: 'Persisted settings data for remnantchat', description: 'Persisted settings data for remnantchat',
}), }),
initialUserSettings, initialUserSettings,
serializationService = serialization, serializationService = serializationServiceInstance,
}: BootstrapProps) => { }: BootstrapProps) => {
const queryParams = useMemo( const queryParams = useMemo(
() => new URLSearchParams(window.location.search), () => new URLSearchParams(window.location.search),
@ -87,11 +90,16 @@ const Bootstrap = ({
) )
const [persistedStorage] = useState(persistedStorageProp) const [persistedStorage] = useState(persistedStorageProp)
const [appNeedsUpdate, setAppNeedsUpdate] = useState(false)
const [hasLoadedSettings, setHasLoadedSettings] = useState(false) const [hasLoadedSettings, setHasLoadedSettings] = useState(false)
const [userSettings, setUserSettings] = const [userSettings, setUserSettings] =
useState<UserSettings>(initialUserSettings) useState<UserSettings>(initialUserSettings)
const { userId } = userSettings const { userId } = userSettings
const handleServiceWorkerUpdate = () => {
setAppNeedsUpdate(true)
}
const persistUserSettings = useCallback( const persistUserSettings = useCallback(
async (newUserSettings: UserSettings) => { async (newUserSettings: UserSettings) => {
if (queryParams.has(QueryParamKeys.IS_EMBEDDED)) { if (queryParams.has(QueryParamKeys.IS_EMBEDDED)) {
@ -109,9 +117,9 @@ const Bootstrap = ({
[persistedStorageProp, queryParams, serializationService, userSettings] [persistedStorageProp, queryParams, serializationService, userSettings]
) )
const { useEffect(() => {
needRefresh: [appNeedsUpdate], serviceWorkerRegistration.register({ onUpdate: handleServiceWorkerUpdate })
} = useRegisterSW() }, [])
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {

View File

@ -3,7 +3,7 @@ import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { encryption } from 'services/Encryption' import { encryptionService } from 'services/Encryption'
import { import {
EnvironmentUnsupportedDialog, EnvironmentUnsupportedDialog,
isEnvironmentSupported, isEnvironmentSupported,
@ -13,7 +13,8 @@ import { ColorMode, UserSettings } from 'models/settings'
import type { BootstrapProps } from './Bootstrap' import type { BootstrapProps } from './Bootstrap'
const Bootstrap = lazy(() => import('./Bootstrap')) // @ts-expect-error
const Bootstrap = lazy(() => import('./Bootstrap.js'))
export interface InitProps extends Omit<BootstrapProps, 'initialUserSettings'> { export interface InitProps extends Omit<BootstrapProps, 'initialUserSettings'> {
getUuid?: typeof uuid getUuid?: typeof uuid
@ -31,7 +32,8 @@ const Init = ({ getUuid = uuid, ...props }: InitProps) => {
if (userSettings !== null) return if (userSettings !== null) return
try { try {
const { publicKey, privateKey } = await encryption.generateKeyPair() const { publicKey, privateKey } =
await encryptionService.generateKeyPair()
setUserSettings({ setUserSettings({
userId: getUuid(), userId: getUuid(),

View File

@ -1,25 +1,16 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import Slider from '@mui/material/Slider' import Slider from '@mui/material/Slider'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Paper from '@mui/material/Paper'
import ListItemIcon from '@mui/material/ListItemIcon' import ListItemIcon from '@mui/material/ListItemIcon'
import VolumeUpIcon from '@mui/icons-material/VolumeUp' import VolumeUp from '@mui/icons-material/VolumeUp'
import VolumeDownIcon from '@mui/icons-material/VolumeDown' import VolumeDown from '@mui/icons-material/VolumeDown'
import VolumeMuteIcon from '@mui/icons-material/VolumeMute' import VolumeMute 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 { interface AudioVolumeProps {
audioEl: HTMLAudioElement audioEl: HTMLAudioElement
audioChannelName: AudioChannelName
} }
export const AudioVolume = ({ export const AudioVolume = ({ audioEl }: AudioVolumeProps) => {
audioEl,
audioChannelName,
}: AudioVolumeProps) => {
const [audioVolume, setAudioVolume] = useState(audioEl.volume) const [audioVolume, setAudioVolume] = useState(audioEl.volume)
useEffect(() => { useEffect(() => {
@ -41,39 +32,19 @@ export const AudioVolume = ({
const formatLabelValue = () => `${Math.round(audioVolume * 100)}%` const formatLabelValue = () => `${Math.round(audioVolume * 100)}%`
let VolumeIcon = VolumeUpIcon let VolumeIcon = VolumeUp
if (audioVolume === 0) { if (audioVolume === 0) {
VolumeIcon = VolumeMuteIcon VolumeIcon = VolumeMute
} else if (audioVolume < 0.5) { } else if (audioVolume < 0.5) {
VolumeIcon = VolumeDownIcon VolumeIcon = VolumeDown
} }
return ( return (
<Paper <Box sx={{ display: 'flex', pt: 1, pr: 3, alignItems: 'center' }}>
sx={{ <ListItemIcon>
alignItems: 'center', <VolumeIcon sx={{ cursor: 'pointer' }} onClick={handleIconClick} />
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> </ListItemIcon>
<Box display="flex" width={1}>
<Slider <Slider
aria-label="Volume" aria-label="Volume"
getAriaValueText={formatLabelValue} getAriaValueText={formatLabelValue}
@ -83,6 +54,5 @@ export const AudioVolume = ({
value={audioVolume * 100} value={audioVolume * 100}
></Slider> ></Slider>
</Box> </Box>
</Paper>
) )
} }

View File

@ -1,4 +1,5 @@
import { HTMLAttributes, useRef, useEffect, useState, useContext } from 'react' import { HTMLAttributes, useRef, useEffect, useState, useContext } from 'react'
import cx from 'classnames'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import useTheme from '@mui/material/styles/useTheme' import useTheme from '@mui/material/styles/useTheme'
@ -11,7 +12,11 @@ export interface ChatTranscriptProps extends HTMLAttributes<HTMLDivElement> {
userId: string userId: string
} }
export const ChatTranscript = ({ messageLog, userId }: ChatTranscriptProps) => { export const ChatTranscript = ({
className,
messageLog,
userId,
}: ChatTranscriptProps) => {
const { showRoomControls } = useContext(ShellContext) const { showRoomControls } = useContext(ShellContext)
const theme = useTheme() const theme = useTheme()
const boxRef = useRef<HTMLDivElement>(null) const boxRef = useRef<HTMLDivElement>(null)
@ -61,13 +66,11 @@ export const ChatTranscript = ({ messageLog, userId }: ChatTranscriptProps) => {
return ( return (
<Box <Box
ref={boxRef} ref={boxRef}
className="ChatTranscript" className={cx('ChatTranscript', className)}
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1, py: transcriptMinPadding,
overflow: 'auto',
pb: transcriptMinPadding,
pt: showRoomControls ? theme.spacing(10) : theme.spacing(2), pt: showRoomControls ? theme.spacing(10) : theme.spacing(2),
px: `max(${transcriptPaddingX}, ${transcriptMinPadding})`, px: `max(${transcriptPaddingX}, ${transcriptMinPadding})`,
transition: `padding-top ${theme.transitions.duration.short}ms ${theme.transitions.easing.easeInOut}`, transition: `padding-top ${theme.transitions.duration.short}ms ${theme.transitions.easing.easeInOut}`,

View File

@ -1,9 +0,0 @@
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')({})

View File

@ -1,10 +1,9 @@
import { useContext, useEffect, useRef, useState } from 'react' import { useContext, useEffect, useRef, useState } from 'react'
import { TorrentFile } from 'webtorrent' import { TorrentFile } from 'webtorrent'
import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress' import CircularProgress from '@mui/material/CircularProgress'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { fileTransfer } from 'lib/FileTransfer' import { fileTransfer } from 'services/FileTransfer'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
type TorrentFiles = Awaited<ReturnType<typeof fileTransfer.download>> type TorrentFiles = Awaited<ReturnType<typeof fileTransfer.download>>
@ -17,65 +16,34 @@ interface InlineFileProps {
file: TorrentFile 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) => { export const InlineFile = ({ file }: InlineFileProps) => {
const containerRef = useRef(null) const containerRef = useRef(null)
const [didRenderingMediaFail, setDidRenderingMediaFail] = useState(false) const [didRenderingMediaFail, setDidRenderingMediaFail] = useState(false)
const [isMediaSupported, setIsMediaSupported] = useState(true)
const shellContext = useContext(ShellContext) const shellContext = useContext(ShellContext)
useEffect(() => { useEffect(() => {
;(async () => {
const { current: container } = containerRef const { current: container } = containerRef
if (!container) return if (!container) return
const { name } = file
const fileNameExtension = name.split('.').pop() ?? ''
if (!supportedMediaExtensions.includes(`.${fileNameExtension}`)) {
setIsMediaSupported(false)
return
}
try { try {
file.appendTo(container) file.appendTo(container)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
setDidRenderingMediaFail(true) setDidRenderingMediaFail(true)
} }
})()
}, [file, containerRef, shellContext.roomId]) }, [file, containerRef, shellContext.roomId])
return ( return (
<Box ref={containerRef} sx={{ '& img': { maxWidth: '100%' } }}> <div ref={containerRef}>
{!isMediaSupported && (
<Typography sx={{ fontStyle: 'italic' }}>
Media preview not supported
</Typography>
)}
{didRenderingMediaFail && ( {didRenderingMediaFail && (
<Typography sx={{ fontStyle: 'italic' }}> <Typography sx={{ fontStyle: 'italic' }}>
Media failed to render Media failed to render
</Typography> </Typography>
)} )}
</Box> </div>
) )
} }

View File

@ -0,0 +1,3 @@
.Message
pre
overflow: auto

View File

@ -5,10 +5,20 @@ import Box from '@mui/material/Box'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
import Typography, { TypographyProps } from '@mui/material/Typography' import Typography, { TypographyProps } from '@mui/material/Typography'
import Link, { LinkProps } from '@mui/material/Link' import Link, { LinkProps } from '@mui/material/Link'
import styled from '@mui/material/styles/styled'
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism' 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' import Markdown from 'react-markdown'
// @ts-ignore
import { CodeProps } from 'react-markdown/lib/ast-to-react' import { CodeProps } from 'react-markdown/lib/ast-to-react'
// @ts-ignore
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import { import {
@ -22,7 +32,7 @@ import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock'
import { InlineMedia } from './InlineMedia' import { InlineMedia } from './InlineMedia'
const StyledMarkdown = styled(Markdown)({}) import './Message.sass'
export interface MessageProps { export interface MessageProps {
message: IMessage | I_InlineMedia message: IMessage | I_InlineMedia
@ -144,26 +154,13 @@ export const Message = ({ message, showAuthor, userId }: MessageProps) => {
) : isYouTubeLink(message) ? ( ) : isYouTubeLink(message) ? (
<YouTube videoId={getYouTubeVideoId(message.text)} /> <YouTube videoId={getYouTubeVideoId(message.text)} />
) : ( ) : (
<StyledMarkdown <Markdown
components={componentMap} components={componentMap}
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
linkTarget="_blank" linkTarget="_blank"
sx={{
'& pre': {
overflow: 'auto',
},
'& ol': {
pl: 2,
listStyleType: 'decimal',
},
'& ul': {
pl: 2,
listStyleType: 'disc',
},
}}
> >
{message.text} {message.text}
</StyledMarkdown> </Markdown>
)} )}
</Box> </Box>
</Tooltip> </Tooltip>

View File

@ -14,7 +14,7 @@ import ArrowUpward from '@mui/icons-material/ArrowUpward'
import { messageCharacterSizeLimit } from 'config/messaging' import { messageCharacterSizeLimit } from 'config/messaging'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { Form } from 'components/Elements' import classNames from 'classnames'
interface MessageFormProps { interface MessageFormProps {
onMessageSubmit: (message: string) => void onMessageSubmit: (message: string) => void
@ -76,17 +76,12 @@ export const MessageForm = ({
} }
return ( return (
<Form <form
onSubmit={handleMessageSubmit} onSubmit={handleMessageSubmit}
sx={{ className={classNames({
...(showActiveTypingStatus && { 'pt-4 px-4': showActiveTypingStatus,
pt: 2, 'p-4': !showActiveTypingStatus,
px: 2, })}
}),
...(!showActiveTypingStatus && {
p: 2,
}),
}}
> >
<Stack direction="row" spacing={2}> <Stack direction="row" spacing={2}>
<FormControl fullWidth> <FormControl fullWidth>
@ -115,6 +110,6 @@ export const MessageForm = ({
<ArrowUpward /> <ArrowUpward />
</Fab> </Fab>
</Stack> </Stack>
</Form> </form>
) )
} }

View File

@ -1,19 +1,11 @@
import { ChangeEvent, useState, SyntheticEvent, useMemo } from 'react' import { ChangeEvent, useState, SyntheticEvent } from 'react'
import { useNavigate } from 'react-router-dom'
import Button from '@mui/material/Button' 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 TextField from '@mui/material/TextField'
import Dialog from '@mui/material/Dialog' import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions' import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent' import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText' import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle' 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 { interface PasswordPromptProps {
isOpen: boolean isOpen: boolean
@ -25,36 +17,16 @@ export const PasswordPrompt = ({
onPasswordEntered, onPasswordEntered,
}: PasswordPromptProps) => { }: PasswordPromptProps) => {
const [password, setPassword] = useState('') 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>) => { const handleFormSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
onPasswordEntered(password) onPasswordEntered(password)
} }
const handleClickShowPassword = () => {
setShowPassword(!showPassword)
}
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => { const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value) setPassword(e.target.value)
} }
const handleGoBackClick = () => {
navigate(-1)
}
const passwordToggleLabel = showPassword ? 'Hide password' : 'Show password'
return ( return (
<Dialog open={isOpen}> <Dialog open={isOpen}>
<form onSubmit={handleFormSubmit}> <form onSubmit={handleFormSubmit}>
@ -66,7 +38,7 @@ export const PasswordPrompt = ({
impossible to know if the password you enter will match the password impossible to know if the password you enter will match the password
entered by other peers. entered by other peers.
</DialogContentText> </DialogContentText>
<DialogContentText sx={{ mb: 2 }}> <DialogContentText>
If there is a mismatch, you will be in the room but be unable to If there is a mismatch, you will be in the room but be unable to
connect to others. An error will not be shown. connect to others. An error will not be shown.
</DialogContentText> </DialogContentText>
@ -75,33 +47,14 @@ export const PasswordPrompt = ({
margin="dense" margin="dense"
id="password" id="password"
label="Password" label="Password"
type={showPassword ? 'text' : 'password'} type="password"
fullWidth fullWidth
variant="outlined" variant="standard"
value={password} value={password}
onChange={handlePasswordChange} onChange={handlePasswordChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Tooltip title={passwordToggleLabel}>
<IconButton
aria-label={passwordToggleLabel}
onClick={handleClickShowPassword}
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/> />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
{!isEmbedded && (
<Button color="secondary" onClick={handleGoBackClick}>
Go back
</Button>
)}
<Button type="submit" disabled={password.length === 0}> <Button type="submit" disabled={password.length === 0}>
Submit Submit
</Button> </Button>

View File

@ -19,7 +19,7 @@ export const usePeerNameDisplay = () => {
const getCustomUsername = (userId: string) => const getCustomUsername = (userId: string) =>
isPeerSelf(userId) isPeerSelf(userId)
? selfCustomUsername ? selfCustomUsername
: (getPeer(userId)?.customUsername ?? '') : getPeer(userId)?.customUsername ?? ''
const getFriendlyName = (userId: string) => { const getFriendlyName = (userId: string) => {
const customUsername = getCustomUsername(userId) const customUsername = getCustomUsername(userId)

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism' import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter' import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock' import { CopyableBlock } from 'components/CopyableBlock/CopyableBlock'
import { encryption } from 'services/Encryption/Encryption' import { encryptionService } from 'services/Encryption/Encryption'
interface PeerPublicKeyProps { interface PeerPublicKeyProps {
publicKey: CryptoKey publicKey: CryptoKey
@ -13,7 +13,7 @@ export const PublicKey = ({ publicKey }: PeerPublicKeyProps) => {
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
setPublicKeyString(await encryption.stringifyCryptoKey(publicKey)) setPublicKeyString(await encryptionService.stringifyCryptoKey(publicKey))
})() })()
}, [publicKey]) }, [publicKey])

View File

@ -3,7 +3,7 @@ import Paper from '@mui/material/Paper'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
import { PeerNameDisplay } from 'components/PeerNameDisplay' import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { StreamType } from 'models/chat' import { VideoStreamType } from 'models/chat'
import { SelectedPeerStream } from './RoomVideoDisplay' import { SelectedPeerStream } from './RoomVideoDisplay'
@ -13,13 +13,13 @@ interface PeerVideoProps {
numberOfVideos: number numberOfVideos: number
onVideoClick?: ( onVideoClick?: (
userId: string, userId: string,
streamType: StreamType, videoStreamType: VideoStreamType,
videoStream: MediaStream videoStream: MediaStream
) => void ) => void
selectedPeerStream: SelectedPeerStream | null selectedPeerStream: SelectedPeerStream | null
userId: string userId: string
videoStream: MediaStream videoStream: MediaStream
streamType: StreamType videoStreamType: VideoStreamType
} }
// Adapted from https://www.geeksforgeeks.org/find-the-next-perfect-square-greater-than-a-given-number/ // Adapted from https://www.geeksforgeeks.org/find-the-next-perfect-square-greater-than-a-given-number/
@ -37,7 +37,7 @@ export const PeerVideo = ({
userId, userId,
selectedPeerStream, selectedPeerStream,
videoStream, videoStream,
streamType, videoStreamType,
}: PeerVideoProps) => { }: PeerVideoProps) => {
const videoRef = useRef<HTMLVideoElement>(null) const videoRef = useRef<HTMLVideoElement>(null)
@ -47,14 +47,13 @@ export const PeerVideo = ({
video.autoplay = true video.autoplay = true
video.srcObject = videoStream video.srcObject = videoStream
video.muted = true
}, [videoRef, videoStream]) }, [videoRef, videoStream])
const cols = Math.sqrt(nextPerfectSquare(numberOfVideos - 1)) const cols = Math.sqrt(nextPerfectSquare(numberOfVideos - 1))
const rows = Math.ceil(numberOfVideos / cols) const rows = Math.ceil(numberOfVideos / cols)
const handleVideoClick = () => { const handleVideoClick = () => {
onVideoClick?.(userId, streamType, videoStream) onVideoClick?.(userId, videoStreamType, videoStream)
} }
return ( return (

View File

@ -1,13 +1,12 @@
import { vi } from 'vitest'
import { PropsWithChildren } from 'react' import { PropsWithChildren } from 'react'
import { render, screen } from '@testing-library/react' import { waitFor, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import { MemoryRouter as Router, Route, Routes } from 'react-router-dom' import { MemoryRouter as Router, Route, Routes } from 'react-router-dom'
import { userSettingsContextStubFactory } from 'test-utils/stubs/settingsContext' import { userSettingsContextStubFactory } from 'test-utils/stubs/settingsContext'
import { mockEncryptionService } from 'test-utils/mocks/mockEncryptionService' import { mockEncryptionService } from 'test-utils/mocks/mockEncryptionService'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { Time } from 'lib/Time'
import { Room, RoomProps } from './' import { Room, RoomProps } from './'
@ -18,17 +17,13 @@ const userSettingsStub = userSettingsContextStubFactory({
userId: mockUserId, userId: mockUserId,
}) })
window.AudioContext = vi.fn().mockImplementation(() => {}) window.AudioContext = jest.fn().mockImplementation()
const mockGetUuid = vi.fn() const mockGetUuid = jest.fn()
const mockMessagedSender = vi.fn().mockImplementation(() => Promise.resolve([])) const mockMessagedSender = jest
.fn()
.mockImplementation(() => Promise.resolve([]))
const mockTimeService = new Time() jest.mock('trystero', () => ({
const mockNowTime = 1234
mockTimeService.now = () => mockNowTime
vi.mock('../../lib/Audio')
vi.mock('trystero/torrent', () => ({
joinRoom: () => ({ joinRoom: () => ({
makeAction: () => [mockMessagedSender, () => {}, () => {}], makeAction: () => [mockMessagedSender, () => {}, () => {}],
ping: () => Promise.resolve(0), ping: () => Promise.resolve(0),
@ -58,14 +53,10 @@ const RouteStub = ({ children }: PropsWithChildren) => {
) )
} }
jest.useFakeTimers().setSystemTime(100)
const RoomStub = (props: RoomProps) => { const RoomStub = (props: RoomProps) => {
return ( return <Room encryptionService={mockEncryptionService} {...props} />
<Room
encryptionService={mockEncryptionService}
timeService={mockTimeService}
{...props}
/>
)
} }
describe('Room', () => { describe('Room', () => {
@ -98,7 +89,9 @@ describe('Room', () => {
const sendButton = screen.getByLabelText('Send') const sendButton = screen.getByLabelText('Send')
const textInput = screen.getByPlaceholderText('Your message') const textInput = screen.getByPlaceholderText('Your message')
await userEvent.type(textInput, 'hello') await waitFor(() => {
userEvent.type(textInput, 'hello')
})
expect(sendButton).not.toBeDisabled() expect(sendButton).not.toBeDisabled()
}) })
@ -113,8 +106,13 @@ describe('Room', () => {
const sendButton = screen.getByLabelText('Send') const sendButton = screen.getByLabelText('Send')
const textInput = screen.getByPlaceholderText('Your message') const textInput = screen.getByPlaceholderText('Your message')
await userEvent.type(textInput, 'hello') await waitFor(() => {
await userEvent.click(sendButton) userEvent.type(textInput, 'hello')
})
await waitFor(() => {
userEvent.click(sendButton)
})
expect(textInput).toHaveValue('') expect(textInput).toHaveValue('')
}) })
@ -133,13 +131,18 @@ describe('Room', () => {
const sendButton = screen.getByLabelText('Send') const sendButton = screen.getByLabelText('Send')
const textInput = screen.getByPlaceholderText('Your message') const textInput = screen.getByPlaceholderText('Your message')
await userEvent.type(textInput, 'hello') await waitFor(() => {
await userEvent.click(sendButton) userEvent.type(textInput, 'hello')
})
await waitFor(() => {
userEvent.click(sendButton)
})
expect(mockMessagedSender).toHaveBeenCalledWith({ expect(mockMessagedSender).toHaveBeenCalledWith({
authorId: mockUserId, authorId: mockUserId,
text: 'hello', text: 'hello',
timeSent: mockNowTime, timeSent: 100,
id: 'abc123', id: 'abc123',
}) })
}) })

View File

@ -8,12 +8,11 @@ import { v4 as uuid } from 'uuid'
import { rtcConfig } from 'config/rtcConfig' import { rtcConfig } from 'config/rtcConfig'
import { trackerUrls } from 'config/trackerUrls' import { trackerUrls } from 'config/trackerUrls'
import { time } from 'lib/Time'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { MessageForm } from 'components/MessageForm' import { MessageForm } from 'components/MessageForm'
import { ChatTranscript } from 'components/ChatTranscript' import { ChatTranscript } from 'components/ChatTranscript'
import { encryption } from 'services/Encryption' import { encryptionService as encryptionServiceInstance } from 'services/Encryption'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { useRoom } from './useRoom' import { useRoom } from './useRoom'
@ -31,15 +30,13 @@ export interface RoomProps {
password?: string password?: string
roomId: string roomId: string
userId: string userId: string
encryptionService?: typeof encryption encryptionService?: typeof encryptionServiceInstance
timeService?: typeof time
} }
export function Room({ export function Room({
appId = `${encodeURI(window.location.origin)}_${process.env.VITE_NAME}`, appId = `${encodeURI(window.location.origin)}_${process.env.REACT_APP_NAME}`,
getUuid = uuid, getUuid = uuid,
encryptionService = encryption, encryptionService = encryptionServiceInstance,
timeService = time,
roomId, roomId,
password, password,
userId, userId,
@ -60,10 +57,10 @@ export function Room({
} = useRoom( } = useRoom(
{ {
appId, appId,
relayUrls: trackerUrls, trackerUrls,
rtcConfig, rtcConfig,
password, password,
relayRedundancy: 4, trackerRedundancy: 4,
}, },
{ {
roomId, roomId,
@ -71,7 +68,6 @@ export function Room({
getUuid, getUuid,
publicKey, publicKey,
encryptionService, encryptionService,
timeService,
} }
) )
@ -157,7 +153,11 @@ export function Room({
height: landscape ? '100%' : '40%', height: landscape ? '100%' : '40%',
}} }}
> >
<ChatTranscript messageLog={messageLog} userId={userId} /> <ChatTranscript
messageLog={messageLog}
userId={userId}
className="grow overflow-auto"
/>
<Divider /> <Divider />
<Box> <Box>
<MessageForm <MessageForm

View File

@ -9,7 +9,7 @@ import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem' import MenuItem from '@mui/material/MenuItem'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
import { PeerRoom } from 'lib/PeerRoom' import { PeerRoom } from 'services/PeerRoom/PeerRoom'
import { useRoomAudio } from './useRoomAudio' import { useRoomAudio } from './useRoomAudio'
import { MediaButton } from './MediaButton' import { MediaButton } from './MediaButton'

View File

@ -6,9 +6,7 @@ import Tooltip from '@mui/material/Tooltip'
import CircularProgress from '@mui/material/CircularProgress' import CircularProgress from '@mui/material/CircularProgress'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
import { PeerRoom } from 'lib/PeerRoom' import { PeerRoom } from 'services/PeerRoom/PeerRoom'
import { Input } from 'components/Elements'
import { useRoomFileShare } from './useRoomFileShare' import { useRoomFileShare } from './useRoomFileShare'
import { MediaButton } from './MediaButton' import { MediaButton } from './MediaButton'
@ -75,12 +73,12 @@ export function RoomFileUploadControls({
px: 1, px: 1,
}} }}
> >
<Input <input
multiple multiple
ref={fileInputRef} ref={fileInputRef}
type="file" type="file"
id="file-upload" id="file-upload"
sx={{ display: 'none' }} className="hidden"
onChange={handleFileSelect} onChange={handleFileSelect}
/> />
<Tooltip <Tooltip

View File

@ -3,7 +3,7 @@ import ScreenShare from '@mui/icons-material/ScreenShare'
import StopScreenShare from '@mui/icons-material/StopScreenShare' import StopScreenShare from '@mui/icons-material/StopScreenShare'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
import { PeerRoom } from 'lib/PeerRoom' import { PeerRoom } from 'services/PeerRoom/PeerRoom'
import { useRoomScreenShare } from './useRoomScreenShare' import { useRoomScreenShare } from './useRoomScreenShare'
import { MediaButton } from './MediaButton' import { MediaButton } from './MediaButton'

View File

@ -9,7 +9,7 @@ import Menu from '@mui/material/Menu'
import MenuItem from '@mui/material/MenuItem' import MenuItem from '@mui/material/MenuItem'
import Tooltip from '@mui/material/Tooltip' import Tooltip from '@mui/material/Tooltip'
import { PeerRoom } from 'lib/PeerRoom' import { PeerRoom } from 'services/PeerRoom/PeerRoom'
import { useRoomVideo } from './useRoomVideo' import { useRoomVideo } from './useRoomVideo'
import { MediaButton } from './MediaButton' import { MediaButton } from './MediaButton'

View File

@ -4,7 +4,7 @@ import Paper from '@mui/material/Paper'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { Peer, StreamType } from 'models/chat' import { Peer, VideoStreamType } from 'models/chat'
import { PeerVideo } from './PeerVideo' import { PeerVideo } from './PeerVideo'
@ -16,7 +16,7 @@ interface PeerWithVideo {
export interface SelectedPeerStream { export interface SelectedPeerStream {
peerId: string peerId: string
streamType: StreamType videoStreamType: VideoStreamType
videoStream: MediaStream videoStream: MediaStream
} }
@ -105,13 +105,13 @@ export const RoomVideoDisplay = ({
const handleVideoClick = ( const handleVideoClick = (
peerId: string, peerId: string,
streamType: StreamType, videoStreamType: VideoStreamType,
videoStream: MediaStream videoStream: MediaStream
) => { ) => {
if (selectedPeerStream?.videoStream === videoStream) { if (selectedPeerStream?.videoStream === videoStream) {
setSelectedPeerStream(null) setSelectedPeerStream(null)
} else if (numberOfVideos > 1) { } else if (numberOfVideos > 1) {
setSelectedPeerStream({ peerId, streamType, videoStream }) setSelectedPeerStream({ peerId, videoStreamType, videoStream })
} }
} }
@ -139,7 +139,7 @@ export const RoomVideoDisplay = ({
userId={selectedPeerStream.peerId} userId={selectedPeerStream.peerId}
selectedPeerStream={selectedPeerStream} selectedPeerStream={selectedPeerStream}
videoStream={selectedPeerStream.videoStream} videoStream={selectedPeerStream.videoStream}
streamType={selectedPeerStream.streamType} videoStreamType={selectedPeerStream.videoStreamType}
/> />
</Box> </Box>
)} )}
@ -168,7 +168,7 @@ export const RoomVideoDisplay = ({
userId={userId} userId={userId}
selectedPeerStream={selectedPeerStream} selectedPeerStream={selectedPeerStream}
videoStream={selfVideoStream} videoStream={selfVideoStream}
streamType={StreamType.WEBCAM} videoStreamType={VideoStreamType.WEBCAM}
/> />
)} )}
{selfScreenStream && ( {selfScreenStream && (
@ -179,7 +179,7 @@ export const RoomVideoDisplay = ({
userId={userId} userId={userId}
selectedPeerStream={selectedPeerStream} selectedPeerStream={selectedPeerStream}
videoStream={selfScreenStream} videoStream={selfScreenStream}
streamType={StreamType.SCREEN_SHARE} videoStreamType={VideoStreamType.SCREEN_SHARE}
/> />
)} )}
{peersWithVideo.map(peerWithVideo => ( {peersWithVideo.map(peerWithVideo => (
@ -191,7 +191,7 @@ export const RoomVideoDisplay = ({
userId={peerWithVideo.peer.userId} userId={peerWithVideo.peer.userId}
selectedPeerStream={selectedPeerStream} selectedPeerStream={selectedPeerStream}
videoStream={peerWithVideo.videoStream} videoStream={peerWithVideo.videoStream}
streamType={StreamType.WEBCAM} videoStreamType={VideoStreamType.WEBCAM}
/> />
)} )}
{peerWithVideo.screenStream && ( {peerWithVideo.screenStream && (
@ -201,7 +201,7 @@ export const RoomVideoDisplay = ({
userId={peerWithVideo.peer.userId} userId={peerWithVideo.peer.userId}
selectedPeerStream={selectedPeerStream} selectedPeerStream={selectedPeerStream}
videoStream={peerWithVideo.screenStream} videoStream={peerWithVideo.screenStream}
streamType={StreamType.SCREEN_SHARE} videoStreamType={VideoStreamType.SCREEN_SHARE}
/> />
)} )}
</Fragment> </Fragment>

View File

@ -1,8 +1,8 @@
import { useCallback, useContext, useEffect, useState } from 'react' import { useCallback, useContext, useEffect, useState } from 'react'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { Peer, PeerVerificationState } from 'models/chat' import { Peer, PeerVerificationState } from 'models/chat'
import { encryption } from 'services/Encryption' import { encryptionService as encryptionServiceInstance } from 'services/Encryption'
import { PeerRoom } from 'lib/PeerRoom' import { PeerRoom } from 'services/PeerRoom'
import { PeerActions } from 'models/network' import { PeerActions } from 'models/network'
import { verificationTimeout } from 'config/messaging' import { verificationTimeout } from 'config/messaging'
import { usePeerNameDisplay } from 'components/PeerNameDisplay' import { usePeerNameDisplay } from 'components/PeerNameDisplay'
@ -10,13 +10,13 @@ import { usePeerNameDisplay } from 'components/PeerNameDisplay'
interface UserPeerVerificationProps { interface UserPeerVerificationProps {
peerRoom: PeerRoom peerRoom: PeerRoom
privateKey: CryptoKey privateKey: CryptoKey
encryptionService?: typeof encryption encryptionService?: typeof encryptionServiceInstance
} }
export const usePeerVerification = ({ export const usePeerVerification = ({
peerRoom, peerRoom,
privateKey, privateKey,
encryptionService = encryption, encryptionService = encryptionServiceInstance,
}: UserPeerVerificationProps) => { }: UserPeerVerificationProps) => {
const { updatePeer, peerList, showAlert } = useContext(ShellContext) const { updatePeer, peerList, showAlert } = useContext(ShellContext)

View File

@ -1,6 +1,6 @@
import { useContext, useEffect, useMemo, useState } from 'react' import { useContext, useEffect, useMemo, useState } from 'react'
import { BaseRoomConfig } from 'trystero' import { BaseRoomConfig } from 'trystero'
import { RelayConfig } from 'trystero/torrent' import { TorrentRoomConfig } from 'trystero/torrent'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { useDebounce } from '@react-hook/debounce' import { useDebounce } from '@react-hook/debounce'
@ -23,15 +23,16 @@ import {
TypingStatus, TypingStatus,
Peer, Peer,
PeerVerificationState, PeerVerificationState,
AudioChannelName,
} from 'models/chat' } from 'models/chat'
import { getPeerName, usePeerNameDisplay } from 'components/PeerNameDisplay' import { getPeerName, usePeerNameDisplay } from 'components/PeerNameDisplay'
import { Audio } from 'lib/Audio' import { NotificationService } from 'services/Notification'
import { time } from 'lib/Time' import { Audio as AudioService } from 'services/Audio'
import { PeerRoom, PeerHookType } from 'lib/PeerRoom' import { PeerRoom, PeerHookType } from 'services/PeerRoom'
import { notification } from 'services/Notification' import { fileTransfer } from 'services/FileTransfer'
import { fileTransfer } from 'lib/FileTransfer' import {
import { AllowedKeyType, encryption } from 'services/Encryption' AllowedKeyType,
encryptionService as encryptionServiceInstance,
} from 'services/Encryption'
import { messageTranscriptSizeLimit } from 'config/messaging' import { messageTranscriptSizeLimit } from 'config/messaging'
@ -42,25 +43,23 @@ interface UseRoomConfig {
userId: string userId: string
publicKey: CryptoKey publicKey: CryptoKey
getUuid?: typeof uuid getUuid?: typeof uuid
encryptionService?: typeof encryption encryptionService?: typeof encryptionServiceInstance
timeService?: typeof time
} }
interface UserMetadata extends Record<string, any> { interface UserMetadata {
userId: string userId: string
customUsername: string customUsername: string
publicKeyString: string publicKeyString: string
} }
export function useRoom( export function useRoom(
{ password, ...roomConfig }: BaseRoomConfig & RelayConfig, { password, ...roomConfig }: BaseRoomConfig & TorrentRoomConfig,
{ {
roomId, roomId,
userId, userId,
publicKey, publicKey,
getUuid = uuid, getUuid = uuid,
encryptionService = encryption, encryptionService = encryptionServiceInstance,
timeService = time,
}: UseRoomConfig }: UseRoomConfig
) { ) {
const isPrivate = password !== undefined const isPrivate = password !== undefined
@ -87,7 +86,9 @@ export function useRoom(
const [messageLog, _setMessageLog] = useState<Array<Message | InlineMedia>>( const [messageLog, _setMessageLog] = useState<Array<Message | InlineMedia>>(
[] []
) )
const [newMessageAudio] = useState(() => new Audio('/sounds/new-message.aac')) const [newMessageAudio] = useState(
() => new AudioService(process.env.PUBLIC_URL + '/sounds/new-message.aac')
)
const { getDisplayUsername } = usePeerNameDisplay() const { getDisplayUsername } = usePeerNameDisplay()
@ -239,7 +240,7 @@ export function useRoom(
const unsentMessage: UnsentMessage = { const unsentMessage: UnsentMessage = {
authorId: userId, authorId: userId,
text: message, text: message,
timeSent: timeService.now(), timeSent: Date.now(),
id: getUuid(), id: getUuid(),
} }
@ -250,7 +251,7 @@ export function useRoom(
setMessageLog([ setMessageLog([
...messageLog, ...messageLog,
{ ...unsentMessage, timeReceived: timeService.now() }, { ...unsentMessage, timeReceived: Date.now() },
]) ])
setIsMessageSending(false) setIsMessageSending(false)
} }
@ -270,10 +271,7 @@ export function useRoom(
userId, userId,
publicKey, publicKey,
customUsername, customUsername,
audioChannelState: { audioState: AudioState.STOPPED,
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
[AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED,
},
videoState: VideoState.STOPPED, videoState: VideoState.STOPPED,
screenShareState: ScreenShareState.NOT_SHARING, screenShareState: ScreenShareState.NOT_SHARING,
offeredFileId: null, offeredFileId: null,
@ -325,14 +323,13 @@ export function useRoom(
if (userSettings.showNotificationOnNewMessage) { if (userSettings.showNotificationOnNewMessage) {
const displayUsername = getDisplayUsername(message.authorId) const displayUsername = getDisplayUsername(message.authorId)
notification.showNotification(`${displayUsername}: ${message.text}`) NotificationService.showNotification(
`${displayUsername}: ${message.text}`
)
} }
} }
setMessageLog([ setMessageLog([...messageLog, { ...message, timeReceived: Date.now() }])
...messageLog,
{ ...message, timeReceived: timeService.now() },
])
updatePeer(peerId, { isTyping: false }) updatePeer(peerId, { isTyping: false })
}) })
@ -342,8 +339,9 @@ export function useRoom(
}) })
;(async () => { ;(async () => {
try { try {
const publicKeyString = const publicKeyString = await encryptionService.stringifyCryptoKey(
await encryptionService.stringifyCryptoKey(publicKey) publicKey
)
const promises: Promise<any>[] = [ const promises: Promise<any>[] = [
sendPeerMetadata({ userId, customUsername, publicKeyString }, peerId), sendPeerMetadata({ userId, customUsername, publicKeyString }, peerId),
@ -398,7 +396,7 @@ export function useRoom(
const unsentInlineMedia: UnsentInlineMedia = { const unsentInlineMedia: UnsentInlineMedia = {
authorId: userId, authorId: userId,
magnetURI: fileOfferId, magnetURI: fileOfferId,
timeSent: timeService.now(), timeSent: Date.now(),
id: getUuid(), id: getUuid(),
} }
@ -409,7 +407,7 @@ export function useRoom(
setMessageLog([ setMessageLog([
...messageLog, ...messageLog,
{ ...unsentInlineMedia, timeReceived: timeService.now() }, { ...unsentInlineMedia, timeReceived: Date.now() },
]) ])
setIsMessageSending(false) setIsMessageSending(false)
} }
@ -435,16 +433,13 @@ export function useRoom(
} }
if (userSettings.showNotificationOnNewMessage) { if (userSettings.showNotificationOnNewMessage) {
notification.showNotification( NotificationService.showNotification(
`${getDisplayUsername(inlineMedia.authorId)} shared media` `${getDisplayUsername(inlineMedia.authorId)} shared media`
) )
} }
} }
setMessageLog([ setMessageLog([...messageLog, { ...inlineMedia, timeReceived: Date.now() }])
...messageLog,
{ ...inlineMedia, timeReceived: timeService.now() },
])
}) })
receiveTypingStatusChange((typingStatus, peerId) => { receiveTypingStatusChange((typingStatus, peerId) => {
@ -454,8 +449,9 @@ export function useRoom(
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
const publicKeyString = const publicKeyString = await encryptionService.stringifyCryptoKey(
await encryptionService.stringifyCryptoKey(publicKey) publicKey
)
sendPeerMetadata({ sendPeerMetadata({
customUsername, customUsername,

View File

@ -2,14 +2,8 @@ import { useContext, useEffect, useCallback, useState } from 'react'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { PeerActions } from 'models/network' import { PeerActions } from 'models/network'
import { import { AudioState, Peer } from 'models/chat'
AudioState, import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom'
Peer,
AudioChannelName,
PeerAudioChannelState,
StreamType,
} from 'models/chat'
import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom'
interface UseRoomAudioConfig { interface UseRoomAudioConfig {
peerRoom: PeerRoom peerRoom: PeerRoom
@ -25,7 +19,7 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
string | null string | null
>(null) >(null)
const { setPeerList, setAudioChannelState, setPeerAudioChannels } = const { peerList, setPeerList, setAudioState, peerAudios, setPeerAudios } =
shellContext shellContext
useEffect(() => { useEffect(() => {
@ -38,46 +32,29 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
})() })()
}, [audioStream]) }, [audioStream])
const [sendAudioChange, receiveAudioChange] = peerRoom.makeAction< const [sendAudioChange, receiveAudioChange] = peerRoom.makeAction<AudioState>(
Partial<PeerAudioChannelState> PeerActions.AUDIO_CHANGE
>(PeerActions.AUDIO_CHANGE) )
receiveAudioChange((peerAudioChannelState, peerId) => { receiveAudioChange((audioState, peerId) => {
setPeerList(peerList => { const newPeerList = peerList.map(peer => {
return peerList.map(peer => {
const newPeer: Peer = { ...peer } const newPeer: Peer = { ...peer }
const microphoneAudioChannel =
peerAudioChannelState[AudioChannelName.MICROPHONE]
if (microphoneAudioChannel) {
if (peer.peerId === peerId) { if (peer.peerId === peerId) {
newPeer.audioChannelState = { newPeer.audioState = audioState
...newPeer.audioChannelState,
...peerAudioChannelState,
}
if (microphoneAudioChannel === AudioState.STOPPED) { if (audioState === AudioState.STOPPED) {
deletePeerAudio(peerId) deletePeerAudio(peerId)
} }
} }
}
return newPeer return newPeer
}) })
})
setPeerList(newPeerList)
}) })
peerRoom.onPeerStream(PeerStreamType.AUDIO, (stream, peerId, metadata) => { peerRoom.onPeerStream(PeerStreamType.AUDIO, (stream, peerId) => {
if (
typeof metadata === 'object' &&
metadata !== null &&
'type' in metadata &&
metadata.type !== StreamType.MICROPHONE
) {
return
}
const audioTracks = stream.getAudioTracks() const audioTracks = stream.getAudioTracks()
if (audioTracks.length === 0) return if (audioTracks.length === 0) return
@ -86,13 +63,7 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
audio.srcObject = stream audio.srcObject = stream
audio.autoplay = true audio.autoplay = true
setPeerAudioChannels(peerAudioChannels => ({ setPeerAudios({ ...peerAudios, [peerId]: audio })
...peerAudioChannels,
[peerId]: {
...peerAudioChannels[peerId],
[AudioChannelName.MICROPHONE]: audio,
},
}))
}) })
const cleanupAudio = useCallback(() => { const cleanupAudio = useCallback(() => {
@ -115,19 +86,9 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
video: false, video: false,
}) })
peerRoom.addStream(newSelfStream, null, { peerRoom.addStream(newSelfStream)
type: StreamType.MICROPHONE, sendAudioChange(AudioState.PLAYING)
}) setAudioState(AudioState.PLAYING)
sendAudioChange({
[AudioChannelName.MICROPHONE]: AudioState.PLAYING,
})
setAudioChannelState(prevState => ({
...prevState,
[AudioChannelName.MICROPHONE]: AudioState.PLAYING,
}))
setAudioStream(newSelfStream) setAudioStream(newSelfStream)
} }
} else { } else {
@ -135,16 +96,8 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
cleanupAudio() cleanupAudio()
peerRoom.removeStream(audioStream, peerRoom.getPeers()) peerRoom.removeStream(audioStream, peerRoom.getPeers())
sendAudioChange(AudioState.STOPPED)
sendAudioChange({ setAudioState(AudioState.STOPPED)
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
})
setAudioChannelState(prevState => ({
...prevState,
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
}))
setAudioStream(null) setAudioStream(null)
} }
} }
@ -153,10 +106,11 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
audioStream, audioStream,
cleanupAudio, cleanupAudio,
isSpeakingToRoom, isSpeakingToRoom,
peerAudios,
peerRoom, peerRoom,
selectedAudioDeviceId, selectedAudioDeviceId,
sendAudioChange, sendAudioChange,
setAudioChannelState, setAudioState,
]) ])
useEffect(() => { useEffect(() => {
@ -185,46 +139,28 @@ export function useRoomAudio({ peerRoom }: UseRoomAudioConfig) {
video: false, video: false,
}) })
peerRoom.addStream(newSelfStream, null, { peerRoom.addStream(newSelfStream)
type: StreamType.MICROPHONE,
})
setAudioStream(newSelfStream) setAudioStream(newSelfStream)
} }
const deletePeerAudio = (peerId: string) => { const deletePeerAudio = (peerId: string) => {
setPeerAudioChannels(({ ...newPeerAudios }) => { const newPeerAudios = { ...peerAudios }
if (!newPeerAudios[peerId]) { delete newPeerAudios[peerId]
return newPeerAudios setPeerAudios(newPeerAudios)
}
const microphoneAudio = newPeerAudios[peerId][AudioChannelName.MICROPHONE]
microphoneAudio?.pause()
const { [AudioChannelName.MICROPHONE]: _, ...newPeerAudioChannels } =
newPeerAudios[peerId]
newPeerAudios[peerId] = newPeerAudioChannels
return newPeerAudios
})
} }
const handleAudioForNewPeer = (peerId: string) => { const handleAudioForNewPeer = (peerId: string) => {
if (audioStream) { if (audioStream) {
peerRoom.addStream(audioStream, peerId, { peerRoom.addStream(audioStream, peerId)
type: StreamType.MICROPHONE,
})
} }
} }
const handleAudioForLeavingPeer = (peerId: string) => { const handleAudioForLeavingPeer = (peerId: string) => {
if (audioStream) { if (audioStream) {
peerRoom.removeStream(audioStream, peerId) peerRoom.removeStream(audioStream, peerId)
}
deletePeerAudio(peerId) deletePeerAudio(peerId)
} }
}
peerRoom.onPeerJoin(PeerHookType.AUDIO, (peerId: string) => { peerRoom.onPeerJoin(PeerHookType.AUDIO, (peerId: string) => {
handleAudioForNewPeer(peerId) handleAudioForNewPeer(peerId)

View File

@ -1,12 +1,13 @@
import { useContext, useEffect, useState } from 'react' import { useContext, useEffect, useState } from 'react'
import { sleep } from 'lib/sleep' import { sleep } from 'utils'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { PeerActions } from 'models/network' import { PeerActions } from 'models/network'
import { FileOfferMetadata, Peer } from 'models/chat' import { FileOfferMetadata, Peer } from 'models/chat'
import { PeerRoom, PeerHookType } from 'lib/PeerRoom' import { PeerRoom, PeerHookType } from 'services/PeerRoom'
import { fileTransfer } from 'lib/FileTransfer'
import { fileTransfer } from 'services/FileTransfer/index'
interface UseRoomFileShareConfig { interface UseRoomFileShareConfig {
onInlineMediaUpload: (files: File[]) => void onInlineMediaUpload: (files: File[]) => void

View File

@ -1,17 +1,11 @@
import { useContext, useEffect, useCallback, useState } from 'react' import { useContext, useEffect, useCallback, useState } from 'react'
import { isRecord } from 'lib/type-guards' import { isRecord } from 'utils'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { PeerActions } from 'models/network' import { PeerActions } from 'models/network'
import { import { ScreenShareState, Peer, VideoStreamType } from 'models/chat'
ScreenShareState, import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom'
Peer,
StreamType,
AudioChannelName,
AudioState,
} from 'models/chat'
import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom'
interface UseRoomScreenShareConfig { interface UseRoomScreenShareConfig {
peerRoom: PeerRoom peerRoom: PeerRoom
@ -22,13 +16,7 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
const roomContext = useContext(RoomContext) const roomContext = useContext(RoomContext)
const [isSharingScreen, setIsSharingScreen] = useState(false) const [isSharingScreen, setIsSharingScreen] = useState(false)
const { const { peerList, setPeerList, setScreenState } = shellContext
peerList,
setPeerList,
setScreenState,
setAudioChannelState,
setPeerAudioChannels,
} = shellContext
const { const {
peerScreenStreams, peerScreenStreams,
@ -62,7 +50,7 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
const isScreenShareStream = const isScreenShareStream =
isRecord(metadata) && isRecord(metadata) &&
'type' in metadata && 'type' in metadata &&
metadata.type === StreamType.SCREEN_SHARE metadata.type === VideoStreamType.SCREEN_SHARE
if (!isScreenShareStream) return if (!isScreenShareStream) return
@ -70,33 +58,6 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
...peerScreenStreams, ...peerScreenStreams,
[peerId]: stream, [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(() => { const cleanupScreenStream = useCallback(() => {
@ -117,9 +78,8 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
}) })
peerRoom.addStream(displayMedia, null, { peerRoom.addStream(displayMedia, null, {
type: StreamType.SCREEN_SHARE, type: VideoStreamType.SCREEN_SHARE,
}) })
setSelfScreenStream(displayMedia) setSelfScreenStream(displayMedia)
sendScreenShare(ScreenShareState.SHARING) sendScreenShare(ScreenShareState.SHARING)
setScreenState(ScreenShareState.SHARING) setScreenState(ScreenShareState.SHARING)
@ -159,33 +119,15 @@ export function useRoomScreenShare({ peerRoom }: UseRoomScreenShareConfig) {
}, [setPeerScreenStreams]) }, [setPeerScreenStreams])
const deletePeerScreen = (peerId: string) => { const deletePeerScreen = (peerId: string) => {
setPeerScreenStreams(({ [peerId]: _, ...newPeerScreens }) => { const newPeerScreens = { ...peerScreenStreams }
return newPeerScreens delete newPeerScreens[peerId]
}) setPeerScreenStreams(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) => { const handleScreenForNewPeer = (peerId: string) => {
if (selfScreenStream) { if (selfScreenStream) {
peerRoom.addStream(selfScreenStream, peerId, { peerRoom.addStream(selfScreenStream, peerId, {
type: StreamType.SCREEN_SHARE, type: VideoStreamType.SCREEN_SHARE,
}) })
} }
} }

View File

@ -3,9 +3,10 @@ import { useContext, useEffect, useCallback, useState } from 'react'
import { RoomContext } from 'contexts/RoomContext' import { RoomContext } from 'contexts/RoomContext'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { PeerActions } from 'models/network' import { PeerActions } from 'models/network'
import { VideoState, Peer, StreamType } from 'models/chat' import { VideoState, Peer, VideoStreamType } from 'models/chat'
import { PeerRoom, PeerHookType, PeerStreamType } from 'lib/PeerRoom' import { PeerRoom, PeerHookType, PeerStreamType } from 'services/PeerRoom'
import { isRecord } from 'lib/type-guards'
import { isRecord } from 'utils'
interface UseRoomVideoConfig { interface UseRoomVideoConfig {
peerRoom: PeerRoom peerRoom: PeerRoom
@ -60,9 +61,8 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
}) })
peerRoom.addStream(newSelfStream, null, { peerRoom.addStream(newSelfStream, null, {
type: StreamType.WEBCAM, type: VideoStreamType.WEBCAM,
}) })
setSelfVideoStream(newSelfStream) setSelfVideoStream(newSelfStream)
} }
})() })()
@ -94,7 +94,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
const isWebcamStream = const isWebcamStream =
isRecord(metadata) && isRecord(metadata) &&
'type' in metadata && 'type' in metadata &&
metadata.type === StreamType.WEBCAM metadata.type === VideoStreamType.WEBCAM
if (!isWebcamStream) return if (!isWebcamStream) return
@ -125,9 +125,8 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
}) })
peerRoom.addStream(newSelfStream, null, { peerRoom.addStream(newSelfStream, null, {
type: StreamType.WEBCAM, type: VideoStreamType.WEBCAM,
}) })
sendVideoChange(VideoState.PLAYING) sendVideoChange(VideoState.PLAYING)
setVideoState(VideoState.PLAYING) setVideoState(VideoState.PLAYING)
setSelfVideoStream(newSelfStream) setSelfVideoStream(newSelfStream)
@ -195,7 +194,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
}, },
}) })
peerRoom.addStream(newSelfStream, null, { type: StreamType.WEBCAM }) peerRoom.addStream(newSelfStream, null, { type: VideoStreamType.WEBCAM })
setSelfVideoStream(newSelfStream) setSelfVideoStream(newSelfStream)
} }
@ -208,7 +207,7 @@ export function useRoomVideo({ peerRoom }: UseRoomVideoConfig) {
const handleVideoForNewPeer = (peerId: string) => { const handleVideoForNewPeer = (peerId: string) => {
if (selfVideoStream) { if (selfVideoStream) {
peerRoom.addStream(selfVideoStream, peerId, { peerRoom.addStream(selfVideoStream, peerId, {
type: StreamType.WEBCAM, type: VideoStreamType.WEBCAM,
}) })
} }
} }

View File

@ -6,7 +6,7 @@ import Circle from '@mui/icons-material/FiberManualRecord'
import { Box } from '@mui/system' import { Box } from '@mui/system'
import ReportIcon from '@mui/icons-material/Report' import ReportIcon from '@mui/icons-material/Report'
import { TrackerConnection } from 'lib/ConnectionTest' import { TrackerConnection } from 'services/ConnectionTest/ConnectionTest'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest' import { ConnectionTestResults as IConnectionTestResults } from './useConnectionTest'

View File

@ -145,9 +145,7 @@ export const Drawer = ({ isDrawerOpen, onDrawerClose, theme }: DrawerProps) => {
<MuiLink <MuiLink
target="_blank" target="_blank"
rel="noopener" rel="noopener"
href={`${import.meta.env.VITE_GITHUB_REPO}/commit/${ href={`${process.env.REACT_APP_GITHUB_REPO}/commit/${commit.hash}`}
commit.hash
}`}
> >
{commit.shortHash} {commit.shortHash}
</MuiLink> </MuiLink>

View File

@ -12,7 +12,7 @@ const { isSecureContext, RTCDataChannel } = window
const doesSupportWebRtc = RTCDataChannel !== undefined const doesSupportWebRtc = RTCDataChannel !== undefined
export const isEnvironmentSupported = export const isEnvironmentSupported =
(isSecureContext && doesSupportWebRtc) || import.meta.env.MODE === 'test' (isSecureContext && doesSupportWebRtc) || process.env.NODE_ENV === 'test'
export const EnvironmentUnsupportedDialog = () => { export const EnvironmentUnsupportedDialog = () => {
const theme = useTheme() const theme = useTheme()

View File

@ -2,11 +2,12 @@ import { forwardRef } from 'react'
import Snackbar from '@mui/material/Snackbar' import Snackbar from '@mui/material/Snackbar'
import MuiAlert, { AlertProps, AlertColor } from '@mui/material/Alert' import MuiAlert, { AlertProps, AlertColor } from '@mui/material/Alert'
const Alert = forwardRef<HTMLDivElement, AlertProps>( const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert(
function Alert(props, ref) { props,
ref
) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} /> return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
} })
)
interface NotificationAreaProps { interface NotificationAreaProps {
alertSeverity: AlertColor alertSeverity: AlertColor

View File

@ -0,0 +1,3 @@
.PeerDownloadFileButton
.MuiCircularProgress-circle
transition: none !important

View File

@ -5,11 +5,12 @@ import Tooltip from '@mui/material/Tooltip'
import Download from '@mui/icons-material/Download' import Download from '@mui/icons-material/Download'
import CircularProgress from '@mui/material/CircularProgress' import CircularProgress from '@mui/material/CircularProgress'
import { isError } from 'lib/type-guards' import { isError } from 'utils'
import { fileTransfer } from 'lib/FileTransfer' import { fileTransfer } from 'services/FileTransfer/index'
import { Peer } from 'models/chat' import { Peer } from 'models/chat'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import './PeerDownloadFileButton.sass'
import { usePeerNameDisplay } from 'components/PeerNameDisplay/usePeerNameDisplay' import { usePeerNameDisplay } from 'components/PeerNameDisplay/usePeerNameDisplay'
interface PeerDownloadFileButtonProps { interface PeerDownloadFileButtonProps {
@ -64,9 +65,6 @@ export const PeerDownloadFileButton = ({
<CircularProgress <CircularProgress
variant={downloadProgress === null ? 'indeterminate' : 'determinate'} variant={downloadProgress === null ? 'indeterminate' : 'determinate'}
value={downloadProgress === null ? undefined : downloadProgress} value={downloadProgress === null ? undefined : downloadProgress}
sx={{
transition: 'none',
}}
/> />
) : ( ) : (
<Tooltip <Tooltip

View File

@ -9,15 +9,9 @@ import Box from '@mui/material/Box'
import CircularProgress from '@mui/material/CircularProgress' import CircularProgress from '@mui/material/CircularProgress'
import { UserInfo } from 'components/UserInfo' import { UserInfo } from 'components/UserInfo'
import { import { AudioState, Peer } from 'models/chat'
AudioState, import { PeerConnectionType } from 'services/PeerRoom'
Peer, import { TrackerConnection } from 'services/ConnectionTest'
AudioChannel,
AudioChannelName,
PeerAudioChannelState,
} from 'models/chat'
import { PeerConnectionType } from 'lib/PeerRoom'
import { TrackerConnection } from 'lib/ConnectionTest'
import { PeerListHeader } from './PeerListHeader' import { PeerListHeader } from './PeerListHeader'
import { PeerListItem } from './PeerListItem' import { PeerListItem } from './PeerListItem'
@ -31,8 +25,8 @@ export interface PeerListProps extends PropsWithChildren {
onPeerListClose: () => void onPeerListClose: () => void
peerList: Peer[] peerList: Peer[]
peerConnectionTypes: Record<string, PeerConnectionType> peerConnectionTypes: Record<string, PeerConnectionType>
peerAudioChannelState: PeerAudioChannelState audioState: AudioState
peerAudioChannels: Record<string, AudioChannel> peerAudios: Record<string, HTMLAudioElement>
connectionTestResults: IConnectionTestResults connectionTestResults: IConnectionTestResults
} }
@ -42,8 +36,8 @@ export const PeerList = ({
onPeerListClose, onPeerListClose,
peerList, peerList,
peerConnectionTypes, peerConnectionTypes,
peerAudioChannelState, audioState,
peerAudioChannels, peerAudios,
connectionTestResults, connectionTestResults,
}: PeerListProps) => { }: PeerListProps) => {
return ( return (
@ -55,8 +49,7 @@ export const PeerList = ({
<Divider /> <Divider />
<List> <List>
<ListItem divider={true}> <ListItem divider={true}>
{peerAudioChannelState[AudioChannelName.MICROPHONE] === {audioState === AudioState.PLAYING && (
AudioState.PLAYING && (
<ListItemIcon> <ListItemIcon>
<VolumeUp /> <VolumeUp />
</ListItemIcon> </ListItemIcon>
@ -70,7 +63,7 @@ export const PeerList = ({
key={peer.peerId} key={peer.peerId}
peer={peer} peer={peer}
peerConnectionTypes={peerConnectionTypes} peerConnectionTypes={peerConnectionTypes}
peerAudioChannels={peerAudioChannels} peerAudios={peerAudios}
/> />
))} ))}
{peerList.length === 0 && {peerList.length === 0 &&

View File

@ -18,20 +18,15 @@ import EnhancedEncryptionIcon from '@mui/icons-material/EnhancedEncryption'
import { AudioVolume } from 'components/AudioVolume' import { AudioVolume } from 'components/AudioVolume'
import { PeerNameDisplay } from 'components/PeerNameDisplay' import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { PublicKey } from 'components/PublicKey' import { PublicKey } from 'components/PublicKey'
import { import { Peer, PeerVerificationState } from 'models/chat'
Peer, import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
AudioChannel,
AudioChannelName,
PeerVerificationState,
} from 'models/chat'
import { PeerConnectionType } from 'lib/PeerRoom'
import { PeerDownloadFileButton } from './PeerDownloadFileButton' import { PeerDownloadFileButton } from './PeerDownloadFileButton'
interface PeerListItemProps { interface PeerListItemProps {
peer: Peer peer: Peer
peerConnectionTypes: Record<string, PeerConnectionType> peerConnectionTypes: Record<string, PeerConnectionType>
peerAudioChannels: Record<string, AudioChannel> peerAudios: Record<string, HTMLAudioElement>
} }
const verificationStateDisplayMap = { const verificationStateDisplayMap = {
@ -57,8 +52,8 @@ const iconRightPadding = 1
export const PeerListItem = ({ export const PeerListItem = ({
peer, peer,
peerConnectionTypes, peerConnectionTypes,
peerAudioChannels, peerAudios,
}: PeerListItemProps) => { }: PeerListItemProps): JSX.Element => {
const [showPeerDialog, setShowPeerDialog] = useState(false) const [showPeerDialog, setShowPeerDialog] = useState(false)
const hasPeerConnection = peer.peerId in peerConnectionTypes const hasPeerConnection = peer.peerId in peerConnectionTypes
@ -74,11 +69,6 @@ export const PeerListItem = ({
setShowPeerDialog(false) setShowPeerDialog(false)
} }
const microphoneAudio =
peerAudioChannels[peer.peerId]?.[AudioChannelName.MICROPHONE]
const screenShareAudio =
peerAudioChannels[peer.peerId]?.[AudioChannelName.SCREEN_SHARE]
return ( return (
<> <>
<ListItem key={peer.peerId} divider={true}> <ListItem key={peer.peerId} divider={true}>
@ -134,17 +124,8 @@ export const PeerListItem = ({
</Box> </Box>
<PeerNameDisplay>{peer.userId}</PeerNameDisplay> <PeerNameDisplay>{peer.userId}</PeerNameDisplay>
</Box> </Box>
{microphoneAudio && ( {peer.peerId in peerAudios && (
<AudioVolume <AudioVolume audioEl={peerAudios[peer.peerId]} />
audioEl={microphoneAudio}
audioChannelName={AudioChannelName.MICROPHONE}
/>
)}
{screenShareAudio && (
<AudioVolume
audioEl={screenShareAudio}
audioChannelName={AudioChannelName.SCREEN_SHARE}
/>
)} )}
</ListItemText> </ListItemText>
</ListItem> </ListItem>

View File

@ -14,8 +14,8 @@ import CloseIcon from '@mui/icons-material/Close'
import { AlertOptions } from 'models/shell' import { AlertOptions } from 'models/shell'
import { useEffect, useState, SyntheticEvent } from 'react' import { useEffect, useState, SyntheticEvent } from 'react'
import { sleep } from 'lib/sleep' import { sleep } from 'utils'
import { encryption } from 'services/Encryption' import { encryptionService } from 'services/Encryption'
export interface RoomShareDialogProps { export interface RoomShareDialogProps {
isOpen: boolean isOpen: boolean
@ -51,7 +51,10 @@ export function RoomShareDialog(props: RoomShareDialogProps) {
const url = window.location.href.split('#')[0] const url = window.location.href.split('#')[0]
const copyWithPass = async () => { const copyWithPass = async () => {
const encoded = await encryption.encodePassword(props.roomId, password) const encoded = await encryptionService.encodePassword(
props.roomId,
password
)
if (encoded === props.password) { if (encoded === props.password) {
const params = new URLSearchParams() const params = new URLSearchParams()

View File

@ -35,7 +35,7 @@ describe('Shell', () => {
userEvent.click(menuButton) userEvent.click(menuButton)
}) })
const navigation = screen.getByLabelText('Navigation menu') const navigation = screen.getByRole('navigation')
await waitFor(() => { await waitFor(() => {
expect(navigation).toBeVisible() expect(navigation).toBeVisible()
@ -56,7 +56,7 @@ describe('Shell', () => {
userEvent.click(closeMenu) userEvent.click(closeMenu)
}) })
const navigation = screen.getByLabelText('Navigation menu') const navigation = screen.getByRole('navigation')
await waitFor(() => { await waitFor(() => {
expect(navigation).not.toBeVisible() expect(navigation).not.toBeVisible()

View File

@ -8,7 +8,7 @@ import {
useState, useState,
} from 'react' } from 'react'
import CssBaseline from '@mui/material/CssBaseline' import CssBaseline from '@mui/material/CssBaseline'
import { ThemeProvider } from '@mui/material/styles' import { ThemeProvider, createTheme } from '@mui/material/styles'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { AlertColor } from '@mui/material/Alert' import { AlertColor } from '@mui/material/Alert'
@ -19,17 +19,10 @@ import { useWindowSize } from '@react-hook/window-size'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { AlertOptions, QueryParamKeys } from 'models/shell' import { AlertOptions, QueryParamKeys } from 'models/shell'
import { import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
AudioState,
ScreenShareState,
VideoState,
Peer,
AudioChannel,
PeerAudioChannelState,
AudioChannelName,
} from 'models/chat'
import { ErrorBoundary } from 'components/ErrorBoundary' import { ErrorBoundary } from 'components/ErrorBoundary'
import { PeerConnectionType } from 'lib/PeerRoom'
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
import { Drawer } from './Drawer' import { Drawer } from './Drawer'
import { UpgradeDialog } from './UpgradeDialog' import { UpgradeDialog } from './UpgradeDialog'
@ -45,7 +38,6 @@ import {
EnvironmentUnsupportedDialog, EnvironmentUnsupportedDialog,
isEnvironmentSupported, isEnvironmentSupported,
} from './EnvironmentUnsupportedDialog' } from './EnvironmentUnsupportedDialog'
import { useShellTheme } from './useShellTheme'
export interface ShellProps extends PropsWithChildren { export interface ShellProps extends PropsWithChildren {
userPeerId: string userPeerId: string
@ -58,7 +50,17 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const { getUserSettings, updateUserSettings } = useContext(SettingsContext) const { getUserSettings, updateUserSettings } = useContext(SettingsContext)
const isEmbedded = queryParams.get(QueryParamKeys.IS_EMBEDDED) !== null const isEmbedded = queryParams.get(QueryParamKeys.IS_EMBEDDED) !== null
const theme = useShellTheme() const { colorMode } = getUserSettings()
const theme = useMemo(
() =>
createTheme({
palette: {
mode: colorMode,
},
}),
[colorMode]
)
const [windowWidth] = useWindowSize() const [windowWidth] = useWindowSize()
const defaultSidebarsOpen = windowWidth >= theme.breakpoints.values.lg const defaultSidebarsOpen = windowWidth >= theme.breakpoints.values.lg
@ -85,11 +87,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
Record<string, PeerConnectionType> Record<string, PeerConnectionType>
>({}) >({})
const [tabHasFocus, setTabHasFocus] = useState(true) const [tabHasFocus, setTabHasFocus] = useState(true)
const [audioChannelState, setAudioChannelState] = const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED)
useState<PeerAudioChannelState>({
[AudioChannelName.MICROPHONE]: AudioState.STOPPED,
[AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED,
})
const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED) const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED)
const [screenState, setScreenState] = useState<ScreenShareState>( const [screenState, setScreenState] = useState<ScreenShareState>(
ScreenShareState.NOT_SHARING ScreenShareState.NOT_SHARING
@ -97,8 +95,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const [customUsername, setCustomUsername] = useState( const [customUsername, setCustomUsername] = useState(
getUserSettings().customUsername getUserSettings().customUsername
) )
const [peerAudioChannels, setPeerAudioChannels] = useState< const [peerAudios, setPeerAudios] = useState<
Record<string, AudioChannel> Record<string, HTMLAudioElement>
>({}) >({})
const showAlert = useCallback((message: string, options?: AlertOptions) => { const showAlert = useCallback((message: string, options?: AlertOptions) => {
@ -147,14 +145,14 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setIsServerConnectionFailureDialogOpen, setIsServerConnectionFailureDialogOpen,
peerConnectionTypes, peerConnectionTypes,
setPeerConnectionTypes, setPeerConnectionTypes,
audioChannelState, audioState,
setAudioChannelState, setAudioState,
videoState, videoState,
setVideoState, setVideoState,
screenState, screenState,
setScreenState, setScreenState,
peerAudioChannels, peerAudios,
setPeerAudioChannels, setPeerAudios,
customUsername, customUsername,
setCustomUsername, setCustomUsername,
connectionTestResults, connectionTestResults,
@ -177,14 +175,14 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setShowRoomControls, setShowRoomControls,
setTitle, setTitle,
showAlert, showAlert,
audioChannelState, audioState,
setAudioChannelState, setAudioState,
videoState, videoState,
setVideoState, setVideoState,
screenState, screenState,
setScreenState, setScreenState,
peerAudioChannels, peerAudios,
setPeerAudioChannels, setPeerAudios,
customUsername, customUsername,
setCustomUsername, setCustomUsername,
connectionTestResults, connectionTestResults,
@ -396,8 +394,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
onPeerListClose={handlePeerListClick} onPeerListClose={handlePeerListClick}
peerList={peerList} peerList={peerList}
peerConnectionTypes={peerConnectionTypes} peerConnectionTypes={peerConnectionTypes}
peerAudioChannelState={audioChannelState} audioState={audioState}
peerAudioChannels={peerAudioChannels} peerAudios={peerAudios}
connectionTestResults={connectionTestResults} connectionTestResults={connectionTestResults}
/> />
{isEmbedded ? ( {isEmbedded ? (

View File

@ -6,17 +6,14 @@ import DialogContent from '@mui/material/DialogContent'
import DialogContentText from '@mui/material/DialogContentText' import DialogContentText from '@mui/material/DialogContentText'
import DialogTitle from '@mui/material/DialogTitle' import DialogTitle from '@mui/material/DialogTitle'
import WarningIcon from '@mui/icons-material/Warning' import WarningIcon from '@mui/icons-material/Warning'
import { useRegisterSW } from 'virtual:pwa-register/react'
interface UpgradeDialogProps { interface UpgradeDialogProps {
appNeedsUpdate: boolean appNeedsUpdate: boolean
} }
export const UpgradeDialog = ({ appNeedsUpdate }: UpgradeDialogProps) => { export const UpgradeDialog = ({ appNeedsUpdate }: UpgradeDialogProps) => {
const { updateServiceWorker } = useRegisterSW()
const handleRestartClick = () => { const handleRestartClick = () => {
updateServiceWorker(true) window.location.reload()
} }
return ( return (

View File

@ -1,12 +1,11 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { sleep } from 'utils'
import { sleep } from 'lib/sleep'
import { import {
ConnectionTest, ConnectionTest,
ConnectionTestEvent, ConnectionTestEvent,
ConnectionTestEvents, ConnectionTestEvents,
TrackerConnection, TrackerConnection,
} from 'lib/ConnectionTest' } from 'services/ConnectionTest/ConnectionTest'
export interface ConnectionTestResults { export interface ConnectionTestResults {
hasHost: boolean hasHost: boolean

View File

@ -1,23 +0,0 @@
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
}

View File

@ -1 +0,0 @@
export const communityRoomNames = ['General', 'Prayer']

View File

@ -11,32 +11,22 @@ export const rtcConfig: RTCConfiguration = {
// CONNECTED TO THE INTERNET. // CONNECTED TO THE INTERNET.
iceServers: [ iceServers: [
{ {
urls: 'turn:relay1.expressturn.com:3478', urls: 'stun:188.148.133.173:3478',
username: 'efQUQ79N77B5BNVVKF',
credential: 'N4EAUgpjMzPLrxSS',
}, },
{ {
urls: 'stun:stun.relay.metered.ca:80', urls: 'turn:188.148.133.173:3478',
username: 'c386d75b5633456cb3bc13812858098d',
credential: '58fd06d85fe14c0f9f46220748b0f565',
}, },
{ {
urls: 'turn:global.relay.metered.ca:80', urls: 'turn:188.148.133.173:3478',
username: 'cfd3dccc1b9162a136a80482', username: '0e2f563eacfd4c4a82ea239b04d1d494',
credential: '1TgyKEd7B6iUxIn9', credential: '8179b4b533f240ad9fe590663bef1bc9',
}, },
{ {
urls: 'turn:global.relay.metered.ca:80?transport=tcp', urls: 'turn:188.148.133.173:3478',
username: 'cfd3dccc1b9162a136a80482', username: 'feab95c3fcd147a2a96a3d3590bf9cda',
credential: '1TgyKEd7B6iUxIn9', credential: '654cafd885424b7fb974e65f631f25f9',
},
{
urls: 'turn:global.relay.metered.ca:443',
username: 'cfd3dccc1b9162a136a80482',
credential: '1TgyKEd7B6iUxIn9',
},
{
urls: 'turns:global.relay.metered.ca:443?transport=tcp',
username: 'cfd3dccc1b9162a136a80482',
credential: '1TgyKEd7B6iUxIn9',
}, },
], ],
} }

View File

@ -1,6 +1,6 @@
export const streamSaverUrl = export const streamSaverUrl =
import.meta.env.VITE_STREAMSAVER_URL ?? process.env.REACT_APP_STREAMSAVER_URL ??
// If you would like to host your own Chitchatter instance with an // If you would like to host your own RemnantChat instance with an
// alternative StreamSaver fork to facilitate file sharing, change this // alternative StreamSaver fork to facilitate file sharing, change this
// string to its respective .mitm.html URL. // string to its respective .mitm.html URL.
// //

View File

@ -8,12 +8,12 @@ let trackerUrls: string[] | undefined = [
// https://github.com/dmotz/trystero/blob/694f49974974cc9df8b621db09215d6df10fad09/src/torrent.js#L27-L33 // https://github.com/dmotz/trystero/blob/694f49974974cc9df8b621db09215d6df10fad09/src/torrent.js#L27-L33
] ]
// If a tracker URL has been provided via the VITE_TRACKER_URL environment // If a tracker URL has been provided via the REACT_APP_TRACKER_URL environment
// variable, prioritize using it. This is mainly relevant for local development // 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 // when using the `npm run dev` script. If you are hosting your own RemnantChat
// instance, consider populating the trackerUrls above instead. // instance, consider populating the trackerUrls above instead.
if (import.meta.env.VITE_TRACKER_URL) { if (process.env.REACT_APP_TRACKER_URL) {
trackerUrls.unshift(import.meta.env.VITE_TRACKER_URL) trackerUrls.unshift(process.env.REACT_APP_TRACKER_URL)
} }
// If no tracker URL overrides have been provided, set trackerUrls to undefined // If no tracker URL overrides have been provided, set trackerUrls to undefined

View File

@ -1,7 +1,7 @@
import { createContext } from 'react' import { createContext } from 'react'
import { ColorMode, UserSettings } from 'models/settings' import { ColorMode, UserSettings } from 'models/settings'
import { encryption } from 'services/Encryption' import { encryptionService } from 'services/Encryption'
export interface SettingsContextProps { export interface SettingsContextProps {
updateUserSettings: (settings: Partial<UserSettings>) => Promise<void> updateUserSettings: (settings: Partial<UserSettings>) => Promise<void>
@ -17,7 +17,7 @@ export const SettingsContext = createContext<SettingsContextProps>({
playSoundOnNewMessage: true, playSoundOnNewMessage: true,
showNotificationOnNewMessage: true, showNotificationOnNewMessage: true,
showActiveTypingStatus: true, showActiveTypingStatus: true,
publicKey: encryption.cryptoKeyStub, publicKey: encryptionService.cryptoKeyStub,
privateKey: encryption.cryptoKeyStub, privateKey: encryptionService.cryptoKeyStub,
}), }),
}) })

View File

@ -1,18 +1,10 @@
import { createContext, Dispatch, SetStateAction } from 'react' import { createContext, Dispatch, SetStateAction } from 'react'
import { AlertOptions } from 'models/shell' import { AlertOptions } from 'models/shell'
import { import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
AudioState, import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
ScreenShareState,
VideoState,
Peer,
AudioChannel,
PeerAudioChannelState,
AudioChannelName,
} from 'models/chat'
import { PeerConnectionType } from 'lib/PeerRoom'
import { ConnectionTestResults } from 'components/Shell/useConnectionTest' import { ConnectionTestResults } from 'components/Shell/useConnectionTest'
import { TrackerConnection } from 'lib/ConnectionTest' import { TrackerConnection } from 'services/ConnectionTest/ConnectionTest'
interface ShellContextProps { interface ShellContextProps {
isEmbedded: boolean isEmbedded: boolean
@ -35,14 +27,14 @@ interface ShellContextProps {
setPeerConnectionTypes: Dispatch< setPeerConnectionTypes: Dispatch<
SetStateAction<Record<string, PeerConnectionType>> SetStateAction<Record<string, PeerConnectionType>>
> >
audioChannelState: PeerAudioChannelState audioState: AudioState
setAudioChannelState: Dispatch<SetStateAction<PeerAudioChannelState>> setAudioState: Dispatch<SetStateAction<AudioState>>
videoState: VideoState videoState: VideoState
setVideoState: Dispatch<SetStateAction<VideoState>> setVideoState: Dispatch<SetStateAction<VideoState>>
screenState: ScreenShareState screenState: ScreenShareState
setScreenState: Dispatch<SetStateAction<ScreenShareState>> setScreenState: Dispatch<SetStateAction<ScreenShareState>>
peerAudioChannels: Record<string, AudioChannel> peerAudios: Record<string, HTMLAudioElement>
setPeerAudioChannels: Dispatch<SetStateAction<Record<string, AudioChannel>>> setPeerAudios: Dispatch<SetStateAction<Record<string, HTMLAudioElement>>>
customUsername: string customUsername: string
setCustomUsername: Dispatch<SetStateAction<string>> setCustomUsername: Dispatch<SetStateAction<string>>
connectionTestResults: ConnectionTestResults connectionTestResults: ConnectionTestResults
@ -68,17 +60,14 @@ export const ShellContext = createContext<ShellContextProps>({
setIsServerConnectionFailureDialogOpen: () => {}, setIsServerConnectionFailureDialogOpen: () => {},
peerConnectionTypes: {}, peerConnectionTypes: {},
setPeerConnectionTypes: () => {}, setPeerConnectionTypes: () => {},
audioChannelState: { audioState: AudioState.STOPPED,
[AudioChannelName.MICROPHONE]: AudioState.STOPPED, setAudioState: () => {},
[AudioChannelName.SCREEN_SHARE]: AudioState.STOPPED,
},
setAudioChannelState: () => {},
videoState: VideoState.STOPPED, videoState: VideoState.STOPPED,
setVideoState: () => {}, setVideoState: () => {},
screenState: ScreenShareState.NOT_SHARING, screenState: ScreenShareState.NOT_SHARING,
setScreenState: () => {}, setScreenState: () => {},
peerAudioChannels: {}, peerAudios: {},
setPeerAudioChannels: () => {}, setPeerAudios: () => {},
customUsername: '', customUsername: '',
setCustomUsername: () => {}, setCustomUsername: () => {},
connectionTestResults: { connectionTestResults: {

View File

@ -1,20 +0,0 @@
a {
color: inherit;
text-decoration: inherit;
}
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}

9
src/index.sass Normal file
View File

@ -0,0 +1,9 @@
@tailwind base
@tailwind components
@tailwind utilities
ol
@apply pl-4 list-decimal
ul
@apply pl-4 list-disc

View File

@ -2,29 +2,10 @@ import './polyfills'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import 'typeface-roboto' import 'typeface-roboto'
import 'modern-normalize/modern-normalize.css' import './index.sass'
import './index.css'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter'
import Init from './Init' import Init from './Init'
import reportWebVitals from './reportWebVitals' 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) const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(<Init />) root.render(<Init />)

View File

@ -1,7 +0,0 @@
export class Time {
now = () => {
return Date.now()
}
}
export const time = new Time()

View File

@ -1 +0,0 @@
export * from './Time'

View File

@ -1,4 +0,0 @@
export const sleep = (milliseconds: number): Promise<void> =>
new Promise<void>(res => {
setTimeout(res, milliseconds)
})

View File

@ -1,4 +1,4 @@
export interface UnsentMessage extends Record<string, any> { export interface UnsentMessage {
id: string id: string
text: string text: string
timeSent: number timeSent: number
@ -31,10 +31,9 @@ export enum VideoState {
STOPPED = 'STOPPED', STOPPED = 'STOPPED',
} }
export enum StreamType { export enum VideoStreamType {
WEBCAM = 'WEBCAM', WEBCAM = 'WEBCAM',
SCREEN_SHARE = 'SCREEN_SHARE', SCREEN_SHARE = 'SCREEN_SHARE',
MICROPHONE = 'MICROPHONE',
} }
export enum ScreenShareState { export enum ScreenShareState {
@ -48,21 +47,12 @@ export enum PeerVerificationState {
VERIFIED, 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 { export interface Peer {
peerId: string peerId: string
userId: string userId: string
publicKey: CryptoKey publicKey: CryptoKey
customUsername: string customUsername: string
audioChannelState: PeerAudioChannelState audioState: AudioState
videoState: VideoState videoState: VideoState
screenShareState: ScreenShareState screenShareState: ScreenShareState
offeredFileId: string | null offeredFileId: string | null
@ -83,11 +73,11 @@ export const isInlineMedia = (
return 'magnetURI' in message return 'magnetURI' in message
} }
export interface FileOfferMetadata extends Record<string, any> { export interface FileOfferMetadata {
magnetURI: string magnetURI: string
isAllInlineMedia: boolean isAllInlineMedia: boolean
} }
export interface TypingStatus extends Record<string, any> { export interface TypingStatus {
isTyping: boolean isTyping: boolean
} }

View File

@ -1,7 +1,6 @@
import { useContext, useEffect } from 'react' import { useContext, useEffect } from 'react'
import MuiMarkdown from 'mui-markdown' import MuiMarkdown from 'mui-markdown'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import useTheme from '@mui/material/styles/useTheme'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { import {
@ -9,6 +8,8 @@ import {
messageCharacterSizeLimit, messageCharacterSizeLimit,
} from 'config/messaging' } from 'config/messaging'
import './index.sass'
const messageTranscriptSizeLimitFormatted = Intl.NumberFormat().format( const messageTranscriptSizeLimitFormatted = Intl.NumberFormat().format(
messageTranscriptSizeLimit messageTranscriptSizeLimit
) )
@ -19,24 +20,13 @@ const messageCharacterSizeLimitFormatted = Intl.NumberFormat().format(
export const About = () => { export const About = () => {
const { setTitle } = useContext(ShellContext) const { setTitle } = useContext(ShellContext)
const theme = useTheme()
useEffect(() => { useEffect(() => {
setTitle('About') setTitle('About')
}, [setTitle]) }, [setTitle])
return ( return (
<Box <Box className="About max-w-3xl mx-auto p-4">
className="About"
sx={{
p: 2,
mx: 'auto',
maxWidth: theme.breakpoints.values.md,
'& p': {
mb: 2,
},
}}
>
<MuiMarkdown> <MuiMarkdown>
{` {`
### User Guide ### User Guide

View File

@ -0,0 +1,3 @@
.About
p
@apply mb-4

View File

@ -1,30 +1,20 @@
import { useContext, useEffect } from 'react' import { useContext, useEffect } from 'react'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import MuiMarkdown from 'mui-markdown' import MuiMarkdown from 'mui-markdown'
import useTheme from '@mui/material/styles/useTheme'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import './index.sass'
export const Disclaimer = () => { export const Disclaimer = () => {
const { setTitle } = useContext(ShellContext) const { setTitle } = useContext(ShellContext)
const theme = useTheme()
useEffect(() => { useEffect(() => {
setTitle('Disclaimer') setTitle('Disclaimer')
}, [setTitle]) }, [setTitle])
return ( return (
<Box <Box className="Disclaimer max-w-3xl mx-auto p-4">
className="Disclaimer"
sx={{
p: 2,
mx: 'auto',
maxWidth: theme.breakpoints.values.md,
'& p': {
mb: 2,
},
}}
>
<MuiMarkdown> <MuiMarkdown>
{` {`
### Interpretation and Definitions ### Interpretation and Definitions

View File

@ -0,0 +1,6 @@
.Disclaimer
ul
@apply my-4
p
@apply mb-4

View File

@ -1,67 +0,0 @@
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>
)
}

View File

@ -10,21 +10,15 @@ import IconButton from '@mui/material/IconButton'
import MuiLink from '@mui/material/Link' import MuiLink from '@mui/material/Link'
import Cached from '@mui/icons-material/Cached' 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 { v4 as uuid } from 'uuid'
import { routes } from 'config/routes' import { routes } from 'config/routes'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { PeerNameDisplay } from 'components/PeerNameDisplay' import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { Form, Main } from 'components/Elements' import { ReactComponent as Logo } from 'img/logo.svg'
import Logo from 'img/logo.svg?react'
import { EmbedCodeDialog } from './EmbedCodeDialog' import { EmbedCodeDialog } from './EmbedCodeDialog'
import { CommunityRoomSelector } from './CommunityRoomSelector'
const StyledLogo = styled(Logo)({})
interface HomeProps { interface HomeProps {
userId: string userId: string
@ -32,7 +26,6 @@ interface HomeProps {
export function Home({ userId }: HomeProps) { export function Home({ userId }: HomeProps) {
const { setTitle } = useContext(ShellContext) const { setTitle } = useContext(ShellContext)
const theme = useTheme()
const [roomName, setRoomName] = useState(uuid()) const [roomName, setRoomName] = useState(uuid())
const [showEmbedCode, setShowEmbedCode] = useState(false) const [showEmbedCode, setShowEmbedCode] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
@ -70,29 +63,11 @@ export function Home({ userId }: HomeProps) {
return ( return (
<Box className="Home"> <Box className="Home">
<Main <main className="mt-6 px-4 max-w-3xl text-center mx-auto">
sx={{
maxWidth: theme.breakpoints.values.md,
mt: 3,
mx: 'auto',
px: 2,
textAlign: 'center',
}}
>
<Link to={routes.ABOUT}> <Link to={routes.ABOUT}>
<StyledLogo <Logo className="px-1 pb-4 mx-auto max-w-md" />
sx={{
px: 0.5,
pb: 2,
mx: 'auto',
maxWidth: theme.breakpoints.values.sm,
}}
/>
</Link> </Link>
<Form <form onSubmit={handleFormSubmit} className="max-w-xl mx-auto">
onSubmit={handleFormSubmit}
sx={{ maxWidth: theme.breakpoints.values.sm, mx: 'auto' }}
>
<Typography sx={{ mb: 2 }}> <Typography sx={{ mb: 2 }}>
Your username:{' '} Your username:{' '}
<PeerNameDisplay paragraph={false} sx={{ fontWeight: 'bold' }}> <PeerNameDisplay paragraph={false} sx={{ fontWeight: 'bold' }}>
@ -101,7 +76,7 @@ export function Home({ userId }: HomeProps) {
</Typography> </Typography>
<FormControl fullWidth> <FormControl fullWidth>
<TextField <TextField
label="Room name (generated on your device)" label="Room name (generated client-side)"
variant="outlined" variant="outlined"
value={roomName} value={roomName}
onChange={handleRoomNameChange} onChange={handleRoomNameChange}
@ -160,28 +135,14 @@ export function Home({ userId }: HomeProps) {
Get embed code Get embed code
</Button> </Button>
</Box> </Box>
</Form> </form>
</Main> </main>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
<Box maxWidth={theme.breakpoints.values.sm} mx="auto" px={2}> <Box className="max-w-3xl text-center mx-auto px-4">
<CommunityRoomSelector />
</Box>
<Divider sx={{ my: 2 }} />
<Box
sx={{
maxWidth: theme.breakpoints.values.sm,
mx: 'auto',
textAlign: 'center',
px: 2,
}}
>
<Typography variant="body1"> <Typography variant="body1">
WEBRTC settings must be enabled in your browser. Is webrtc{' '} The secure communication tool that is designed for simplicity,
<MuiLink href="https://webrtc-security.github.io/" target="_blank"> privacy. All interaction between you and your online peers is
secure encrypted. Conversation records are dumped once everyone leaves.
</MuiLink>
? All interaction between you and your online peers is encrypted.
Conversation records are dumped once everyone leaves.
</Typography> </Typography>
<Box className="max-w-3xl text-center mx-auto my-4 px-4"> <Box className="max-w-3xl text-center mx-auto my-4 px-4">
{' '} {' '}
@ -189,7 +150,19 @@ export function Home({ userId }: HomeProps) {
</Box> </Box>
</Box> </Box>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
<Typography variant="body1" sx={{ textAlign: 'center', mb: 1 }}> <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">&#x2022; General Chat</Typography>
</MuiLink>
</Box>
<Typography variant="body1" sx={{ textAlign: 'center' }}>
Licensed under{' '} Licensed under{' '}
<MuiLink <MuiLink
href="https://githaven.org/Shiloh/remnantchat/src/branch/main/LICENSE" href="https://githaven.org/Shiloh/remnantchat/src/branch/main/LICENSE"
@ -202,8 +175,9 @@ export function Home({ userId }: HomeProps) {
href="https://githaven.org/Shiloh/remnantchat/src/branch/main/README.md" href="https://githaven.org/Shiloh/remnantchat/src/branch/main/README.md"
target="_blank" target="_blank"
> >
read the docs. read the docs
</MuiLink> </MuiLink>
.
</Typography> </Typography>
<EmbedCodeDialog <EmbedCodeDialog
showEmbedCode={showEmbedCode} showEmbedCode={showEmbedCode}

View File

@ -3,9 +3,9 @@ import { Room } from 'components/Room'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { notification } from 'services/Notification' import { NotificationService } from 'services/Notification'
import { PasswordPrompt } from 'components/PasswordPrompt' import { PasswordPrompt } from 'components/PasswordPrompt'
import { encryption } from 'services/Encryption' import { encryptionService } from 'services/Encryption'
interface PublicRoomProps { interface PublicRoomProps {
userId: string userId: string
@ -22,7 +22,7 @@ export function PrivateRoom({ userId }: PublicRoomProps) {
const [secret, setSecret] = useState(urlParams.get('secret') ?? '') const [secret, setSecret] = useState(urlParams.get('secret') ?? '')
useEffect(() => { useEffect(() => {
notification.requestPermission() NotificationService.requestPermission()
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -31,7 +31,7 @@ export function PrivateRoom({ userId }: PublicRoomProps) {
const handlePasswordEntered = async (password: string) => { const handlePasswordEntered = async (password: string) => {
if (password.length !== 0) if (password.length !== 0)
setSecret(await encryption.encodePassword(roomId, password)) setSecret(await encryptionService.encodePassword(roomId, password))
} }
if (urlParams.has('pwd') && !urlParams.has('secret')) if (urlParams.has('pwd') && !urlParams.has('secret'))

View File

@ -3,7 +3,7 @@ import { Room } from 'components/Room'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { notification } from 'services/Notification' import { NotificationService } from 'services/Notification'
interface PublicRoomProps { interface PublicRoomProps {
userId: string userId: string
@ -14,7 +14,7 @@ export function PublicRoom({ userId }: PublicRoomProps) {
const { setTitle } = useContext(ShellContext) const { setTitle } = useContext(ShellContext)
useEffect(() => { useEffect(() => {
notification.requestPermission() NotificationService.requestPermission()
}, []) }, [])
useEffect(() => { useEffect(() => {

View File

@ -10,15 +10,15 @@ import FormControlLabel from '@mui/material/FormControlLabel'
import Paper from '@mui/material/Paper' import Paper from '@mui/material/Paper'
import useTheme from '@mui/material/styles/useTheme' import useTheme from '@mui/material/styles/useTheme'
import { settings } from 'services/Settings' import { settingsService } from 'services/Settings'
import { notification } from 'services/Notification' import { NotificationService } from 'services/Notification'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { StorageContext } from 'contexts/StorageContext' import { StorageContext } from 'contexts/StorageContext'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { PeerNameDisplay } from 'components/PeerNameDisplay' import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { ConfirmDialog } from 'components/ConfirmDialog' import { ConfirmDialog } from 'components/ConfirmDialog'
import { isErrorWithMessage } from '../../lib/type-guards' import { isErrorWithMessage } from '../../utils'
interface SettingsProps { interface SettingsProps {
userId: string userId: string
@ -45,7 +45,7 @@ export const Settings = ({ userId }: SettingsProps) => {
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
await notification.requestPermission() await NotificationService.requestPermission()
// This state needs to be set to cause a rerender so that // This state needs to be set to cause a rerender so that
// areNotificationsAvailable is up-to-date. // areNotificationsAvailable is up-to-date.
@ -93,7 +93,7 @@ export const Settings = ({ userId }: SettingsProps) => {
const handleExportSettingsClick = async () => { const handleExportSettingsClick = async () => {
try { try {
await settings.exportSettings(getUserSettings()) await settingsService.exportSettings(getUserSettings())
} catch (e) { } catch (e) {
if (isErrorWithMessage(e)) { if (isErrorWithMessage(e)) {
showAlert(e.message, { severity: 'error' }) showAlert(e.message, { severity: 'error' })
@ -103,7 +103,7 @@ export const Settings = ({ userId }: SettingsProps) => {
const handleImportSettingsClick = async ([[, file]]: Result[]) => { const handleImportSettingsClick = async ([[, file]]: Result[]) => {
try { try {
const userSettings = await settings.importSettings(file) const userSettings = await settingsService.importSettings(file)
updateUserSettings(userSettings) updateUserSettings(userSettings)
@ -115,10 +115,10 @@ export const Settings = ({ userId }: SettingsProps) => {
} }
} }
const areNotificationsAvailable = notification.permission === 'granted' const areNotificationsAvailable = NotificationService.permission === 'granted'
return ( return (
<Box sx={{ p: 2, mx: 'auto', maxWidth: theme.breakpoints.values.md }}> <Box className="max-w-3xl mx-auto p-4">
<Typography <Typography
variant="h2" variant="h2"
sx={{ sx={{

View File

@ -1 +1,9 @@
import 'webrtc-adapter' import 'webrtc-adapter'
import { Buffer } from 'buffer'
// @ts-ignore
import process from 'process/browser'
// Polyfill
window.Buffer = Buffer
window.process = process

View File

@ -1 +1 @@
/// <reference types="vite-plugin-svgr/client" /> /// <reference types="react-scripts" />

73
src/service-worker.js Normal file
View File

@ -0,0 +1,73 @@
/* 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.

View File

@ -0,0 +1,141 @@
// 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)
})
}
}

View File

@ -1,4 +1,4 @@
import { getRelaySockets } from 'trystero/torrent' import { getTrackers } from 'trystero/torrent'
import { rtcConfig } from 'config/rtcConfig' import { rtcConfig } from 'config/rtcConfig'
import { parseCandidate } from 'sdp' import { parseCandidate } from 'sdp'
@ -104,15 +104,17 @@ export class ConnectionTest extends EventTarget {
} }
testTrackerConnection() { testTrackerConnection() {
const relaySockets = Object.values(getRelaySockets()) const trackers = getTrackers()
if (relaySockets.length === 0) { const trackerSockets = Object.values(trackers)
if (trackerSockets.length === 0) {
// Trystero has not yet initialized tracker sockets // Trystero has not yet initialized tracker sockets
this.trackerConnection = TrackerConnection.SEARCHING this.trackerConnection = TrackerConnection.SEARCHING
return this.trackerConnection return this.trackerConnection
} }
const readyStates = relaySockets.map(({ readyState }) => readyState) const readyStates = trackerSockets.map(({ readyState }) => readyState)
const haveAllTrackerConnectionsFailed = readyStates.every( const haveAllTrackerConnectionsFailed = readyStates.every(
readyState => readyState === WebSocket.CLOSED readyState => readyState === WebSocket.CLOSED

View File

@ -110,4 +110,4 @@ export class EncryptionService {
} }
} }
export const encryption = new EncryptionService() export const encryptionService = new EncryptionService()

View File

@ -1,17 +1,18 @@
export class NotificationService { export class NotificationService {
permission: NotificationPermission = 'default' static permission: NotificationPermission
requestPermission = async () => { static requestPermission = async () => {
if (this.permission === 'granted') return if (NotificationService.permission === 'granted') return
this.permission = await Notification.requestPermission() NotificationService.permission = await Notification.requestPermission()
} }
showNotification = (message: string, options?: NotificationOptions) => { static showNotification = (
if (this.permission !== 'granted') return message: string,
options?: NotificationOptions
) => {
if (NotificationService.permission !== 'granted') return
new Notification(message, options) new Notification(message, options)
} }
} }
export const notification = new NotificationService()

View File

@ -1,8 +1,7 @@
import { joinRoom, Room, BaseRoomConfig, DataPayload } from 'trystero/torrent' import { joinRoom, Room, BaseRoomConfig } from 'trystero'
import { RelayConfig } from 'trystero/torrent' import { TorrentRoomConfig } from 'trystero/torrent'
import { sleep } from 'lib/sleep' import { sleep } from 'utils'
import { StreamType } from 'models/chat'
export enum PeerHookType { export enum PeerHookType {
NEW_PEER = 'NEW_PEER', NEW_PEER = 'NEW_PEER',
@ -28,7 +27,7 @@ const streamQueueAddDelay = 1000
export class PeerRoom { export class PeerRoom {
private room: Room private room: Room
private roomConfig: RelayConfig & BaseRoomConfig private roomConfig: TorrentRoomConfig & BaseRoomConfig
private peerJoinHandlers: Map< private peerJoinHandlers: Map<
PeerHookType, PeerHookType,
@ -49,19 +48,7 @@ export class PeerRoom {
private isProcessingPendingStreams = false private isProcessingPendingStreams = false
private processPendingStreams = async () => { constructor(config: TorrentRoomConfig & BaseRoomConfig, roomId: string) {
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.roomConfig = config
this.room = joinRoom(this.roomConfig, roomId) this.room = joinRoom(this.roomConfig, roomId)
@ -168,26 +155,34 @@ export class PeerRoom {
return peerConnections return peerConnections
} }
makeAction = <T extends DataPayload>(namespace: string) => { makeAction = <T>(namespace: string) => {
return this.room.makeAction<T>(namespace) return this.room.makeAction<T>(namespace)
} }
addStream = ( addStream = (...args: Parameters<Room['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 // 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 // conditions on the receiver's end where streams and their metadata get
// mixed up. // mixed up.
this.streamQueue.push( this.streamQueue.push(
() => Promise.all(this.room.addStream(stream, targetPeers, metadata)), () => Promise.all(this.room.addStream(...args)),
() => sleep(streamQueueAddDelay) () => sleep(streamQueueAddDelay)
) )
this.processPendingStreams() 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) => { removeStream: Room['removeStream'] = (stream, targetPeers) => {
return this.room.removeStream(stream, targetPeers) return this.room.removeStream(stream, targetPeers)
} }

View File

@ -1,5 +1,5 @@
import { ColorMode, UserSettings } from 'models/settings' import { ColorMode, UserSettings } from 'models/settings'
import { AllowedKeyType, encryption } from 'services/Encryption' import { AllowedKeyType, encryptionService } from 'services/Encryption'
export interface SerializedUserSettings export interface SerializedUserSettings
extends Omit<UserSettings, 'publicKey' | 'privateKey'> { extends Omit<UserSettings, 'publicKey' | 'privateKey'> {
@ -42,9 +42,13 @@ export class SerializationService {
...userSettingsRest ...userSettingsRest
} = userSettings } = userSettings
const publicKey = await encryption.stringifyCryptoKey(publicCryptoKey) const publicKey = await encryptionService.stringifyCryptoKey(
publicCryptoKey
)
const privateKey = await encryption.stringifyCryptoKey(privateCryptoKey) const privateKey = await encryptionService.stringifyCryptoKey(
privateCryptoKey
)
return { return {
...userSettingsRest, ...userSettingsRest,
@ -62,11 +66,11 @@ export class SerializationService {
...userSettingsForIndexedDbRest ...userSettingsForIndexedDbRest
} = serializedUserSettings } = serializedUserSettings
const publicKey = await encryption.parseCryptoKeyString( const publicKey = await encryptionService.parseCryptoKeyString(
publicCryptoKeyString, publicCryptoKeyString,
AllowedKeyType.PUBLIC AllowedKeyType.PUBLIC
) )
const privateKey = await encryption.parseCryptoKeyString( const privateKey = await encryptionService.parseCryptoKeyString(
privateCryptoKeyString, privateCryptoKeyString,
AllowedKeyType.PRIVATE AllowedKeyType.PRIVATE
) )
@ -79,4 +83,4 @@ export class SerializationService {
} }
} }
export const serialization = new SerializationService() export const serializationService = new SerializationService()

View File

@ -1,10 +1,10 @@
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import { UserSettings } from 'models/settings' import { UserSettings } from 'models/settings'
import { encryption } from 'services/Encryption' import { encryptionService } from 'services/Encryption'
import { import {
isSerializedUserSettings, isSerializedUserSettings,
serialization, serializationService,
} from 'services/Serialization/Serialization' } from 'services/Serialization/Serialization'
class InvalidFileError extends Error { class InvalidFileError extends Error {
@ -16,7 +16,7 @@ const encryptionTestTarget = 'remnantchat'
export class SettingsService { export class SettingsService {
exportSettings = async (userSettings: UserSettings) => { exportSettings = async (userSettings: UserSettings) => {
const serializedUserSettings = const serializedUserSettings =
await serialization.serializeUserSettings(userSettings) await serializationService.serializeUserSettings(userSettings)
const blob = new Blob([JSON.stringify(serializedUserSettings)], { const blob = new Blob([JSON.stringify(serializedUserSettings)], {
type: 'application/json;charset=utf-8', type: 'application/json;charset=utf-8',
@ -44,14 +44,14 @@ export class SettingsService {
} }
const deserializedUserSettings = const deserializedUserSettings =
await serialization.deserializeUserSettings(parsedFileResult) await serializationService.deserializeUserSettings(parsedFileResult)
const encryptedString = await encryption.encryptString( const encryptedString = await encryptionService.encryptString(
deserializedUserSettings.publicKey, deserializedUserSettings.publicKey,
encryptionTestTarget encryptionTestTarget
) )
const decryptedString = await encryption.decryptString( const decryptedString = await encryptionService.decryptString(
deserializedUserSettings.privateKey, deserializedUserSettings.privateKey,
encryptedString encryptedString
) )
@ -77,4 +77,4 @@ export class SettingsService {
} }
} }
export const settings = new SettingsService() export const settingsService = new SettingsService()

View File

@ -3,16 +3,12 @@
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom' import '@testing-library/jest-dom'
import { vi } from 'vitest'
afterEach(() => { afterEach(() => {
vi.restoreAllMocks() jest.restoreAllMocks()
}) })
vi.mock('trystero') jest.mock('secure-file-transfer', () => ({
vi.mock('trystero/torrent')
vi.mock('secure-file-transfer', () => ({
__esModule: true, __esModule: true,
FileTransfer: class FileTransfer { FileTransfer: class FileTransfer {
rescindAll() {} rescindAll() {}

View File

@ -1,11 +1,10 @@
import { vi } from 'vitest' import { encryptionService } from 'services/Encryption'
import { encryption } from 'services/Encryption'
export const mockEncryptionService = encryption export const mockEncryptionService = encryptionService
mockEncryptionService.generateKeyPair = vi.fn(async () => ({ mockEncryptionService.generateKeyPair = jest.fn(async () => ({
publicKey: encryption.cryptoKeyStub, publicKey: encryptionService.cryptoKeyStub,
privateKey: encryption.cryptoKeyStub, privateKey: encryptionService.cryptoKeyStub,
})) }))
mockEncryptionService.encodePassword = async () => '' mockEncryptionService.encodePassword = async () => ''
@ -13,4 +12,4 @@ mockEncryptionService.encodePassword = async () => ''
mockEncryptionService.stringifyCryptoKey = async () => '' mockEncryptionService.stringifyCryptoKey = async () => ''
mockEncryptionService.parseCryptoKeyString = async () => mockEncryptionService.parseCryptoKeyString = async () =>
encryption.cryptoKeyStub encryptionService.cryptoKeyStub

View File

@ -1,13 +1,16 @@
import { UserSettings } from 'models/settings' import { UserSettings } from 'models/settings'
import { encryption } from 'services/Encryption' import { encryptionService } from 'services/Encryption'
import { serialization, SerializedUserSettings } from 'services/Serialization' import {
serializationService,
SerializedUserSettings,
} from 'services/Serialization'
export const mockSerializedPublicKey = 'public key' export const mockSerializedPublicKey = 'public key'
export const mockSerializedPrivateKey = 'private key' export const mockSerializedPrivateKey = 'private key'
export const mockSerialization = serialization export const mockSerializationService = serializationService
mockSerialization.serializeUserSettings = async ( mockSerializationService.serializeUserSettings = async (
userSettings: UserSettings userSettings: UserSettings
) => { ) => {
const { publicKey, privateKey, ...userSettingsRest } = userSettings const { publicKey, privateKey, ...userSettingsRest } = userSettings
@ -19,14 +22,14 @@ mockSerialization.serializeUserSettings = async (
} }
} }
mockSerialization.deserializeUserSettings = async ( mockSerializationService.deserializeUserSettings = async (
serializedUserSettings: SerializedUserSettings serializedUserSettings: SerializedUserSettings
) => { ) => {
const { publicKey, privateKey, ...userSettingsRest } = serializedUserSettings const { publicKey, privateKey, ...userSettingsRest } = serializedUserSettings
return { return {
publicKey: encryption.cryptoKeyStub, publicKey: encryptionService.cryptoKeyStub,
privateKey: encryption.cryptoKeyStub, privateKey: encryptionService.cryptoKeyStub,
...userSettingsRest, ...userSettingsRest,
} }
} }

View File

@ -1,6 +1,6 @@
import { SettingsContextProps } from 'contexts/SettingsContext' import { SettingsContextProps } from 'contexts/SettingsContext'
import { ColorMode, UserSettings } from 'models/settings' import { ColorMode, UserSettings } from 'models/settings'
import { encryption } from 'services/Encryption' import { encryptionService } from 'services/Encryption'
export const userSettingsContextStubFactory = ( export const userSettingsContextStubFactory = (
userSettingsOverrides: Partial<UserSettings> = {} userSettingsOverrides: Partial<UserSettings> = {}
@ -14,8 +14,8 @@ export const userSettingsContextStubFactory = (
playSoundOnNewMessage: true, playSoundOnNewMessage: true,
showNotificationOnNewMessage: true, showNotificationOnNewMessage: true,
showActiveTypingStatus: true, showActiveTypingStatus: true,
publicKey: encryption.cryptoKeyStub, publicKey: encryptionService.cryptoKeyStub,
privateKey: encryption.cryptoKeyStub, privateKey: encryptionService.cryptoKeyStub,
...userSettingsOverrides, ...userSettingsOverrides,
}), }),
} }

View File

@ -1,5 +1,5 @@
import { ColorMode, UserSettings } from 'models/settings' import { ColorMode, UserSettings } from 'models/settings'
import { encryption } from 'services/Encryption' import { encryptionService } from 'services/Encryption'
export const userSettingsStubFactory = ( export const userSettingsStubFactory = (
overrides: Partial<UserSettings> = {} overrides: Partial<UserSettings> = {}
@ -11,8 +11,8 @@ export const userSettingsStubFactory = (
playSoundOnNewMessage: true, playSoundOnNewMessage: true,
showNotificationOnNewMessage: true, showNotificationOnNewMessage: true,
showActiveTypingStatus: true, showActiveTypingStatus: true,
publicKey: encryption.cryptoKeyStub, publicKey: encryptionService.cryptoKeyStub,
privateKey: encryption.cryptoKeyStub, privateKey: encryptionService.cryptoKeyStub,
...overrides, ...overrides,
} }
} }

View File

@ -1,3 +1,8 @@
export const sleep = (milliseconds: number): Promise<void> =>
new Promise<void>(res => {
setTimeout(res, milliseconds)
})
export const isRecord = (variable: any): variable is Record<string, any> => { export const isRecord = (variable: any): variable is Record<string, any> => {
return ( return (
typeof variable === 'object' && typeof variable === 'object' &&

Some files were not shown because too many files have changed in this diff Show More