mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-06 18:03:48 +00:00
Compare commits
68 Commits
v1.10.9
...
sw_digeste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50539ed485 | ||
|
|
490bad5734 | ||
|
|
1d5f2e8023 | ||
|
|
b261cb73ba | ||
|
|
f9b8b0fadf | ||
|
|
db8d5f97ea | ||
|
|
3b31472c52 | ||
|
|
f7ea519106 | ||
|
|
76c47c9623 | ||
|
|
fa86212139 | ||
|
|
331c61fec8 | ||
|
|
fa24e77d3b | ||
|
|
6ca039910a | ||
|
|
9f02f7b3ca | ||
|
|
04d65da779 | ||
|
|
e6f2c776dc | ||
|
|
51299bcf73 | ||
|
|
5f6d330386 | ||
|
|
f103f37e16 | ||
|
|
4b99ff46da | ||
|
|
be381ea438 | ||
|
|
9b3571feac | ||
|
|
8a56a271bc | ||
|
|
07e46e472e | ||
|
|
3b772d0619 | ||
|
|
d70f9d762e | ||
|
|
8592499d22 | ||
|
|
e29ea44025 | ||
|
|
00f1a20177 | ||
|
|
3c8848d406 | ||
|
|
0d17ada58b | ||
|
|
74bd7dd406 | ||
|
|
f4a947527d | ||
|
|
90f10910aa | ||
|
|
aacf24c31f | ||
|
|
c0e5b66d41 | ||
|
|
42bd71a3dc | ||
|
|
a98499ea5a | ||
|
|
7c471910ef | ||
|
|
da558ddceb | ||
|
|
1df8fe258e | ||
|
|
65936a4d7d | ||
|
|
7c6062e1e0 | ||
|
|
902b5c6b8f | ||
|
|
19d33e11d8 | ||
|
|
d8908e01ea | ||
|
|
2d2cfec5f0 | ||
|
|
40a12b5501 | ||
|
|
5ee8bb871e | ||
|
|
ef3c338dad | ||
|
|
6d95f3f4e2 | ||
|
|
c33d49702e | ||
|
|
1d62a9ff49 | ||
|
|
3dd40e238a | ||
|
|
417d5421a6 | ||
|
|
7af51bbd5f | ||
|
|
88739107e4 | ||
|
|
6de97e7ff1 | ||
|
|
b61de4eb87 | ||
|
|
cfe5b4afda | ||
|
|
91fc2b7bf5 | ||
|
|
c670b39732 | ||
|
|
e5a09b6be1 | ||
|
|
1d81b744ea | ||
|
|
c37412cfd3 | ||
|
|
a5dc8b6da2 | ||
|
|
d81c03a560 | ||
|
|
f22abca783 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
fqdn.env
|
||||
/docker/certs
|
||||
/dev/certs
|
||||
qrcode-svg/
|
||||
turnserver.conf
|
||||
rtc_config.json
|
||||
|
||||
3
dev/nginx-with-openssl.Dockerfile
Normal file
3
dev/nginx-with-openssl.Dockerfile
Normal file
@@ -0,0 +1,3 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
41
dev/nginx/default.conf
Normal file
41
dev/nginx/default.conf
Normal file
@@ -0,0 +1,41 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
expires epoch;
|
||||
|
||||
location / {
|
||||
proxy_connect_timeout 300;
|
||||
proxy_pass http://pairdrop:3000;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
}
|
||||
|
||||
location /ca.crt {
|
||||
alias /etc/ssl/certs/pairdropCA.crt;
|
||||
}
|
||||
|
||||
# To allow POST on static pages
|
||||
error_page 405 =200 $uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
ssl_certificate /etc/ssl/certs/pairdrop-dev.crt;
|
||||
ssl_certificate_key /etc/ssl/certs/pairdrop-dev.key;
|
||||
|
||||
expires epoch;
|
||||
|
||||
location / {
|
||||
proxy_connect_timeout 300;
|
||||
proxy_pass http://pairdrop:3000;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
}
|
||||
|
||||
location /ca.crt {
|
||||
alias /etc/ssl/certs/pairdropCA.crt;
|
||||
}
|
||||
# To allow POST on static pages
|
||||
error_page 405 =200 $uri;
|
||||
}
|
||||
|
||||
9
dev/openssl/create.sh
Normal file
9
dev/openssl/create.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
cnf_dir='/mnt/openssl/'
|
||||
certs_dir='/etc/ssl/certs/'
|
||||
openssl req -config ${cnf_dir}pairdropCA.cnf -new -x509 -days 1 -keyout ${certs_dir}pairdropCA.key -out ${certs_dir}pairdropCA.crt
|
||||
openssl req -config ${cnf_dir}pairdropCert.cnf -new -out /tmp/pairdrop-dev.csr -keyout ${certs_dir}pairdrop-dev.key
|
||||
openssl x509 -req -in /tmp/pairdrop-dev.csr -CA ${certs_dir}pairdropCA.crt -CAkey ${certs_dir}pairdropCA.key -CAcreateserial -extensions req_ext -extfile ${cnf_dir}pairdropCert.cnf -sha512 -days 1 -out ${certs_dir}pairdrop-dev.crt
|
||||
|
||||
exec "$@"
|
||||
26
dev/openssl/pairdropCA.cnf
Normal file
26
dev/openssl/pairdropCA.cnf
Normal file
@@ -0,0 +1,26 @@
|
||||
[ req ]
|
||||
default_bits = 2048
|
||||
default_md = sha256
|
||||
default_days = 1
|
||||
encrypt_key = no
|
||||
distinguished_name = subject
|
||||
x509_extensions = x509_ext
|
||||
string_mask = utf8only
|
||||
prompt = no
|
||||
|
||||
[ subject ]
|
||||
organizationName = PairDrop
|
||||
OU = CA
|
||||
commonName = pairdrop-CA
|
||||
|
||||
[ x509_ext ]
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer
|
||||
|
||||
# You only need digitalSignature below. *If* you don't allow
|
||||
# RSA Key transport (i.e., you use ephemeral cipher suites), then
|
||||
# omit keyEncipherment because that's key transport.
|
||||
|
||||
basicConstraints = critical, CA:TRUE, pathlen:0
|
||||
keyUsage = critical, digitalSignature, keyEncipherment, cRLSign, keyCertSign
|
||||
|
||||
29
dev/openssl/pairdropCert.cnf
Normal file
29
dev/openssl/pairdropCert.cnf
Normal file
@@ -0,0 +1,29 @@
|
||||
[ req ]
|
||||
default_bits = 2048
|
||||
default_md = sha256
|
||||
default_days = 1
|
||||
encrypt_key = no
|
||||
distinguished_name = subject
|
||||
req_extensions = req_ext
|
||||
string_mask = utf8only
|
||||
prompt = no
|
||||
|
||||
[ subject ]
|
||||
organizationName = PairDrop
|
||||
OU = Development
|
||||
|
||||
# Use a friendly name here because it's presented to the user. The server's DNS
|
||||
# names are placed in Subject Alternate Names. Plus, DNS names here is deprecated
|
||||
# by both IETF and CA/Browser Forums. If you place a DNS name here, then you
|
||||
# must include the DNS name in the SAN too (otherwise, Chrome and others that
|
||||
# strictly follow the CA/Browser Baseline Requirements will fail).
|
||||
|
||||
commonName = ${ENV::FQDN}
|
||||
|
||||
[ req_ext ]
|
||||
subjectKeyIdentifier = hash
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = digitalSignature, keyEncipherment
|
||||
subjectAltName = DNS:${ENV::FQDN}
|
||||
nsComment = "OpenSSL Generated Certificate"
|
||||
extendedKeyUsage = serverAuth
|
||||
34
docker-compose-dev.yml
Normal file
34
docker-compose-dev.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
version: "3"
|
||||
services:
|
||||
pairdrop:
|
||||
build: .
|
||||
container_name: pairdrop
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PUID=1000 # UID to run the application as
|
||||
- PGID=1000 # GID to run the application as
|
||||
- WS_FALLBACK=false # Set to true to enable websocket fallback if the peer to peer WebRTC connection is not available to the client.
|
||||
- RATE_LIMIT=false # Set to true to limit clients to 1000 requests per 5 min.
|
||||
- RTC_CONFIG=false # Set to the path of a file that specifies the STUN/TURN servers.
|
||||
- DEBUG_MODE=false # Set to true to debug container and peer connections.
|
||||
- TZ=Etc/UTC # Time Zone
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000" # Web UI. Change the port number before the last colon e.g. `127.0.0.1:9000:3000`
|
||||
nginx:
|
||||
build:
|
||||
context: dev/
|
||||
dockerfile: nginx-with-openssl.Dockerfile
|
||||
image: "nginx-with-openssl"
|
||||
volumes:
|
||||
- ./public:/usr/share/nginx/html
|
||||
- ./dev/certs:/etc/ssl/certs
|
||||
- ./dev/openssl:/mnt/openssl
|
||||
- ./dev/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
||||
ports:
|
||||
- "8080:80"
|
||||
- "8443:443"
|
||||
environment:
|
||||
- FQDN=localhost
|
||||
entrypoint: /mnt/openssl/create.sh
|
||||
command: ["nginx", "-g", "daemon off;"]
|
||||
restart: unless-stopped
|
||||
@@ -652,68 +652,74 @@ To run PairDrop including its own coturn-server you need to punch holes in the f
|
||||
|
||||
<br>
|
||||
|
||||
### Firewall
|
||||
To run PairDrop including its own coturn-server you need to punch holes in the firewall. These ports must be opened additionally:
|
||||
- 3478 tcp/udp
|
||||
- 5349 tcp/udp
|
||||
- 10000:20000 tcp/udp
|
||||
|
||||
<br>
|
||||
|
||||
## Local Development
|
||||
|
||||
### Install
|
||||
|
||||
All files needed for developing are available on the branch `dev`.
|
||||
All files needed for developing are available in the folder `./dev`.
|
||||
|
||||
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
|
||||
For convenience, there is also a docker compose file for developing:
|
||||
|
||||
Then, clone the repository and run docker-compose:
|
||||
#### Developing with docker compose
|
||||
First, [Install docker with docker compose.](https://docs.docker.com/compose/install/)
|
||||
|
||||
Then, clone the repository and run docker compose:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/schlagmichdoch/PairDrop.git && cd PairDrop
|
||||
```
|
||||
```bash
|
||||
git checkout dev
|
||||
```
|
||||
```bash
|
||||
docker compose -f docker-compose-dev.yml up -d
|
||||
docker compose -f docker-compose-dev.yml up --no-deps --build
|
||||
```
|
||||
|
||||
Now point your web browser to `http://localhost:8080`.
|
||||
|
||||
- To restart the containers, run `docker compose restart`.
|
||||
- To stop the containers, run `docker compose stop`.
|
||||
- To debug the Node.js server, run `docker logs pairdrop`.
|
||||
|
||||
- After changes to the code you have to rerun the `docker compose` command
|
||||
|
||||
<br>
|
||||
|
||||
### Testing PWA related features
|
||||
#### Testing PWA related features
|
||||
|
||||
PWAs requires the app to be served under a correctly set up and trusted TLS endpoint.
|
||||
|
||||
The NGINX container creates a CA certificate and a website certificate for you.
|
||||
To correctly set the common name of the certificate,
|
||||
you need to change the FQDN environment variable in `docker/fqdn.env`
|
||||
to the fully qualified domain name of your workstation.
|
||||
you need to change the FQDN environment variable in `docker-compose-dev.yml`
|
||||
to the fully qualified domain name of your workstation. (Default: localhost)
|
||||
|
||||
If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \
|
||||
For your convenience, you can download the crt file from `http://<Your FQDN>:8080/ca.crt`. \
|
||||
Install that certificate to the trust store of your operating system. \
|
||||
|
||||
- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
|
||||
- On macOS, double-click the installed CA certificate in `Keychain Access`,
|
||||
##### Windows
|
||||
- Make sure to install it to the `Trusted Root Certification Authorities` store.
|
||||
|
||||
##### macOS
|
||||
- Double-click the installed CA certificate in `Keychain Access`,
|
||||
- expand `Trust`, and select `Always Trust` for SSL.
|
||||
- Firefox uses its own trust store. To install the CA,
|
||||
- point Firefox at `http://<Your FQDN>:8080/ca.crt`.
|
||||
|
||||
##### Firefox
|
||||
Firefox uses its own trust store. To install the CA:
|
||||
- point Firefox at `http://<Your FQDN>:8080/ca.crt` (Default: `http://localhost:8080/ca.crt`)
|
||||
- When prompted, select `Trust this CA to identify websites` and click _OK_.
|
||||
|
||||
Alternatively:
|
||||
1. Download `ca.crt` from `http://<Your FQDN>:8080/ca.crt` (Default: `http://localhost:8080/ca.crt`)
|
||||
2. Go to `about:preferences#privacy` scroll down to `Security` and `Certificates` and click `View Certificates`
|
||||
3. Import the downloaded certificate file (step 1)
|
||||
|
||||
##### Chrome
|
||||
- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`).
|
||||
- Additionally, after installing a new cert, you need to clear the Storage (DevTools → Application → Clear storage → Clear site data).
|
||||
|
||||
##### Google Chrome
|
||||
- To skip the installation of the certificate, you can also open `chrome://flags/#unsafely-treat-insecure-origin-as-secure`
|
||||
- The feature `Insecure origins treated as secure` must be enabled and the list must include your PairDrop test instance. E.g.: `http://127.0.0.1:3000,https://127.0.0.1:8443`
|
||||
|
||||
Please note that the certificates (CA and webserver cert) expire after a day.
|
||||
Also, whenever you restart the NGINX Docker container new certificates are created.
|
||||
|
||||
The site is served on `https://<Your FQDN>:8443`.
|
||||
The site is served on `https://<Your FQDN>:8443` (Default: `https://localhost:8443`).
|
||||
|
||||
[< Back](/README.md)
|
||||
|
||||
@@ -477,7 +477,7 @@
|
||||
<div class="center file-preview"></div>
|
||||
<div class="row-reverse center btn-row wrap">
|
||||
<button id="share-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.share" data-i18n-attrs="text" hidden></button>
|
||||
<button id="download-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus disabled></button>
|
||||
<button id="download-btn" class="btn btn-rounded btn-grey" data-i18n-key="dialogs.download" data-i18n-attrs="text" autofocus disabled hidden></button>
|
||||
<button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
|
||||
</div>
|
||||
</x-paper>
|
||||
@@ -774,7 +774,7 @@
|
||||
<script src="scripts/ui-main.js" defer></script>
|
||||
<script src="scripts/main.js" defer></script>
|
||||
<!-- Sounds -->
|
||||
<audio id="blop" autobuffer="true">
|
||||
<audio id="blop" preload="metadata" disableremoteplayback="true" x-webkit-airplay="deny" muted>
|
||||
<source src="sounds/blop.mp3" type="audio/mpeg">
|
||||
<source src="sounds/blop.ogg" type="audio/ogg">
|
||||
</audio>
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"pairing-cleared": "جميع الأجهزة غير مقترنة",
|
||||
"notifications-enabled": "تم تمكين الإشعارات",
|
||||
"online-requirement-pairing": "يجب أن تكون متصلاً بالإنترنت لإقران الأجهزة",
|
||||
"ios-memory-limit": "لا يمكن إرسال ملفات إلى iOS إلا بحجم يصل إلى 200 ميجابايت مرة واحدة",
|
||||
"online-requirement-public-room": "يجب أن تكون متصلاً بالإنترنت لإنشاء غرفة عامة",
|
||||
"copied-text-error": "فشلت الكتابة من الحافظة. انسخ يدويًا!",
|
||||
"download-successful": "تم تحميل {{descriptor}}",
|
||||
|
||||
@@ -145,7 +145,6 @@
|
||||
"pairing-cleared": "Tots els dispositius desvinculats",
|
||||
"notifications-enabled": "Notificacions habilitades",
|
||||
"online-requirement-pairing": "Has d'estar en línia per vincular dispositius",
|
||||
"ios-memory-limit": "Tan sols és possible enviar fitxers de fins a 200 MB a iOS",
|
||||
"online-requirement-public-room": "Cal que estiguis en línia per poder crear una sala pública",
|
||||
"room-url-copied-to-clipboard": "Enllaç a la sala pública copiat al porta-retalls",
|
||||
"copied-text-error": "L'escriptura al porta-retalls ha fallat. Copiar manualment!",
|
||||
|
||||
@@ -138,7 +138,6 @@
|
||||
"message-transfer-completed": "Nachricht übertragen",
|
||||
"rate-limit-join-key": "Rate Limit erreicht. Warte 10 Sekunden und versuche es erneut.",
|
||||
"selected-peer-left": "Ausgewählter Peer ist gegangen",
|
||||
"ios-memory-limit": "Für Übertragungen an iOS Geräte beträgt die maximale Dateigröße 200 MB",
|
||||
"public-room-left": "Öffentlichen Raum {{publicRoomId}} verlassen",
|
||||
"copied-to-clipboard-error": "Konnte nicht kopieren. Kopiere manuell.",
|
||||
"public-room-id-invalid": "Ungültige Raum-ID",
|
||||
|
||||
@@ -158,11 +158,13 @@
|
||||
"connecting": "Connecting…",
|
||||
"files-incorrect": "Files are incorrect",
|
||||
"file-transfer-completed": "File transfer completed",
|
||||
"ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once",
|
||||
"message-transfer-completed": "Message transfer completed",
|
||||
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?",
|
||||
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
|
||||
"selected-peer-left": "Selected peer left"
|
||||
"selected-peer-left": "Selected peer left",
|
||||
"error-sharing-size": "Files too big to be shared. They can be downloaded instead.",
|
||||
"error-sharing-default": "Error while sharing. It can be downloaded instead.",
|
||||
"ram-exceed-ios": "One of the files is bigger than 250 MB and will crash the page on iOS. Use https and do not use private tabs on the iOS device to prevent this."
|
||||
},
|
||||
"document-titles": {
|
||||
"file-received": "File Received",
|
||||
@@ -176,9 +178,14 @@
|
||||
"click-to-send-share-mode": "Click to send {{descriptor}}",
|
||||
"click-to-send": "Click to send files or right click to send a message",
|
||||
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
|
||||
"connecting": "Connecting…",
|
||||
"preparing": "Preparing…",
|
||||
"waiting": "Waiting…",
|
||||
"processing": "Processing…",
|
||||
"transferring": "Transferring…"
|
||||
"transferring": "Sending…",
|
||||
"receiving": "Receiving…",
|
||||
"transfer-complete": "Sent",
|
||||
"receive-complete": "Received",
|
||||
"error": "Error"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
"pairing-cleared": "Todos los dispositivos han sido desemparejados",
|
||||
"notifications-enabled": "Notificaciones habilitadas",
|
||||
"online-requirement-pairing": "Debes estar en línea para emparejar dispositivos",
|
||||
"ios-memory-limit": "Enviar archivos a iOS sólo admite hasta 200 MB a la vez",
|
||||
"online-requirement-public-room": "Debes estar en línea para crear una sala pública",
|
||||
"copied-text-error": "Error al escribir en el portapapeles. ¡Cópielo manualmente!",
|
||||
"download-successful": "{{descriptor}} descargado",
|
||||
|
||||
@@ -156,7 +156,6 @@
|
||||
"connecting": "Connexion…",
|
||||
"files-incorrect": "Les fichiers sont incorrects",
|
||||
"file-transfer-completed": "Transfert de fichier terminé",
|
||||
"ios-memory-limit": "L'envoi de fichiers vers iOS n'est possible que jusqu'à 200 Mo à la fois",
|
||||
"message-transfer-completed": "Transfert de message terminé",
|
||||
"unfinished-transfers-warning": "Il y a des transferts inachevés. Êtes-vous sûr de vouloir fermer PairDrop ?",
|
||||
"rate-limit-join-key": "Limite de débit atteinte. Attendez 10 secondes et réessayez.",
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"pairing-cleared": "Semua Perangkat dilepaskan",
|
||||
"notifications-enabled": "Notifikasi diaktifkan",
|
||||
"online-requirement-pairing": "Anda harus online untuk memasangkan perangkat",
|
||||
"ios-memory-limit": "Mengirim file ke iOS hanya dapat dilakukan hingga 200 MB sekaligus",
|
||||
"online-requirement-public-room": "Anda harus online untuk membuat ruang publik",
|
||||
"copied-text-error": "Menyalin ke papan klip gagal. Salinlah secara manual!",
|
||||
"download-successful": "{{descriptor}} diunduh",
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
"pairing-cleared": "Tutti i dispositivi sono stati dissociati",
|
||||
"notifications-enabled": "Notifiche attivate",
|
||||
"online-requirement-pairing": "Devi essere online per associare dispositivi",
|
||||
"ios-memory-limit": "L'invio di file a dispositivi iOS è possibile solo 200 MB alla volta",
|
||||
"online-requirement-public-room": "Devi essere online per creare una stanza pubblica",
|
||||
"copied-text-error": "Scrittura negli appunti fallita. Copia manualmente!",
|
||||
"download-successful": "{{descriptor}} scaricato",
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"pairing-cleared": "全てのデバイスとのペアリングを解除しました",
|
||||
"notifications-enabled": "通知が有効になりました",
|
||||
"online-requirement-pairing": "ペアリングするにはオンラインである必要があります",
|
||||
"ios-memory-limit": "iOSへのファイル送信は一度に200MBまでしかできません",
|
||||
"online-requirement-public-room": "公開ルームを作成するにはオンラインである必要があります",
|
||||
"copied-text-error": "クリップボードにコピーできませんでした。手動でコピーしてください。",
|
||||
"download-successful": "{{descriptor}}をダウンロードしました",
|
||||
|
||||
@@ -128,7 +128,6 @@
|
||||
"pairing-cleared": "ಎಲ್ಲಾ ಸಾಧನಗಳನ್ನು ಜೋಡಿಯಾಗಿ ತೆಗೆಯಲಾಗಿದೆ",
|
||||
"notifications-enabled": "ಸೂಚನೆಗಳನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ",
|
||||
"online-requirement-pairing": "ಸಾಧನಗಳನ್ನು ಜೋಡಿಸಲು ನೀವು ಆನ್ಲೈನ್ ಇರಬೇಕು",
|
||||
"ios-memory-limit": "iOSಗೆ ಫೈಲ್ಗಳನ್ನು ಕಳುಹಿಸುವುದು ಒಂದೇ ಬಾರಿಗೆ 200 MB ವರೆಗೆ ಮಾತ್ರ ಸಾಧ್ಯವಾಗಿದೆ",
|
||||
"online-requirement-public-room": "ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯನ್ನು ರಚಿಸಲು ನೀವು ಆನ್ಲೈನ್ ಇರಬೇಕು",
|
||||
"room-url-copied-to-clipboard": "ಸಾರ್ವಜನಿಕ ಕೊಠಡಿಯ ಲಿಂಕ್ ಅನ್ನು ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ನಕಲಿಸಲಾಗಿದೆ",
|
||||
"copied-text-error": "ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ಬರೆಯುವುದು ವಿಫಲವಾಗಿದೆ. ಕೈಯಾರೆ ನಕಲಿಸಿ!",
|
||||
|
||||
@@ -116,7 +116,6 @@
|
||||
"request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}",
|
||||
"message-received": "Melding mottatt av {{name}} - Klikk for å åpne",
|
||||
"files-incorrect": "Filene er uriktige",
|
||||
"ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen",
|
||||
"unfinished-transfers-warning": "Lukk med ufullførte overføringer?",
|
||||
"rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen."
|
||||
},
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"pairing-cleared": "Alle apparaten ontkoppeld",
|
||||
"notifications-enabled": "Meldingen geactiveerd",
|
||||
"online-requirement-pairing": "U moet online zijn om apparaten te koppelen",
|
||||
"ios-memory-limit": "Bestandsoverdrachten naar iOS kunnen slechts met 200 MB per keer",
|
||||
"online-requirement-public-room": "U moet online zijn om een openbare kamer te maken",
|
||||
"copied-text-error": "Schrijven naar klembord mislukt. Kopieer handmatig!",
|
||||
"download-successful": "{{descriptor}} downloaden",
|
||||
|
||||
@@ -153,7 +153,6 @@
|
||||
"connecting": "Conectando…",
|
||||
"files-incorrect": "Os arquivos estão incorretos",
|
||||
"file-transfer-completed": "Transferência de arquivo concluída",
|
||||
"ios-memory-limit": "Enviar arquivos para iOS só é possível até 200 MB de uma vez",
|
||||
"message-transfer-completed": "Transferência de mensagem concluída",
|
||||
"unfinished-transfers-warning": "Há transferências inacabadas. Tem certeza de que deseja fechar o PairDrop?",
|
||||
"rate-limit-join-key": "Limite de taxa atingido. Aguarde 10 segundos e tente novamente.",
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"pairing-cleared": "Toate dispozitivele sunt decuplate",
|
||||
"notifications-enabled": "Notificări activate",
|
||||
"online-requirement-pairing": "Trebuie să fiți online pentru a asocia dispozitivele",
|
||||
"ios-memory-limit": "Trimiterea de fișiere pe iOS este posibilă doar până la 200 MB simultan",
|
||||
"online-requirement-public-room": "Trebuie să fiți online pentru a crea o cameră publică",
|
||||
"copied-text-error": "Scrierea în clipboard a eșuat. Copiați manual!",
|
||||
"download-successful": "{{descriptor}} descărcat",
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
"online-requirement": "Для сопряжения устройств вам нужно быть в сети.",
|
||||
"files-incorrect": "Файлы неверны",
|
||||
"message-transfer-completed": "Передача сообщения завершена",
|
||||
"ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз",
|
||||
"selected-peer-left": "Выбранный узел вышел",
|
||||
"request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}",
|
||||
"rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.",
|
||||
|
||||
@@ -154,7 +154,6 @@
|
||||
"files-incorrect": "文件不正确",
|
||||
"file-transfer-completed": "文件传输已完成",
|
||||
"connecting": "连接中…",
|
||||
"ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB",
|
||||
"rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。",
|
||||
"public-room-left": "已退出公共房间 {{publicRoomId}}",
|
||||
"copied-to-clipboard-error": "无法复制。请手动复制。",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"background_color": "#efefef",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "minimal-ui",
|
||||
"display": "standalone",
|
||||
"theme_color": "#3367d6",
|
||||
"screenshots" : [
|
||||
{
|
||||
|
||||
@@ -2,18 +2,27 @@ class BrowserTabsConnector {
|
||||
constructor() {
|
||||
this.bc = new BroadcastChannel('pairdrop');
|
||||
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
|
||||
Events.on('broadcast-send', e => this._broadcastSend(e.detail.type, e.detail.data));
|
||||
Events.on('broadcast-self-display-name-changed', e => this._onBroadcastSelfDisplayNameChanged(e.detail.displayName));
|
||||
}
|
||||
|
||||
_broadcastSend(message) {
|
||||
this.bc.postMessage(message);
|
||||
_broadcastSend(type, data) {
|
||||
this.bc.postMessage({ type, data });
|
||||
}
|
||||
|
||||
_onBroadcastSelfDisplayNameChanged(displayName) {
|
||||
this._broadcastSend('self-display-name-changed', { displayName: displayName });
|
||||
}
|
||||
|
||||
_onMessage(e) {
|
||||
console.log('Broadcast:', e.data)
|
||||
switch (e.data.type) {
|
||||
const type = e.data.type;
|
||||
const data = e.data.data;
|
||||
|
||||
Logger.debug('Broadcast:', type, data);
|
||||
|
||||
switch (type) {
|
||||
case 'self-display-name-changed':
|
||||
Events.fire('self-display-name-changed', e.data.detail);
|
||||
Events.fire('self-display-name-changed', data.displayName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +34,11 @@ class BrowserTabsConnector {
|
||||
: false;
|
||||
}
|
||||
|
||||
static isOnlyTab() {
|
||||
let peerIdsBrowser = JSON.parse(localStorage.getItem('peer_ids_browser'));
|
||||
return peerIdsBrowser.length <= 1;
|
||||
}
|
||||
|
||||
static async addPeerIdToLocalStorage() {
|
||||
const peerId = sessionStorage.getItem('peer_id');
|
||||
if (!peerId) return false;
|
||||
|
||||
@@ -2,28 +2,28 @@ class Localization {
|
||||
constructor() {
|
||||
Localization.$htmlRoot = document.querySelector('html');
|
||||
|
||||
Localization.defaultLocale = "en";
|
||||
Localization.supportedLocales = ["ar", "be", "ca", "da", "de", "en", "es", "fr", "he", "hu", "id", "it", "ja", "kn", "nb", "nl", "pl", "pt-BR", "ro", "ru", "tr", "zh-CN", "zh-TW"];
|
||||
Localization.supportedLocalesRtl = ["ar", "he"];
|
||||
Localization.localeDefault = "en";
|
||||
Localization.localesSupported = ["ar", "be", "ca", "da", "de", "en", "es", "fr", "he", "hu", "id", "it", "ja", "kn", "nb", "nl", "pl", "pt-BR", "ro", "ru", "tr", "zh-CN", "zh-TW"];
|
||||
Localization.localesRtl = ["ar", "he"];
|
||||
|
||||
Localization.translations = {};
|
||||
Localization.translationsDefaultLocale = {};
|
||||
|
||||
Localization.systemLocale = Localization.getSupportedOrDefaultLocales(navigator.languages);
|
||||
Localization.localeSystem = Localization.getSupportedOrDefaultLocales(navigator.languages);
|
||||
|
||||
let storedLanguageCode = localStorage.getItem('language_code');
|
||||
|
||||
Localization.initialLocale = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode)
|
||||
Localization.localeInitial = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode)
|
||||
? storedLanguageCode
|
||||
: Localization.systemLocale;
|
||||
: Localization.localeSystem;
|
||||
}
|
||||
|
||||
static localeIsSupported(locale) {
|
||||
return Localization.supportedLocales.indexOf(locale) > -1;
|
||||
return Localization.localesSupported.indexOf(locale) > -1;
|
||||
}
|
||||
|
||||
static localeIsRtl(locale) {
|
||||
return Localization.supportedLocalesRtl.indexOf(locale) > -1;
|
||||
return Localization.localesRtl.indexOf(locale) > -1;
|
||||
}
|
||||
|
||||
static currentLocaleIsRtl() {
|
||||
@@ -31,7 +31,7 @@ class Localization {
|
||||
}
|
||||
|
||||
static currentLocaleIsDefault() {
|
||||
return Localization.locale === Localization.defaultLocale
|
||||
return Localization.locale === Localization.localeDefault
|
||||
}
|
||||
|
||||
static getSupportedOrDefaultLocales(locales) {
|
||||
@@ -44,15 +44,15 @@ class Localization {
|
||||
// If there is no perfect match for browser locales, try generic locales first before resorting to the default locale
|
||||
return locales.find(Localization.localeIsSupported)
|
||||
|| localesGeneric.find(Localization.localeIsSupported)
|
||||
|| Localization.defaultLocale;
|
||||
|| Localization.localeDefault;
|
||||
}
|
||||
|
||||
async setInitialTranslation() {
|
||||
await Localization.setTranslation(Localization.initialLocale)
|
||||
await Localization.setTranslation(Localization.localeInitial)
|
||||
}
|
||||
|
||||
static async setTranslation(locale) {
|
||||
if (!locale) locale = Localization.systemLocale;
|
||||
if (!locale) locale = Localization.localeSystem;
|
||||
|
||||
await Localization.setLocale(locale)
|
||||
await Localization.translatePage();
|
||||
@@ -67,8 +67,8 @@ class Localization {
|
||||
Localization.$htmlRoot.setAttribute('lang', locale);
|
||||
|
||||
|
||||
console.log("Page successfully translated",
|
||||
`System language: ${Localization.systemLocale}`,
|
||||
Logger.debug("Page successfully translated",
|
||||
`System language: ${Localization.localeSystem}`,
|
||||
`Selected language: ${locale}`
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@ class Localization {
|
||||
static async setLocale(newLocale) {
|
||||
if (newLocale === Localization.locale) return false;
|
||||
|
||||
Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale);
|
||||
Localization.translationsDefaultLocale = await Localization.fetchTranslationsFor(Localization.localeDefault);
|
||||
|
||||
const newTranslations = await Localization.fetchTranslationsFor(newLocale);
|
||||
|
||||
@@ -145,7 +145,7 @@ class Localization {
|
||||
translation = translationObj[lastKey];
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Logger.error(e);
|
||||
}
|
||||
|
||||
if (!translation) {
|
||||
@@ -179,7 +179,7 @@ class Localization {
|
||||
}
|
||||
catch (e) {
|
||||
// Log warnings and help calls
|
||||
console.warn(e);
|
||||
Logger.warn(e);
|
||||
Localization.logTranslationMissingOrBroken(key, attr, data, useDefault);
|
||||
Localization.logHelpCallKey(key, attr);
|
||||
Localization.logHelpCall();
|
||||
@@ -192,7 +192,7 @@ class Localization {
|
||||
else {
|
||||
// Is not default locale yet
|
||||
// Get translation for default language with same arguments
|
||||
console.log(`Using default language ${Localization.defaultLocale.toUpperCase()} instead.`);
|
||||
Logger.debug(`Using default language ${Localization.localeDefault.toUpperCase()} instead.`);
|
||||
translation = this.getTranslation(key, attr, data, true);
|
||||
}
|
||||
}
|
||||
@@ -202,14 +202,14 @@ class Localization {
|
||||
|
||||
static logTranslationMissingOrBroken(key, attr, data, useDefault) {
|
||||
let usedLocale = useDefault
|
||||
? Localization.defaultLocale.toUpperCase()
|
||||
? Localization.localeDefault.toUpperCase()
|
||||
: Localization.locale.toUpperCase();
|
||||
|
||||
console.warn(`Missing or broken translation for language ${usedLocale}.\n`, 'key:', key, 'attr:', attr, 'data:', data);
|
||||
Logger.warn(`Missing or broken translation for language ${usedLocale}.\n`, 'key:', key, 'attr:', attr, 'data:', data);
|
||||
}
|
||||
|
||||
static logHelpCall() {
|
||||
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
|
||||
Logger.warn("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
|
||||
}
|
||||
|
||||
static logHelpCallKey(key, attr) {
|
||||
@@ -219,7 +219,7 @@ class Localization {
|
||||
? key
|
||||
: `${key}_${attr}`;
|
||||
|
||||
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`);
|
||||
Logger.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`);
|
||||
}
|
||||
|
||||
static escapeHTML(unsafeText) {
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
class Logger {
|
||||
|
||||
static debug(message, ...optionalParams) {
|
||||
if (window.debugMode) {
|
||||
console.debug("DEBUG:", message, ...optionalParams);
|
||||
}
|
||||
}
|
||||
static log(message, ...optionalParams) {
|
||||
console.log("LOG:", message, ...optionalParams);
|
||||
}
|
||||
|
||||
static warn(message, ...optionalParams) {
|
||||
console.warn("WARN:", message, ...optionalParams);
|
||||
}
|
||||
|
||||
static error(message, ...optionalParams) {
|
||||
console.error("ERROR:", message, ...optionalParams);
|
||||
}
|
||||
}
|
||||
|
||||
class PairDrop {
|
||||
|
||||
constructor() {
|
||||
@@ -34,14 +54,14 @@ class PairDrop {
|
||||
|
||||
this.initialize()
|
||||
.then(_ => {
|
||||
console.log("Initialization completed.");
|
||||
Logger.log("Initialization completed.");
|
||||
});
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Translate page before fading in
|
||||
await this.localization.setInitialTranslation()
|
||||
console.log("Initial translation successful.");
|
||||
Logger.log("Initial translation successful.");
|
||||
|
||||
// Show "Loading..." until connected to WsServer
|
||||
await this.footerUI.showLoading();
|
||||
@@ -56,16 +76,16 @@ class PairDrop {
|
||||
await this.backgroundCanvas.fadeIn();
|
||||
|
||||
// Load deferred assets
|
||||
console.log("Load deferred assets...");
|
||||
Logger.log("Load deferred assets...");
|
||||
await this.loadDeferredAssets();
|
||||
console.log("Loading of deferred assets completed.");
|
||||
Logger.log("Loading of deferred assets completed.");
|
||||
|
||||
console.log("Hydrate UI...");
|
||||
Logger.log("Hydrate UI...");
|
||||
await this.hydrate();
|
||||
console.log("UI hydrated.");
|
||||
Logger.log("UI hydrated.");
|
||||
|
||||
// Evaluate url params as soon as ws is connected
|
||||
console.log("Evaluate URL params as soon as websocket connection is established.");
|
||||
Logger.log("Evaluate URL params as soon as websocket connection is established.");
|
||||
Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true});
|
||||
}
|
||||
|
||||
@@ -74,14 +94,14 @@ class PairDrop {
|
||||
navigator.serviceWorker
|
||||
.register('service-worker.js')
|
||||
.then(serviceWorker => {
|
||||
console.log('Service Worker registered');
|
||||
Logger.log('Service Worker registered');
|
||||
window.serviceWorker = serviceWorker
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onPwaInstallable(e) {
|
||||
if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
|
||||
if (!window.matchMedia('(display-mode: standalone)').matches) {
|
||||
// only display install btn when not installed
|
||||
this.$headerInstallBtn.removeAttribute('hidden');
|
||||
this.$headerInstallBtn.addEventListener('click', () => {
|
||||
@@ -117,6 +137,7 @@ class PairDrop {
|
||||
let stylesheet = document.createElement('link');
|
||||
stylesheet.rel = 'preload';
|
||||
stylesheet.as = 'style';
|
||||
stylesheet.defer = true;
|
||||
stylesheet.href = url;
|
||||
stylesheet.onload = _ => {
|
||||
stylesheet.onload = null;
|
||||
@@ -133,10 +154,10 @@ class PairDrop {
|
||||
return new Promise( async (resolve) => {
|
||||
try {
|
||||
await this.loadStyleSheet(url);
|
||||
console.log(`Stylesheet loaded successfully: ${url}`);
|
||||
Logger.log(`Stylesheet loaded successfully: ${url}`);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('Error loading stylesheet:', error);
|
||||
Logger.error('Error loading stylesheet:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -156,10 +177,10 @@ class PairDrop {
|
||||
return new Promise( async (resolve) => {
|
||||
try {
|
||||
await this.loadScript(url);
|
||||
console.log(`Script loaded successfully: ${url}`);
|
||||
Logger.log(`Script loaded successfully: ${url}`);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('Error loading script:', error);
|
||||
Logger.error('Error loading script:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -229,12 +250,17 @@ class PairDrop {
|
||||
this.publicRoomDialog._createPublicRoom();
|
||||
}
|
||||
}
|
||||
else if (urlParams.has("debug") && urlParams.get("debug") === "true") {
|
||||
window.debugMode = true;
|
||||
}
|
||||
|
||||
// remove url params from url
|
||||
const urlWithoutParams = getUrlWithoutArguments();
|
||||
window.history.replaceState({}, "Rewrite URL", urlWithoutParams);
|
||||
if (!window.debugMode) {
|
||||
// remove url params from url
|
||||
const urlWithoutParams = getUrlWithoutArguments();
|
||||
window.history.replaceState({}, "Rewrite URL", urlWithoutParams);
|
||||
}
|
||||
|
||||
console.log("URL params evaluated.");
|
||||
Logger.log("URL params evaluated.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,19 +7,18 @@ class PersistentStorage {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 5);
|
||||
DBOpenRequest.onerror = e => {
|
||||
PersistentStorage.logBrowserNotCapable();
|
||||
console.log('Error initializing database: ');
|
||||
console.log(e)
|
||||
Logger.error('Error initializing database:', e);
|
||||
};
|
||||
DBOpenRequest.onsuccess = _ => {
|
||||
console.log('Database initialised.');
|
||||
Logger.debug('Database initialised.');
|
||||
};
|
||||
DBOpenRequest.onupgradeneeded = async e => {
|
||||
const db = e.target.result;
|
||||
const txn = e.target.transaction;
|
||||
|
||||
db.onerror = e => console.log('Error loading database: ' + e);
|
||||
db.onerror = e => Logger.error('Error loading database:', e);
|
||||
|
||||
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
|
||||
Logger.debug(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
|
||||
|
||||
if (e.oldVersion === 0) {
|
||||
// initiate v1
|
||||
@@ -54,7 +53,7 @@ class PersistentStorage {
|
||||
}
|
||||
|
||||
static logBrowserNotCapable() {
|
||||
console.log("This browser does not support IndexedDB. Paired devices will be gone after the browser is closed.");
|
||||
Logger.log("This browser does not support IndexedDB. Paired devices will be gone after the browser is closed.");
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
@@ -66,7 +65,7 @@ class PersistentStorage {
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.put(value, key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Added key-pair: ${key} - ${value}`);
|
||||
Logger.debug(`Request successful. Added key-pair: ${key} - ${value}`);
|
||||
resolve(value);
|
||||
};
|
||||
}
|
||||
@@ -85,7 +84,7 @@ class PersistentStorage {
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.get(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);
|
||||
Logger.debug(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);
|
||||
resolve(objectStoreRequest.result);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +103,7 @@ class PersistentStorage {
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.delete(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted key: ${key}`);
|
||||
Logger.debug(`Request successful. Deleted key: ${key}`);
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
@@ -128,7 +127,7 @@ class PersistentStorage {
|
||||
'auto_accept': false
|
||||
});
|
||||
objectStoreRequest.onsuccess = e => {
|
||||
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
|
||||
Logger.debug(`Request successful. RoomSecret added: ${e.target.result}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
@@ -145,7 +144,7 @@ class PersistentStorage {
|
||||
for (let i = 0; i < roomSecrets.length; i++) {
|
||||
secrets.push(roomSecrets[i].secret);
|
||||
}
|
||||
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
|
||||
Logger.debug(`Request successful. Retrieved ${secrets.length} room_secrets`);
|
||||
return(secrets);
|
||||
} catch (e) {
|
||||
this.logBrowserNotCapable();
|
||||
@@ -182,13 +181,13 @@ class PersistentStorage {
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
const key = e.target.result;
|
||||
if (!key) {
|
||||
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
|
||||
Logger.debug(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const objectStoreRequestRetrieval = objectStore.get(key);
|
||||
objectStoreRequestRetrieval.onsuccess = e => {
|
||||
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
|
||||
Logger.debug(`Request successful. Retrieved entry for room_secret: ${key}`);
|
||||
resolve({
|
||||
"entry": e.target.result,
|
||||
"key": key
|
||||
@@ -215,14 +214,14 @@ class PersistentStorage {
|
||||
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
if (!e.target.result) {
|
||||
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
|
||||
Logger.debug(`Nothing to delete. room_secret not existing: ${roomSecret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const key = e.target.result;
|
||||
const objectStoreRequestDeletion = objectStore.delete(key);
|
||||
objectStoreRequestDeletion.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted room_secret: ${key}`);
|
||||
Logger.debug(`Request successful. Deleted room_secret: ${key}`);
|
||||
resolve(roomSecret);
|
||||
}
|
||||
objectStoreRequestDeletion.onerror = e => {
|
||||
@@ -245,7 +244,7 @@ class PersistentStorage {
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
const objectStoreRequest = objectStore.clear();
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log('Request successful. All room_secrets cleared');
|
||||
Logger.debug('Request successful. All room_secrets cleared');
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
@@ -255,15 +254,15 @@ class PersistentStorage {
|
||||
})
|
||||
}
|
||||
|
||||
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
|
||||
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
|
||||
static updateRoomSecretDisplayName(roomSecret, displayName) {
|
||||
return this.updateRoomSecret(roomSecret, null, displayName, null);
|
||||
}
|
||||
|
||||
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
|
||||
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
|
||||
return this.updateRoomSecret(roomSecret, null, null, null, autoAccept);
|
||||
}
|
||||
|
||||
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
|
||||
static updateRoomSecret(roomSecret, updatedRoomSecret = null, updatedDisplayName = null, updatedDeviceName = null, updatedAutoAccept = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
|
||||
DBOpenRequest.onsuccess = e => {
|
||||
@@ -278,16 +277,16 @@ class PersistentStorage {
|
||||
const objectStore = transaction.objectStore('room_secrets');
|
||||
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
|
||||
const updatedRoomSecretEntry = {
|
||||
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
|
||||
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
|
||||
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
|
||||
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
|
||||
'secret': updatedRoomSecret !== null ? updatedRoomSecret : roomSecretEntry.entry.secret,
|
||||
'display_name': updatedDisplayName !== null ? updatedDisplayName : roomSecretEntry.entry.display_name,
|
||||
'device_name': updatedDeviceName !== null ? updatedDeviceName : roomSecretEntry.entry.device_name,
|
||||
'auto_accept': updatedAutoAccept !== null ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
|
||||
};
|
||||
|
||||
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
|
||||
|
||||
objectStoreRequestUpdate.onsuccess = e => {
|
||||
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
|
||||
Logger.debug(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
|
||||
resolve({
|
||||
"entry": updatedRoomSecretEntry,
|
||||
"key": roomSecretEntry.key
|
||||
|
||||
131
public/scripts/sw-file-digester.js
Normal file
131
public/scripts/sw-file-digester.js
Normal file
@@ -0,0 +1,131 @@
|
||||
self.accessHandle = undefined;
|
||||
self.messageQueue = [];
|
||||
self.busy = false;
|
||||
|
||||
|
||||
self.addEventListener('message', async e => {
|
||||
// Put message into queue if busy
|
||||
if (self.busy) {
|
||||
self.messageQueue.push(e.data);
|
||||
return;
|
||||
}
|
||||
|
||||
await digestMessage(e.data);
|
||||
});
|
||||
|
||||
async function digestMessage(message) {
|
||||
self.busy = true;
|
||||
try {
|
||||
switch (message.type) {
|
||||
case "check-support":
|
||||
await checkSupport();
|
||||
break;
|
||||
case "chunk":
|
||||
await onChunk(message.id, message.chunk, message.offset);
|
||||
break;
|
||||
case "get-file":
|
||||
await onGetFile(message.id);
|
||||
break;
|
||||
case "delete-file":
|
||||
await onDeleteFile(message.id);
|
||||
break;
|
||||
case "clear-directory":
|
||||
await onClearDirectory();
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
self.postMessage({type: "error", error: e});
|
||||
}
|
||||
|
||||
// message is digested. Digest next message.
|
||||
await messageDigested();
|
||||
}
|
||||
|
||||
async function messageDigested() {
|
||||
if (!self.messageQueue.length) {
|
||||
// no chunk in queue -> set flag to false and stop
|
||||
this.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Digest next message in queue
|
||||
await this.digestMessage(self.messageQueue.pop());
|
||||
}
|
||||
|
||||
async function checkSupport() {
|
||||
try {
|
||||
const accessHandle = await getAccessHandle("test");
|
||||
self.postMessage({type: "support", supported: true});
|
||||
accessHandle.close();
|
||||
}
|
||||
catch (e) {
|
||||
self.postMessage({type: "support", supported: false});
|
||||
}
|
||||
}
|
||||
|
||||
async function getFileHandle(id) {
|
||||
const dirHandle = await navigator.storage.getDirectory();
|
||||
return await dirHandle.getFileHandle(id, {create: true});
|
||||
}
|
||||
|
||||
async function getAccessHandle(id) {
|
||||
const fileHandle = await getFileHandle(id);
|
||||
|
||||
if (!self.accessHandle) {
|
||||
// Create FileSystemSyncAccessHandle on the file.
|
||||
self.accessHandle = await fileHandle.createSyncAccessHandle();
|
||||
}
|
||||
|
||||
return self.accessHandle;
|
||||
}
|
||||
|
||||
async function onChunk(id, chunk, offset) {
|
||||
const accessHandle = await getAccessHandle(id);
|
||||
|
||||
// Write the message to the end of the file.
|
||||
let encodedMessage = new DataView(chunk);
|
||||
accessHandle.write(encodedMessage, { at: offset });
|
||||
|
||||
self.postMessage({type: "chunk-written", offset: offset});
|
||||
}
|
||||
|
||||
async function onGetFile(id) {
|
||||
const fileHandle = await getFileHandle(id);
|
||||
let file = await fileHandle.getFile();
|
||||
|
||||
self.postMessage({type: "file", file: file});
|
||||
}
|
||||
|
||||
async function onDeleteFile(id) {
|
||||
const accessHandle = await getAccessHandle(id);
|
||||
|
||||
// Truncate the file to 0 bytes
|
||||
accessHandle.truncate(0);
|
||||
|
||||
// Persist changes to disk.
|
||||
accessHandle.flush();
|
||||
|
||||
// Always close FileSystemSyncAccessHandle if done.
|
||||
accessHandle.close();
|
||||
|
||||
self.postMessage({type: "file-deleted", id: id});
|
||||
}
|
||||
|
||||
async function onClearDirectory() {
|
||||
const dirHandle = await navigator.storage.getDirectory();
|
||||
|
||||
// Iterate through directory entries and truncate all entries to 0
|
||||
for await (const [id, fileHandle] of dirHandle.entries()) {
|
||||
const accessHandle = await fileHandle.createSyncAccessHandle();
|
||||
|
||||
// Truncate the file to 0 bytes
|
||||
accessHandle.truncate(0);
|
||||
|
||||
// Persist changes to disk.
|
||||
accessHandle.flush();
|
||||
|
||||
// Always close FileSystemSyncAccessHandle if done.
|
||||
accessHandle.close();
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ class FooterUI {
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
|
||||
Events.on('display-name', e => this._onDisplayName(e.detail.displayName));
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail.displayName));
|
||||
|
||||
// Load saved display name on page load
|
||||
Events.on('ws-connected', _ => this._loadSavedDisplayName());
|
||||
@@ -238,8 +238,8 @@ class FooterUI {
|
||||
|
||||
if (!displayName) return;
|
||||
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
Events.fire('self-display-name-changed', displayName);
|
||||
Logger.debug("Retrieved edited display name:", displayName)
|
||||
Events.fire('self-display-name-changed', { displayName: displayName });
|
||||
}
|
||||
|
||||
_onDisplayName(displayName){
|
||||
@@ -275,25 +275,25 @@ class FooterUI {
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-permanently"));
|
||||
})
|
||||
.catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
Logger.debug("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('edited_display_name', newDisplayName);
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily"));
|
||||
})
|
||||
.finally(() => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
Events.fire('self-display-name-changed', { displayName: newDisplayName });
|
||||
Events.fire('broadcast-self-display-name-changed', { displayName: newDisplayName });
|
||||
});
|
||||
}
|
||||
else {
|
||||
PersistentStorage.delete('edited_display_name')
|
||||
.catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
Logger.debug("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('edited_display_name');
|
||||
})
|
||||
.finally(() => {
|
||||
Events.fire('notify-user', Localization.getTranslation("notifications.display-name-random-again"));
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
Events.fire('self-display-name-changed', { displayName: '' });
|
||||
Events.fire('broadcast-self-display-name-changed', { displayName: '' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
1231
public/scripts/ui.js
1231
public/scripts/ui.js
File diff suppressed because it is too large
Load Diff
@@ -62,41 +62,67 @@ window.isMobile = window.iOS || window.android;
|
||||
|
||||
|
||||
// Helper functions
|
||||
|
||||
const audioPlayer = (() => {
|
||||
const blop = document.getElementById('blop');
|
||||
blop.addEventListener('ended', _ => {
|
||||
blop.muted = true
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
playBlop() {
|
||||
if (window.isMobile) return;
|
||||
|
||||
blop.muted = false;
|
||||
blop.play();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const zipper = (() => {
|
||||
|
||||
let zipWriter;
|
||||
return {
|
||||
createNewZipWriter() {
|
||||
zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), { bufferedWrite: true, level: 0 });
|
||||
},
|
||||
addFile(file, options) {
|
||||
return zipWriter.add(file.name, new zip.BlobReader(file), options);
|
||||
},
|
||||
async getBlobURL() {
|
||||
if (zipWriter) {
|
||||
const blobURL = URL.createObjectURL(await zipWriter.close());
|
||||
zipWriter = null;
|
||||
return blobURL;
|
||||
async getObjectUrlOfZipFile(files, onZipProgressCallback){
|
||||
try {
|
||||
const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"));
|
||||
|
||||
let bytesProcessed = 0;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
await zipWriter.add(
|
||||
files[i].name,
|
||||
new zip.BlobReader(files[i]),
|
||||
{
|
||||
onprogress: (progress) => onZipProgressCallback(bytesProcessed + progress)
|
||||
}
|
||||
);
|
||||
bytesProcessed += files[i].size;
|
||||
}
|
||||
|
||||
return URL.createObjectURL(await zipWriter.close());
|
||||
}
|
||||
else {
|
||||
throw new Error("Zip file closed");
|
||||
}
|
||||
},
|
||||
async getZipFile(filename = "archive.zip") {
|
||||
if (zipWriter) {
|
||||
const file = new File([await zipWriter.close()], filename, {type: "application/zip"});
|
||||
zipWriter = null;
|
||||
return file;
|
||||
}
|
||||
else {
|
||||
throw new Error("Zip file closed");
|
||||
catch (e) {
|
||||
Logger.error(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async getEntries(file, options) {
|
||||
return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options);
|
||||
try {
|
||||
return await (new zip.ZipReader(new zip.BlobReader(file))).getEntries(options);
|
||||
}
|
||||
catch (e) {
|
||||
Logger.error(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async getData(entry, options) {
|
||||
return await entry.getData(new zip.BlobWriter(), options);
|
||||
try {
|
||||
return await entry.getData(new zip.BlobWriter(), options);
|
||||
}
|
||||
catch (e) {
|
||||
Logger.error(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -521,7 +547,7 @@ function getThumbnailAsDataUrl(file, width = undefined, height = undefined, qual
|
||||
let dataUrl = canvas.toDataURL("image/jpeg", quality);
|
||||
resolve(dataUrl);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Logger.error(e);
|
||||
reject(new Error(`Could not create an image thumbnail from type ${file.type}`));
|
||||
}
|
||||
})
|
||||
@@ -593,4 +619,27 @@ function isUrlValid(url) {
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// polyfill for crypto.randomUUID()
|
||||
// Credits: @Briguy37 - https://stackoverflow.com/a/8809472/14678591
|
||||
function generateUUID() {
|
||||
return crypto && crypto.randomUUID()
|
||||
? crypto.randomUUID()
|
||||
: () => {
|
||||
let
|
||||
d = new Date().getTime(),
|
||||
d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || 0;
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||
let r = Math.random() * 16;
|
||||
if (d > 0) {
|
||||
r = (d + r) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
} else {
|
||||
r = (d2 + r) % 16 | 0;
|
||||
d2 = Math.floor(d2 / 16);
|
||||
}
|
||||
return (c == 'x' ? r : (r & 0x7 | 0x8)).toString(16);
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -104,6 +104,8 @@ x-peer {
|
||||
padding: 8px;
|
||||
align-content: start;
|
||||
flex-wrap: wrap;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
x-peer input[type="file"] {
|
||||
@@ -121,8 +123,6 @@ x-peer label {
|
||||
x-peer x-icon {
|
||||
--icon-size: 40px;
|
||||
margin-bottom: 4px;
|
||||
transition: transform 150ms;
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -148,7 +148,7 @@ x-peer:not(.type-ip):not(.type-secret).type-public-id .icon-wrapper {
|
||||
.highlight-wrapper {
|
||||
align-self: center;
|
||||
align-items: center;
|
||||
margin: 7px auto 0;
|
||||
margin: 10px auto 0;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
@@ -187,14 +187,16 @@ x-peer:not(.type-public-id) .highlight-room-public-id {
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer:not([status]):hover x-icon,
|
||||
x-peer:not([status]):focus x-icon {
|
||||
x-peer:is(:not([status]), [status$=-complete], [status=error]):hover,
|
||||
x-peer:is(:not([status]), [status$=-complete], [status=error]):focus {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
x-peer[status] x-icon {
|
||||
x-peer[status]:not([status$=-complete]) x-icon {
|
||||
box-shadow: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
x-peer[status] {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@@ -237,7 +239,7 @@ x-peer.ws-peer .highlight-wrapper {
|
||||
|
||||
.status,
|
||||
.device-name {
|
||||
opacity: 0.7;
|
||||
color: color-mix(in srgb, rgb(var(--text-color)) 30%, grey);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -246,14 +248,22 @@ x-peer[status] .device-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
x-peer[status] {
|
||||
x-peer[status]:not([status$=-complete]):not([status=error]) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
x-peer x-icon {
|
||||
x-peer {
|
||||
animation: pop 600ms ease-out 1;
|
||||
}
|
||||
|
||||
x-peer[status$=-complete] .status {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
x-peer[status=error] .status {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(0.7);
|
||||
@@ -744,7 +754,6 @@ x-dialog .dialog-subheader {
|
||||
top: -8px;
|
||||
clip: rect(0px, 80px, 80px, 40px);
|
||||
--progress: rotate(0deg);
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.circle {
|
||||
@@ -758,6 +767,10 @@ x-dialog .dialog-subheader {
|
||||
transform: var(--progress);
|
||||
}
|
||||
|
||||
.animate .circle {
|
||||
transition: transform 200ms linear;
|
||||
}
|
||||
|
||||
.over50 {
|
||||
clip: rect(auto, auto, auto, auto);
|
||||
}
|
||||
|
||||
@@ -924,6 +924,7 @@ x-peers:empty~x-instructions {
|
||||
body {
|
||||
/* Constant colors */
|
||||
--primary-color: #4285f4;
|
||||
--error-color: #ff6b6b;
|
||||
--paired-device-color: #00a69c;
|
||||
--public-room-color: #ed9d01;
|
||||
--accent-color: var(--primary-color);
|
||||
|
||||
@@ -44,15 +44,18 @@ export default class Peer {
|
||||
_setIP(request) {
|
||||
if (request.headers['cf-connecting-ip']) {
|
||||
this.ip = request.headers['cf-connecting-ip'].split(/\s*,\s*/)[0];
|
||||
} else if (request.headers['x-forwarded-for']) {
|
||||
}
|
||||
else if (request.headers['x-forwarded-for']) {
|
||||
this.ip = request.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
|
||||
} else {
|
||||
this.ip = request.connection.remoteAddress;
|
||||
}
|
||||
else {
|
||||
this.ip = request.socket.remoteAddress ?? '';
|
||||
}
|
||||
|
||||
// remove the prefix used for IPv4-translated addresses
|
||||
if (this.ip.substring(0,7) === "::ffff:")
|
||||
if (this.ip.substring(0,7) === "::ffff:") {
|
||||
this.ip = this.ip.substring(7);
|
||||
}
|
||||
|
||||
let ipv6_was_localized = false;
|
||||
if (this.conf.ipv6Localize && this.ip.includes(':')) {
|
||||
|
||||
@@ -89,22 +89,12 @@ export default class PairDropWsServer {
|
||||
this._onLeavePublicRoom(sender);
|
||||
break;
|
||||
case 'signal':
|
||||
this._signalAndRelay(sender, message);
|
||||
this._signalAndWsRelay(sender, message);
|
||||
break;
|
||||
case 'request':
|
||||
case 'header':
|
||||
case 'partition':
|
||||
case 'partition-received':
|
||||
case 'progress':
|
||||
case 'files-transfer-response':
|
||||
case 'file-transfer-complete':
|
||||
case 'message-transfer-complete':
|
||||
case 'text':
|
||||
case 'display-name-changed':
|
||||
case 'ws-chunk':
|
||||
case 'ws-relay':
|
||||
// relay ws-fallback
|
||||
if (this._conf.wsFallback) {
|
||||
this._signalAndRelay(sender, message);
|
||||
this._signalAndWsRelay(sender, message);
|
||||
}
|
||||
else {
|
||||
console.log("Websocket fallback is not activated on this instance.")
|
||||
@@ -112,7 +102,7 @@ export default class PairDropWsServer {
|
||||
}
|
||||
}
|
||||
|
||||
_signalAndRelay(sender, message) {
|
||||
_signalAndWsRelay(sender, message) {
|
||||
const room = message.roomType === 'ip'
|
||||
? sender.ip
|
||||
: message.roomId;
|
||||
@@ -261,7 +251,6 @@ export default class PairDropWsServer {
|
||||
return;
|
||||
}
|
||||
|
||||
this._leavePublicRoom(sender);
|
||||
this._joinPublicRoom(sender, message.publicRoomId);
|
||||
}
|
||||
|
||||
@@ -322,7 +311,7 @@ export default class PairDropWsServer {
|
||||
|
||||
_joinPublicRoom(peer, publicRoomId) {
|
||||
// prevent joining of 2 public rooms simultaneously
|
||||
this._leavePublicRoom(peer);
|
||||
this._leavePublicRoom(peer, true);
|
||||
|
||||
this._joinRoom(peer, 'public-id', publicRoomId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user