Compare commits

...

21 Commits

Author SHA1 Message Date
schlagmichdoch 67a1b04da2 increase version to v1.3.0 2023-03-03 19:45:04 +01:00
schlagmichdoch 8b2eb67266 fix position of close btn on about page 2023-03-03 19:43:31 +01:00
schlagmichdoch 827b10219d Merge pull request #58 from schlagmichdoch/define_turn_config_dynamically
STUN/TURN config can now be changed dynamically via environment variable 🎉
2023-03-03 19:42:52 +01:00
schlagmichdoch e7ab5e26cc Add dynamic stun/turn config as new feature to README.md 2023-03-03 19:41:55 +01:00
schlagmichdoch 8bcaa3f60f Fix header hierarchy for dynamic stun/turn in docs 2023-03-03 18:28:49 +01:00
schlagmichdoch c0a4224a59 merge master into branch 2023-03-03 18:01:24 +01:00
schlagmichdoch 1e35bab327 increase version to v1.2.2 2023-03-03 17:07:02 +01:00
schlagmichdoch bb0493d071 Make user notifications and document titles more concise. 2023-03-03 17:03:10 +01:00
schlagmichdoch bfb5aa8546 fix overwrite method _onMessage of class RTCPeer 2023-03-03 16:36:55 +01:00
schlagmichdoch a9d7960a59 increase version to v1.2.1 2023-03-03 13:12:06 +01:00
schlagmichdoch 39ca5b2d21 ws-fallback: remove all WSPeers when server connection disconnects + fix onPeerLeft 2023-03-03 13:10:14 +01:00
schlagmichdoch cf715b2872 stability on reconnect: prevent "peer-left" signal after "peer-joined" by leaving rooms first before reentering them, clear _keepAlive timeout before joining ip room and not manually terminating sockets 2023-03-03 13:10:14 +01:00
schlagmichdoch bbb8c1b10f ws-fallback: prevent signaling from stopping on reconnect. Do not stop to signal until both devices have sent event "peer-connected" 2023-03-03 13:10:13 +01:00
schlagmichdoch d6ef5887dd move logging of rtc message from class Peer class to overwritten method in class RTCPeer 2023-03-03 12:38:34 +01:00
schlagmichdoch f9f1abef7a Replace all urls in received messages with links. Center the message if it does not include any whitespace. 2023-03-03 12:28:50 +01:00
schlagmichdoch d244f5fa47 fix circles position on ios safari are shifted by url bar 2023-03-03 12:03:20 +01:00
schlagmichdoch 3a2d8c75f7 - restructure and unify dialogs to use less space on mobile and be clearer
- give user option both options "share" and "download" on mobile
- add fallback if zipper fails that downloads files individually
- fix dequeuing of message queue not possible if sending peer has left
2023-03-03 12:01:43 +01:00
schlagmichdoch 545cdc2459 Fix browser reloading when first message is sent by preventing event default on submit 2023-03-02 16:30:47 +01:00
schlagmichdoch de76da52fe merge master into branch 2023-03-01 21:55:50 +01:00
schlagmichdoch 66359da2ca get rtcConfig dynamically from the server 2023-02-24 18:08:48 +01:00
schlagmichdoch 74b88c2e7d fix dialog heights 2023-02-24 16:53:13 +01:00
16 changed files with 716 additions and 544 deletions
+1
View File
@@ -63,6 +63,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87)) * Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87))
* Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101)) * Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101))
* To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558) * To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558)
* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers)
## Screenshots ## Screenshots
<div align="center"> <div align="center">
+73 -11
View File
@@ -17,7 +17,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ls
Set options by using the following flags in the `docker run` command: Set options by using the following flags in the `docker run` command:
##### Port ##### Port
``` ```bash
-p 127.0.0.1:8080:3000 -p 127.0.0.1:8080:3000
``` ```
> Specify the port used by the docker image > Specify the port used by the docker image
@@ -30,7 +30,7 @@ Set options by using the following flags in the `docker run` command:
> Limits clients to 1000 requests per 5 min > Limits clients to 1000 requests per 5 min
##### Websocket Fallback (for VPN) ##### Websocket Fallback (for VPN)
``` ```bash
-e WS_FALLBACK=true -e WS_FALLBACK=true
``` ```
> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. > Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client.
@@ -42,6 +42,36 @@ Set options by using the following flags in the `docker run` command:
> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. > Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust.
> Additionally, beware that all traffic using this fallback debits the servers data plan. > Additionally, beware that all traffic using this fallback debits the servers data plan.
##### Specify STUN/TURN Servers
```bash
-e RTC_CONFIG="rtc_config.json"
```
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> Default configuration:
> ```json
> {
> "sdpSemantics": "unified-plan",
> "iceServers": [
> {
> "urls": "stun:stun.l.google.com:19302"
> },
> {
> "urls": "stun:openrelay.metered.ca:80"
> },
> {
> "urls": "turn:openrelay.metered.ca:443",
> "username": "openrelayproject",
> "credential": "openrelayproject"
> }
> ]
> }
> ```
<br>
### Docker Image from GHCR ### Docker Image from GHCR
```bash ```bash
docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod
@@ -141,6 +171,38 @@ $env:PORT=3010; npm start
``` ```
> Specify the port PairDrop is running on. (Default: 3000) > Specify the port PairDrop is running on. (Default: 3000)
#### Specify STUN/TURN Server
On Unix based systems
```bash
RTC_CONFIG="rtc_config.json" npm start
```
On Windows
```bash
$env:RTC_CONFIG="rtc_config.json"; npm start
```
> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration.
> You can use `pairdrop/rtc_config_example.json` as a starting point.
>
> Default configuration:
> ```json
> {
> "sdpSemantics": "unified-plan",
> "iceServers": [
> {
> "urls": "stun:stun.l.google.com:19302"
> },
> {
> "urls": "stun:openrelay.metered.ca:80"
> },
> {
> "urls": "turn:openrelay.metered.ca:443",
> "username": "openrelayproject",
> "credential": "openrelayproject"
> }
> ]
> }
> ```
### Options / Flags ### Options / Flags
#### Local Run #### Local Run
```bash ```bash
@@ -262,13 +324,13 @@ server {
### Using Apache ### Using Apache
install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel` install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel`
```shell ```bash
a2enmod proxy a2enmod proxy
``` ```
```shell ```bash
a2enmod proxy_http a2enmod proxy_http
``` ```
```shell ```bash
a2enmod proxy_wstunnel a2enmod proxy_wstunnel
``` ```
@@ -278,7 +340,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
**pairdrop.conf** **pairdrop.conf**
#### Allow http and https requests #### Allow http and https requests
``` ```apacheconf
<VirtualHost *:80> <VirtualHost *:80>
ProxyPass / http://127.0.0.1:3000/ ProxyPass / http://127.0.0.1:3000/
RewriteEngine on RewriteEngine on
@@ -295,7 +357,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
</VirtualHost> </VirtualHost>
``` ```
#### Automatic http to https redirect: #### Automatic http to https redirect:
``` ```apacheconf
<VirtualHost *:80> <VirtualHost *:80>
Redirect permanent / https://127.0.0.1:3000/ Redirect permanent / https://127.0.0.1:3000/
</VirtualHost> </VirtualHost>
@@ -308,10 +370,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian)
</VirtualHost> </VirtualHost>
``` ```
Activate the new virtual host and reload apache: Activate the new virtual host and reload apache:
```shell ```bash
a2ensite pairdrop a2ensite pairdrop
``` ```
```shell ```bash
service apache2 reload service apache2 reload
``` ```
@@ -322,7 +384,7 @@ All files needed for developing are available on the branch `dev`.
First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/) First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/)
Then, clone the repository and run docker-compose: Then, clone the repository and run docker-compose:
```shell ```bash
git clone https://github.com/schlagmichdoch/PairDrop.git git clone https://github.com/schlagmichdoch/PairDrop.git
cd PairDrop cd PairDrop
@@ -347,7 +409,7 @@ The nginx container creates a CA certificate and a website certificate for you.
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. 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 Windows, make sure to install it to the `Trusted Root Certification Authorities` store.
- On MacOS, double click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL. - On 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`. When prompted, select `Trust this CA to identify websites` and click OK. - Firefox uses its own trust store. To install the CA, point Firefox at `http://<Your FQDN>:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK.
- 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). - 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).
+30 -6
View File
@@ -1,6 +1,8 @@
const process = require('process') const process = require('process')
const crypto = require('crypto') const crypto = require('crypto')
const {spawn} = require('child_process') const {spawn} = require('child_process')
const WebSocket = require('ws');
const fs = require('fs');
// Handle SIGINT // Handle SIGINT
process.on('SIGINT', () => { process.on('SIGINT', () => {
@@ -49,6 +51,25 @@ if (process.argv.includes('--auto-restart')) {
); );
} }
const rtcConfig = process.env.RTC_CONFIG
? fs.readFileSync(process.env.RTC_CONFIG, 'utf8')
: {
"sdpSemantics": "unified-plan",
"iceServers": [
{
"urls": "stun:stun.l.google.com:19302"
},
{
"urls": "stun:openrelay.metered.ca:80"
},
{
"urls": "turn:openrelay.metered.ca:443",
"username": "openrelayproject",
"credential": "openrelayproject"
}
]
};
const express = require('express'); const express = require('express');
const RateLimit = require('express-rate-limit'); const RateLimit = require('express-rate-limit');
const http = require('http'); const http = require('http');
@@ -99,7 +120,6 @@ const { uniqueNamesGenerator, animals, colors } = require('unique-names-generato
class PairDropServer { class PairDropServer {
constructor() { constructor() {
const WebSocket = require('ws');
this._wss = new WebSocket.Server({ server }); this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
@@ -110,10 +130,14 @@ class PairDropServer {
} }
_onConnection(peer) { _onConnection(peer) {
this._joinRoom(peer);
peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e); peer.socket.onerror = e => console.error(e);
this._keepAlive(peer); this._keepAlive(peer);
this._send(peer, {
type: 'rtc-config',
config: rtcConfig
});
this._joinRoom(peer);
// send displayName // send displayName
this._send(peer, { this._send(peer, {
@@ -317,6 +341,10 @@ class PairDropServer {
_joinRoom(peer, roomType = 'ip', roomSecret = '') { _joinRoom(peer, roomType = 'ip', roomSecret = '') {
const room = roomType === 'ip' ? peer.ip : roomSecret; const room = roomType === 'ip' ? peer.ip : roomSecret;
if (this._rooms[room] && this._rooms[room][peer.id]) {
this._leaveRoom(peer, roomType, roomSecret);
}
// if room doesn't exist, create it // if room doesn't exist, create it
if (!this._rooms[room]) { if (!this._rooms[room]) {
this._rooms[room] = {}; this._rooms[room] = {};
@@ -341,10 +369,6 @@ class PairDropServer {
// delete the peer // delete the peer
delete this._rooms[room][peer.id]; delete this._rooms[room][peer.id];
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room //if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) { if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room]; delete this._rooms[room];
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.0", "version": "1.3.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.0", "version": "1.3.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.0", "version": "1.3.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+49 -43
View File
@@ -69,7 +69,7 @@
<use xlink:href="#clear-pair-devices-icon" /> <use xlink:href="#clear-pair-devices-icon" />
</svg> </svg>
</a> </a>
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a> <a id="cancel-paste-mode" class="button" hidden>Done</a>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center"> <div id="center">
@@ -106,18 +106,17 @@
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div> <div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr> <hr>
<div id="key-input-container"> <div id="key-input-container">
<input id="char0" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input id="char1" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char2" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char3" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char4" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char5" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div> <div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" disabled>Pair</button>
<div class="separator"></div> <button class="button" close>Cancel</button>
<a class="button" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -130,9 +129,9 @@
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Unpair Devices</h2> <h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div> <div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button> <button class="button" type="submit">Unpair Devices</button>
<a class="button" close>Cancel</a> <button class="button" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -142,25 +141,23 @@
<x-dialog id="receive-request-dialog"> <x-dialog id="receive-request-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">PairDrop</h2> <h2 class="center"></h2>
<div class="text-center file-description"> <div class="center column file-description">
<div> <div>
<span id="requesting-peer-display-name"></span> <span class="display-name"></span>
<span>would like to share</span> <span>would like to share</span>
</div> </div>
<div id="file-name" class="row" > <div class="row file-name" >
<span id="file-stem"></span> <span class="file-stem"></span>
<span id="file-extension"></span> <span class="file-extension"></span>
</div> </div>
<div class="row"> <div class="row file-other">
<span id="file-other"></span>
</div> </div>
<div class="row font-body2 file-size"></div>
</div> </div>
<div class="font-body2 text-center file-size"></div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<div class="separator"></div>
<button id="decline-request" class="button" title="ESCAPE">Decline</button> <button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div> </div>
</x-paper> </x-paper>
@@ -170,13 +167,23 @@
<x-dialog id="receive-file-dialog"> <x-dialog id="receive-file-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 id="receive-title" class="center"></h2> <h2 class="center"></h2>
<div class="text-center file-description"></div> <div class="center column file-description">
<div class="font-body2 text-center file-size"></div> <div>
<span class="display-name"></span>
<span>has sent</span>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<a id="share-or-download" class="button" autofocus></a> <button id="share-btn" class="button" autofocus hidden>Share</button>
<div class="separator"></div> <button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button> <button class="button" close>Close</button>
</div> </div>
</x-paper> </x-paper>
@@ -187,16 +194,16 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">PairDrop</h2> <h2 class="text-center">Send Message</h2>
<div class="text-center"> <div class="dialog-subheader text-center">
<span>Send a Message to</span> <span>Send a Message to</span>
<span id="text-send-peer-display-name"></span> <span class="display-name"></span>
</div> </div>
<div class="row-separator"></div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button> <button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<div class="separator"></div> <button class="button" title="ESCAPE" close>Cancel</button>
<a class="button" title="ESCAPE" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -206,16 +213,15 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2>PairDrop - Message Received</h2> <h2 class="text-center">Message Received</h2>
<div id="receive-text-description-container"> <div class="text-center dialog-subheader">
<span id="receive-text-peer-display-name"></span> <span class="display-name"></span>
<span>sent the following message:</span> <span>has sent:</span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="row-reverse"> <div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button id="close" class="button" title="ESCAPE">Close</button> <button id="close" class="button" title="ESCAPE">Close</button>
</div> </div>
</x-paper> </x-paper>
@@ -236,14 +242,14 @@
</div> </div>
<!-- About Page --> <!-- About Page -->
<x-about id="about" class="full center column"> <x-about id="about" class="full center column">
<section class="center column fade-in"> <header class="row-reverse fade-in">
<header class="row-reverse">
<a href="#" class="close icon-button"> <a href="#" class="close icon-button">
<svg class="icon"> <svg class="icon">
<use xlink:href="#close-icon" /> <use xlink:href="#close-icon" />
</svg> </svg>
</a> </a>
</header> </header>
<section class="center column fade-in">
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
+29 -29
View File
@@ -36,6 +36,7 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
} }
_sendRoomSecrets(roomSecrets) { _sendRoomSecrets(roomSecrets) {
@@ -58,10 +59,17 @@ class ServerConnection {
this.send({ type: 'pair-device-join', roomKey: roomKey }) this.send({ type: 'pair-device-join', roomKey: roomKey })
} }
_setRtcConfig(config) {
window.rtcConfig = config;
}
_onMessage(msg) { _onMessage(msg) {
msg = JSON.parse(msg); msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg); if (msg.type !== 'ping') console.log('WS:', msg);
switch (msg.type) { switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers': case 'peers':
Events.fire('peers', msg); Events.fire('peers', msg);
break; break;
@@ -145,15 +153,17 @@ class ServerConnection {
this._socket.close(); this._socket.close();
this._socket = null; this._socket = null;
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
} }
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...'); Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 5000); this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
_onVisibilityChange() { _onVisibilityChange() {
@@ -319,26 +329,25 @@ class Peer {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
message = JSON.parse(message); const messageJSON = JSON.parse(message);
console.log('RTC:', message); switch (messageJSON.type) {
switch (message.type) {
case 'request': case 'request':
this._onFilesTransferRequest(message); this._onFilesTransferRequest(messageJSON);
break; break;
case 'header': case 'header':
this._onFilesHeader(message); this._onFilesHeader(messageJSON);
break; break;
case 'partition': case 'partition':
this._onReceivedPartitionEnd(message); this._onReceivedPartitionEnd(messageJSON);
break; break;
case 'partition-received': case 'partition-received':
this._sendNextPartition(); this._sendNextPartition();
break; break;
case 'progress': case 'progress':
this._onDownloadProgress(message.progress); this._onDownloadProgress(messageJSON.progress);
break; break;
case 'files-transfer-response': case 'files-transfer-response':
this._onFileTransferRequestResponded(message); this._onFileTransferRequestResponded(messageJSON);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted();
@@ -347,7 +356,7 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(message); this._onTextReceived(messageJSON);
break; break;
} }
} }
@@ -441,7 +450,7 @@ class Peer {
if (!this._requestAccepted.header.length) { if (!this._requestAccepted.header.length) {
this._busy = false; this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted}); Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = []; this._filesReceived = [];
this._requestAccepted = null; this._requestAccepted = null;
} }
@@ -492,6 +501,7 @@ class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller if (!peerId) return; // we will listen for a caller
this._connect(peerId, true); this._connect(peerId, true);
} }
@@ -509,7 +519,7 @@ class RTCPeer extends Peer {
_openConnection(peerId, isCaller) { _openConnection(peerId, isCaller) {
this._isCaller = isCaller; this._isCaller = isCaller;
this._peerId = peerId; this._peerId = peerId;
this._conn = new RTCPeerConnection(RTCPeer.config); this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
@@ -568,6 +578,13 @@ class RTCPeer extends Peer {
this._channel = channel; this._channel = channel;
} }
_onMessage(message) {
if (typeof message === 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
}
getConnectionHash() { getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n"); const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
@@ -852,20 +869,3 @@ class Events {
return window.removeEventListener(type, callback, false); return window.removeEventListener(type, callback, false);
} }
} }
RTCPeer.config = {
'sdpSemantics': 'unified-plan',
'iceServers': [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'stun:openrelay.metered.ca:80'
},
{
urls: 'turn:openrelay.metered.ca:443',
username: 'openrelayproject',
credential: 'openrelayproject',
},
]
}
+149 -119
View File
@@ -1,6 +1,5 @@
const $ = query => document.getElementById(query); const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(query); const $$ = query => document.body.querySelector(query);
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
window.isProductionEnvironment = !window.location.host.startsWith('localhost'); window.isProductionEnvironment = !window.location.host.startsWith('localhost');
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent); window.android = /android/i.test(navigator.userAgent);
@@ -28,7 +27,7 @@ class PeersUI {
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {}; this.peers = {};
this.$cancelPasteModeBtn = $('cancel-paste-mode-btn'); this.$cancelPasteModeBtn = $('cancel-paste-mode');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e)); Events.on('dragover', e => this._onDragOver(e));
@@ -473,10 +472,14 @@ class Dialog {
class ReceiveDialog extends Dialog { class ReceiveDialog extends Dialog {
constructor(id) { constructor(id) {
super(id); super(id);
this.$fileDescription = this.$el.querySelector('.file-description');
this.$fileDescriptionNode = this.$el.querySelector('.file-description'); this.$displayName = this.$el.querySelector('.display-name');
this.$fileSizeNode = this.$el.querySelector('.file-size'); this.$fileStem = this.$el.querySelector('.file-stem');
this.$previewBox = this.$el.querySelector('.file-preview') this.$fileExtension = this.$el.querySelector('.file-extension');
this.$fileOther = this.$el.querySelector('.file-other');
this.$fileSize = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview');
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
} }
_formatFileSize(bytes) { _formatFileSize(bytes) {
@@ -492,6 +495,26 @@ class ReceiveDialog extends Dialog {
return bytes + ' Bytes'; return bytes + ' Bytes';
} }
} }
_parseFileData(displayName, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
fileOtherText += imagesOnly ? 'image' : 'file';
} else {
fileOtherText += imagesOnly ? 'images' : 'files';
}
this.$fileOther.innerText = fileOtherText;
}
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
} }
class ReceiveFileDialog extends ReceiveDialog { class ReceiveFileDialog extends ReceiveDialog {
@@ -499,24 +522,25 @@ class ReceiveFileDialog extends ReceiveDialog {
constructor() { constructor() {
super('receive-file-dialog'); super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download'); this.$downloadBtn = this.$el.querySelector('#download-btn');
this.$receiveTitleNode = this.$el.querySelector('#receive-title') this.$shareBtn = this.$el.querySelector('#share-btn');
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request)); Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
this._filesQueue = []; this._filesQueue = [];
} }
_onFilesReceived(sender, files, request) { _onFilesReceived(sender, files, imagesOnly, totalSize) {
this._nextFiles(sender, files, request); const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play(); window.blop.play();
} }
_nextFiles(sender, nextFiles, nextRequest) { _nextFiles() {
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
if (this._busy) return; if (this._busy) return;
this._busy = true; this._busy = true;
const {peerId, files, request} = this._filesQueue.shift(); const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peerId, files, request); this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
} }
_dequeueFile() { _dequeueFile() {
@@ -547,7 +571,6 @@ class ReceiveFileDialog extends ReceiveDialog {
let element = document.createElement(previewElement[mime]); let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file); element.src = URL.createObjectURL(file);
element.controls = true; element.controls = true;
element.classList.add('element-preview');
element.onload = _ => { element.onload = _ => {
this.$previewBox.appendChild(element); this.$previewBox.appendChild(element);
resolve(true) resolve(true)
@@ -558,30 +581,32 @@ class ReceiveFileDialog extends ReceiveDialog {
}); });
} }
async _displayFiles(peerId, files, request) { async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback); this._parseFileData(displayName, files, imagesOnly, totalSize);
let url;
let title;
let filenameDownload;
let descriptor = request.imagesOnly ? "Image" : "File";
let size = this._formatFileSize(request.totalSize);
let description = files[0].name;
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
url = URL.createObjectURL(files[0]) descriptor = imagesOnly ? 'Image' : 'File';
title = `PairDrop - ${descriptor} Received`
filenameDownload = files[0].name;
} else { } else {
title = `PairDrop - ${files.length} ${descriptor}s Received` descriptor = imagesOnly ? 'Images' : 'Files';
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`; }
if(files.length>2) description += "s"; this.$receiveTitle.innerText = `${descriptor} Received`;
if(!shareInsteadOfDownload) { const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
if (canShare) {
this.$shareBtn.removeAttribute('hidden');
this.$shareBtn.onclick = _ => {
navigator.share({files: files})
.catch(err => {
console.error(err);
});
}
}
let downloadZipped = false;
if (files.length > 1) {
downloadZipped = true;
try {
let bytesCompleted = 0; let bytesCompleted = 0;
zipper.createNewZipWriter(); zipper.createNewZipWriter();
for (let i=0; i<files.length; i++) { for (let i=0; i<files.length; i++) {
@@ -589,7 +614,7 @@ class ReceiveFileDialog extends ReceiveDialog {
onprogress: (progress) => { onprogress: (progress) => {
Events.fire('set-progress', { Events.fire('set-progress', {
peerId: peerId, peerId: peerId,
progress: (bytesCompleted + progress) / request.totalSize, progress: (bytesCompleted + progress) / totalSize,
status: 'process' status: 'process'
}) })
} }
@@ -609,49 +634,58 @@ class ReceiveFileDialog extends ReceiveDialog {
let minutes = now.getMinutes().toString(); let minutes = now.getMinutes().toString();
minutes = minutes.length < 2 ? "0" + minutes : minutes; minutes = minutes.length < 2 ? "0" + minutes : minutes;
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`; filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
} catch (e) {
console.error(e);
downloadZipped = false;
} }
} }
this.$receiveTitleNode.textContent = title; this.$downloadBtn.innerText = "Download";
this.$fileDescriptionNode.textContent = description; this.$downloadBtn.onclick = _ => {
this.$fileSizeNode.textContent = size; if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
if (shareInsteadOfDownload) { tmpZipBtn.download = filenameDownload;
this.$shareOrDownloadBtn.innerText = "Share"; tmpZipBtn.href = url;
this.continue = _ => { tmpZipBtn.click();
navigator.share({files: files})
.catch(err => console.error(err));
}
this.continueCallback = _ => this.continue();
} else { } else {
this.$shareOrDownloadBtn.innerText = "Download again"; this._downloadFilesIndividually(files);
this.continue = _ => {
let tmpBtn = document.createElement("a");
tmpBtn.download = filenameDownload;
tmpBtn.href = url;
tmpBtn.click();
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
} }
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
if (!canShare) {
this.$downloadBtn.innerText = "Download again";
}
Events.fire('notify-user', `${descriptor} downloaded successfully`);
this.$downloadBtn.style.pointerEvents = "none";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => { this.createPreviewElement(files[0]).finally(_ => {
document.title = files.length === 1 document.title = files.length === 1
? 'File received - PairDrop' ? 'File received - PairDrop'
: `(${files.length}) Files received - PairDrop`; : `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.continue(); this.show();
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r)); }).catch(r => console.error(r));
} }
_downloadFilesIndividually(files) {
let tmpBtn = document.createElement("a");
for (let i=0; i<files.length; i++) {
tmpBtn.download = files[i].name;
tmpBtn.href = URL.createObjectURL(files[i]);
tmpBtn.click();
}
}
hide() { hide() {
this.$shareOrDownloadBtn.removeAttribute('href'); this.$shareBtn.setAttribute('hidden', '');
this.$shareOrDownloadBtn.removeAttribute('download');
this.$previewBox.innerHTML = ''; this.$previewBox.innerHTML = '';
super.hide(); super.hide();
this._dequeueFile(); this._dequeueFile();
@@ -663,11 +697,6 @@ class ReceiveRequestDialog extends ReceiveDialog {
constructor() { constructor() {
super('receive-request-dialog'); super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name');
this.$fileStemNode = this.$el.querySelector('#file-stem');
this.$fileExtensionNode = this.$el.querySelector('#file-extension');
this.$fileOtherNode = this.$el.querySelector('#file-other');
this.$acceptRequestBtn = this.$el.querySelector('#accept-request'); this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
this.$declineRequestBtn = this.$el.querySelector('#decline-request'); this.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true)); this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
@@ -699,32 +728,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
_showRequestDialog(request, peerId) { _showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
const fileName = request.header[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtensionNode.innerText = fileExtension
if (request.header.length >= 2) {
let fileOtherText = ` and ${request.header.length - 1} other `;
fileOtherText += request.imagesOnly ? 'image' : 'file';
if (request.header.length > 2) fileOtherText += "s";
this.$fileOtherNode.innerText = fileOtherText;
}
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img'); let element = document.createElement('img');
element.src = request.thumbnailDataUrl; element.src = request.thumbnailDataUrl;
element.classList.add('element-preview');
this.$previewBox.appendChild(element) this.$previewBox.appendChild(element)
} }
document.title = 'File Transfer Requested - PairDrop'; this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show(); this.show();
} }
@@ -759,7 +774,7 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn = $('clear-pair-devices'); this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form'); let createJoinForm = this.$el.querySelector('form');
createJoinForm.addEventListener('submit', _ => this._onSubmit()); createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel()) this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e))); this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
@@ -838,7 +853,7 @@ class PairDeviceDialog extends Dialog {
}) })
this.$submitBtn.removeAttribute("disabled"); this.$submitBtn.removeAttribute("disabled");
if (document.activeElement === this.$inputRoomKeyChars[5]) { if (document.activeElement === this.$inputRoomKeyChars[5]) {
this._onSubmit(); this._pairDeviceJoin(this.inputRoomKey);
} }
} }
} }
@@ -888,7 +903,8 @@ class PairDeviceDialog extends Dialog {
return url.href; return url.href;
} }
_onSubmit() { _onSubmit(e) {
e.preventDefault();
this._pairDeviceJoin(this.inputRoomKey); this._pairDeviceJoin(this.inputRoomKey);
} }
@@ -975,14 +991,19 @@ class ClearDevicesDialog extends Dialog {
super('clear-devices-dialog'); super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form'); let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
} }
_onClearPairDevices() { _onClearPairDevices() {
this.show(); this.show();
} }
_onSubmit() { _onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets'); Events.fire('clear-room-secrets');
this.hide(); this.hide();
} }
@@ -993,10 +1014,10 @@ class SendTextDialog extends Dialog {
super('send-text-dialog'); super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input'); this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name'); this.$peerDisplayName = this.$el.querySelector('.display-name');
this.$form = this.$el.querySelector('form'); this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]'); this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send()); this.$form.addEventListener('submit', e => this._onSubmit(e));
this.$text.addEventListener('input', e => this._onChange(e)); this.$text.addEventListener('input', e => this._onChange(e));
Events.on("keydown", e => this._onKeyDown(e)); Events.on("keydown", e => this._onKeyDown(e));
} }
@@ -1038,6 +1059,11 @@ class SendTextDialog extends Dialog {
sel.addRange(range); sel.addRange(range);
} }
_onSubmit(e) {
e.preventDefault();
this._send();
}
_send() { _send() {
Events.fire('send-text', { Events.fire('send-text', {
to: this.correspondingPeerId, to: this.correspondingPeerId,
@@ -1061,7 +1087,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e)); Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name'); this.$displayNameNode = this.$el.querySelector('.display-name');
this._receiveTextQueue = []; this._receiveTextQueue = [];
} }
@@ -1091,18 +1117,23 @@ class ReceiveTextDialog extends Dialog {
} }
_showReceiveTextDialog(text, peerId) { _showReceiveTextDialog(text, peerId) {
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); this.$displayNameNode.innerText = $(peerId).ui._displayName();
if (isURL(text)) { this.$text.innerText = text;
const $a = document.createElement('a'); this.$text.classList.remove('text-center');
$a.href = text;
$a.target = '_blank'; // Beautify text if text is short
$a.textContent = text; if (text.length < 2000) {
this.$text.innerHTML = ''; // replace urls with actual links
this.$text.appendChild($a); this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
} else { return `<a href="${url}" target="_blank">${url}</a>`;
this.$text.textContent = text; });
if (!/\s/.test(text)) {
this.$text.classList.add('text-center');
} }
}
this._setDocumentTitleMessages(); this._setDocumentTitleMessages();
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
@@ -1112,7 +1143,7 @@ class ReceiveTextDialog extends Dialog {
_setDocumentTitleMessages() { _setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop' ? 'Message Received - PairDrop'
: `(${this._receiveTextQueue.length + 1}) Messages Received - PairDrop`; : `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
} }
async _onCopy() { async _onCopy() {
@@ -1187,7 +1218,7 @@ class Base64ZipDialog extends Dialog {
} }
_setPasteBtnToProcessing() { _setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none"; this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = "Processing...";
} }
@@ -1332,7 +1363,7 @@ class Notifications {
_messageNotification(message, peerId) { _messageNotification(message, peerId) {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName(); const peerDisplayName = $(peerId).ui._displayName();
if (isURL(message)) { if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
this._bind(notification, _ => window.open(message, '_blank', null, true)); this._bind(notification, _ => window.open(message, '_blank', null, true));
} else { } else {
@@ -1393,7 +1424,7 @@ class NetworkStatusUI {
constructor() { constructor() {
Events.on('offline', _ => this._showOfflineMessage()); Events.on('offline', _ => this._showOfflineMessage());
Events.on('online', _ => this._showOnlineMessage()); Events.on('online', _ => this._showOnlineMessage());
Events.on('ws-connected', _ => this._showOnlineMessage()); Events.on('ws-connected', _ => this._onWsConnected());
Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-disconnected', _ => this._onWsDisconnected());
if (!navigator.onLine) this._showOfflineMessage(); if (!navigator.onLine) this._showOfflineMessage();
} }
@@ -1404,17 +1435,16 @@ class NetworkStatusUI {
} }
_showOnlineMessage() { _showOnlineMessage() {
window.animateBackground(true);
if (!this.firstConnect) {
this.firstConnect = true;
return;
}
Events.fire('notify-user', 'You are back online'); Events.fire('notify-user', 'You are back online');
window.animateBackground(true);
}
_onWsConnected() {
window.animateBackground(true);
} }
_onWsDisconnected() { _onWsDisconnected() {
window.animateBackground(false); window.animateBackground(false);
if (!this.firstConnect) this.firstConnect = true;
} }
} }
@@ -1752,8 +1782,8 @@ Events.on('load', () => {
let x0, y0, w, h, dw, offset; let x0, y0, w, h, dw, offset;
function init() { function init() {
w = window.innerWidth; w = document.documentElement.clientWidth;
h = window.innerHeight; h = document.documentElement.clientHeight;
c.width = w; c.width = w;
c.height = h; c.height = h;
offset = $$('footer').offsetHeight - 32; offset = $$('footer').offsetHeight - 32;
+1 -1
View File
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.2.0'; const cacheVersion = 'v1.3.0';
const cacheTitle = `pairdrop-cache-${cacheVersion}`; const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',
+56 -63
View File
@@ -564,7 +564,7 @@ x-dialog x-background {
z-index: 10; z-index: 10;
transition: opacity 300ms; transition: opacity 300ms;
will-change: opacity; will-change: opacity;
padding: 35px; padding: 15px;
overflow: overlay; overflow: overlay;
} }
@@ -575,19 +575,20 @@ x-dialog x-paper {
padding: 16px 24px; padding: 16px 24px;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
transition: transform 300ms; transition: transform 300ms;
will-change: transform; will-change: transform;
} }
#pair-device-dialog x-paper { #pair-device-dialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
width: calc(100vw - 20px);
height: 625px;
} }
x-dialog:not([show]) { x-dialog:not([show]) {
@@ -602,12 +603,6 @@ x-dialog:not([show]) x-background {
opacity: 0; opacity: 0;
} }
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a { x-dialog a {
color: var(--primary-color); color: var(--primary-color);
@@ -646,7 +641,7 @@ x-dialog .font-subheading {
} }
#key-input-container>input:nth-of-type(4) { #key-input-container>input:nth-of-type(4) {
margin-left: 18px; margin-left: 5%;
} }
#room-key { #room-key {
@@ -658,16 +653,11 @@ x-dialog .font-subheading {
} }
#room-key-qr-code { #room-key-qr-code {
padding: inherit; margin: 16px;
margin: auto;
width: 150px;
height: 150px;
} }
#pair-device-dialog hr { #pair-device-dialog hr {
margin-top: 40px; margin: 40px -24px;
margin-bottom: 40px;
width: 100%;
} }
#pair-device-dialog x-background { #pair-device-dialog x-background {
@@ -681,29 +671,24 @@ x-dialog .row {
margin-bottom: 8px; margin-bottom: 8px;
} }
x-dialog h2 { /* button row*/
margin-top: 1rem; x-paper > div:last-child {
} margin: auto -24px -15px;
#receive-request-dialog h2,
#receive-file-dialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px 0;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
height: 50px;
} }
.separator { x-paper > div:last-child > .button {
border: solid 1.25px var(--border-color); height: 100%;
margin-bottom: -16px; width: 50%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
} }
.file-description { .file-description {
word-break: break-word; margin-bottom: 25px;
width: 80%;
margin: auto;
} }
.file-description .row { .file-description .row {
@@ -715,26 +700,26 @@ x-dialog .row-reverse {
word-break: normal; word-break: normal;
} }
#file-name { .file-name {
font-style: italic; font-style: italic;
max-width: 100%;
} }
#file-stem { .file-stem {
max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-all; white-space: nowrap;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
} }
/* Send Text Dialog */ /* Send Text Dialog */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input { #text-input {
min-height: 120px; min-height: 200px;
margin: 14px auto;
} }
/* Receive Text Dialog */ /* Receive Text Dialog */
@@ -742,14 +727,14 @@ x-dialog .row-reverse {
#receive-text-dialog #text { #receive-text-dialog #text {
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: calc(100vh - 393px);
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-user-select: all; -webkit-user-select: all;
-moz-user-select: all; -moz-user-select: all;
user-select: all; user-select: all;
white-space: pre-wrap; white-space: pre-wrap;
margin-top:36px; padding: 15px 0;
} }
#receive-text-dialog #text a { #receive-text-dialog #text a {
@@ -768,11 +753,7 @@ x-dialog .row-reverse {
.row-separator { .row-separator {
border-bottom: solid 2.5px var(--border-color); border-bottom: solid 2.5px var(--border-color);
margin: auto -25px; margin: auto -24px;
}
#receive-text-description-container {
margin-bottom: 25px;
} }
#base64-paste-btn { #base64-paste-btn {
@@ -800,7 +781,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0; padding: 2px 16px 0;
box-sizing: border-box; box-sizing: border-box;
min-height: 36px; min-height: 36px;
min-width: 100px;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
font-weight: 700; font-weight: 700;
@@ -811,6 +791,7 @@ x-dialog .row-reverse {
user-select: none; user-select: none;
background: inherit; background: inherit;
color: var(--primary-color); color: var(--primary-color);
overflow: hidden;
} }
.button[disabled] { .button[disabled] {
@@ -848,7 +829,7 @@ x-dialog .row-reverse {
opacity: 0.1; opacity: 0.1;
} }
#cancel-paste-mode-btn { #cancel-paste-mode {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -875,7 +856,6 @@ button::-moz-focus-inner {
/* Icon Button */ /* Icon Button */
.icon-button { .icon-button {
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -885,10 +865,7 @@ button::-moz-focus-inner {
border-radius: 50%; border-radius: 50%;
} }
/* Text Input */ /* Text Input */
.textarea { .textarea {
box-sizing: border-box; box-sizing: border-box;
border: none; border: none;
@@ -902,9 +879,8 @@ button::-moz-focus-inner {
display: block; display: block;
overflow: auto; overflow: auto;
resize: none; resize: none;
min-height: 40px;
line-height: 16px; line-height: 16px;
max-height: 300px; max-height: calc(100vh - 254px);
white-space: pre; white-space: pre;
} }
@@ -963,6 +939,13 @@ button::-moz-focus-inner {
margin: 8px 8px -16px; margin: 8px 8px -16px;
} }
#about section {
flex-grow: 1;
}
#about header {
align-self: end;
}
/* Loading Indicator */ /* Loading Indicator */
@@ -1094,6 +1077,14 @@ x-peers:empty~x-instructions {
} }
/* Responsive Styles */ /* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
padding: 15px;
}
x-paper > div:last-child {
margin: auto -15px -15px;
}
}
@media screen and (min-height: 800px) { @media screen and (min-height: 800px) {
footer { footer {
@@ -1166,7 +1157,9 @@ x-dialog x-paper {
display: none; display: none;
} }
.element-preview { .file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%; max-width: 100%;
max-height: 40vh; max-height: 40vh;
margin: auto; margin: auto;
+47 -41
View File
@@ -69,7 +69,7 @@
<use xlink:href="#clear-pair-devices-icon" /> <use xlink:href="#clear-pair-devices-icon" />
</svg> </svg>
</a> </a>
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a> <a id="cancel-paste-mode" class="button" hidden>Done</a>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center"> <div id="center">
@@ -109,18 +109,17 @@
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div> <div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr> <hr>
<div id="key-input-container"> <div id="key-input-container">
<input id="char0" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input id="char1" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char2" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char3" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char4" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char5" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div> <div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" disabled>Pair</button>
<div class="separator"></div> <button class="button" close>Cancel</button>
<a class="button" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -133,9 +132,9 @@
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Unpair Devices</h2> <h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div> <div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button> <button class="button" type="submit">Unpair Devices</button>
<a class="button" close>Cancel</a> <button class="button" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -145,25 +144,23 @@
<x-dialog id="receive-request-dialog"> <x-dialog id="receive-request-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">PairDrop</h2> <h2 class="center"></h2>
<div class="text-center file-description"> <div class="center column file-description">
<div> <div>
<span id="requesting-peer-display-name"></span> <span class="display-name"></span>
<span>would like to share</span> <span>would like to share</span>
</div> </div>
<div id="file-name" class="row" > <div class="row file-name" >
<span id="file-stem"></span> <span class="file-stem"></span>
<span id="file-extension"></span> <span class="file-extension"></span>
</div> </div>
<div class="row"> <div class="row file-other">
<span id="file-other"></span>
</div> </div>
<div class="row font-body2 file-size"></div>
</div> </div>
<div class="font-body2 text-center file-size"></div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<div class="separator"></div>
<button id="decline-request" class="button" title="ESCAPE">Decline</button> <button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div> </div>
</x-paper> </x-paper>
@@ -173,13 +170,23 @@
<x-dialog id="receive-file-dialog"> <x-dialog id="receive-file-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 id="receive-title" class="center"></h2> <h2 class="center"></h2>
<div class="text-center file-description"></div> <div class="center column file-description">
<div class="font-body2 text-center file-size"></div> <div>
<span class="display-name"></span>
<span>has sent</span>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<a id="share-or-download" class="button" autofocus></a> <button id="share-btn" class="button" autofocus hidden>Share</button>
<div class="separator"></div> <button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button> <button class="button" close>Close</button>
</div> </div>
</x-paper> </x-paper>
@@ -190,16 +197,16 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">PairDrop</h2> <h2 class="text-center">Send Message</h2>
<div class="text-center"> <div class="dialog-subheader text-center">
<span>Send a Message to</span> <span>Send a Message to</span>
<span id="text-send-peer-display-name"></span> <span class="display-name"></span>
</div> </div>
<div class="row-separator"></div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button> <button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<div class="separator"></div> <button class="button" title="ESCAPE" close>Cancel</button>
<a class="button" title="ESCAPE" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -209,16 +216,15 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2>PairDrop - Message Received</h2> <h2 class="text-center">Message Received</h2>
<div id="receive-text-description-container"> <div class="text-center dialog-subheader">
<span id="receive-text-peer-display-name"></span> <span class="display-name"></span>
<span>sent the following message:</span> <span>has sent:</span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="row-reverse"> <div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button id="close" class="button" title="ESCAPE">Close</button> <button id="close" class="button" title="ESCAPE">Close</button>
</div> </div>
</x-paper> </x-paper>
+49 -38
View File
@@ -34,6 +34,7 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
} }
_sendRoomSecrets(roomSecrets) { _sendRoomSecrets(roomSecrets) {
@@ -56,10 +57,17 @@ class ServerConnection {
this.send({ type: 'pair-device-join', roomKey: roomKey }) this.send({ type: 'pair-device-join', roomKey: roomKey })
} }
_setRtcConfig(config) {
window.rtcConfig = config;
}
_onMessage(msg) { _onMessage(msg) {
msg = JSON.parse(msg); msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg); if (msg.type !== 'ping') console.log('WS:', msg);
switch (msg.type) { switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers': case 'peers':
Events.fire('peers', msg); Events.fire('peers', msg);
break; break;
@@ -155,15 +163,17 @@ class ServerConnection {
this._socket.close(); this._socket.close();
this._socket = null; this._socket = null;
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
} }
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...'); Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 5000); this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
_onVisibilityChange() { _onVisibilityChange() {
@@ -324,31 +334,30 @@ class Peer {
this.sendJSON({ type: 'progress', progress: progress }); this.sendJSON({ type: 'progress', progress: progress });
} }
_onMessage(message, logMessage = true) { _onMessage(message) {
if (typeof message !== 'string') { if (typeof message !== 'string') {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
message = JSON.parse(message); const messageJSON = JSON.parse(message);
if (logMessage) console.log('RTC:', message); switch (messageJSON.type) {
switch (message.type) {
case 'request': case 'request':
this._onFilesTransferRequest(message); this._onFilesTransferRequest(messageJSON);
break; break;
case 'header': case 'header':
this._onFilesHeader(message); this._onFilesHeader(messageJSON);
break; break;
case 'partition': case 'partition':
this._onReceivedPartitionEnd(message); this._onReceivedPartitionEnd(messageJSON);
break; break;
case 'partition-received': case 'partition-received':
this._sendNextPartition(); this._sendNextPartition();
break; break;
case 'progress': case 'progress':
this._onDownloadProgress(message.progress); this._onDownloadProgress(messageJSON.progress);
break; break;
case 'files-transfer-response': case 'files-transfer-response':
this._onFileTransferRequestResponded(message); this._onFileTransferRequestResponded(messageJSON);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted();
@@ -357,7 +366,7 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(message); this._onTextReceived(messageJSON);
break; break;
} }
} }
@@ -451,7 +460,7 @@ class Peer {
if (!this._requestAccepted.header.length) { if (!this._requestAccepted.header.length) {
this._busy = false; this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted}); Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = []; this._filesReceived = [];
this._requestAccepted = null; this._requestAccepted = null;
} }
@@ -502,6 +511,7 @@ class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller if (!peerId) return; // we will listen for a caller
this._connect(peerId, true); this._connect(peerId, true);
} }
@@ -519,7 +529,7 @@ class RTCPeer extends Peer {
_openConnection(peerId, isCaller) { _openConnection(peerId, isCaller) {
this._isCaller = isCaller; this._isCaller = isCaller;
this._peerId = peerId; this._peerId = peerId;
this._conn = new RTCPeerConnection(RTCPeer.config); this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
@@ -578,6 +588,13 @@ class RTCPeer extends Peer {
this._channel = channel; this._channel = channel;
} }
_onMessage(message) {
if (typeof message === 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
}
getConnectionHash() { getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n"); const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
@@ -682,6 +699,7 @@ class WSPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = false;
if (!peerId) return; // we will listen for a caller if (!peerId) return; // we will listen for a caller
this._sendSignal(); this._sendSignal();
} }
@@ -700,15 +718,15 @@ class WSPeer extends Peer {
this._server.send(message); this._server.send(message);
} }
_sendSignal() { _sendSignal(connected = false) {
this.sendJSON({type: 'signal'}); this.sendJSON({type: 'signal', connected: connected});
} }
onServerMessage(message) { onServerMessage(message) {
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()}) Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (this._peerId) return; if (message.connected) return;
this._peerId = message.sender.id; this._peerId = message.sender.id;
this._sendSignal(); this._sendSignal(true);
} }
getConnectionHash() { getConnectionHash() {
@@ -730,6 +748,7 @@ class PeersManager {
Events.on('peer-left', e => this._onPeerLeft(e.detail)); Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail)); Events.on('ws-relay', e => this._onWsRelay(e.detail));
} }
@@ -749,7 +768,7 @@ class PeersManager {
_onWsRelay(message) { _onWsRelay(message) {
const messageJSON = JSON.parse(message) const messageJSON = JSON.parse(message)
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk); if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message, false) this.peers[messageJSON.sender.id]._onMessage(message)
} }
_onPeers(msg) { _onPeers(msg) {
@@ -797,15 +816,24 @@ class PeersManager {
} }
_onPeerLeft(msg) { _onPeerLeft(msg) {
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) { if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', msg.peerId) console.log('WSPeer left:', msg.peerId);
Events.fire('peer-disconnected', msg.peerId) Events.fire('peer-disconnected', msg.peerId);
} else if (msg.disconnect === true) { } else if (msg.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately // if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId); Events.fire('peer-disconnected', msg.peerId);
} }
} }
_onWsDisconnected() {
for (const peerId in this.peers) {
console.debug(this.peers[peerId].rtcSupported);
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
Events.fire('peer-disconnected', peerId);
}
}
}
_onPeerDisconnected(peerId) { _onPeerDisconnected(peerId) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
delete this.peers[peerId]; delete this.peers[peerId];
@@ -919,20 +947,3 @@ class Events {
return window.removeEventListener(type, callback, false); return window.removeEventListener(type, callback, false);
} }
} }
RTCPeer.config = {
'sdpSemantics': 'unified-plan',
'iceServers': [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'stun:openrelay.metered.ca:80'
},
{
urls: 'turn:openrelay.metered.ca:443',
username: 'openrelayproject',
credential: 'openrelayproject',
},
]
}
+149 -119
View File
@@ -1,6 +1,5 @@
const $ = query => document.getElementById(query); const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(query); const $$ = query => document.body.querySelector(query);
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
window.isProductionEnvironment = !window.location.host.startsWith('localhost'); window.isProductionEnvironment = !window.location.host.startsWith('localhost');
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent); window.android = /android/i.test(navigator.userAgent);
@@ -28,7 +27,7 @@ class PeersUI {
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {}; this.peers = {};
this.$cancelPasteModeBtn = $('cancel-paste-mode-btn'); this.$cancelPasteModeBtn = $('cancel-paste-mode');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e)); Events.on('dragover', e => this._onDragOver(e));
@@ -474,10 +473,14 @@ class Dialog {
class ReceiveDialog extends Dialog { class ReceiveDialog extends Dialog {
constructor(id) { constructor(id) {
super(id); super(id);
this.$fileDescription = this.$el.querySelector('.file-description');
this.$fileDescriptionNode = this.$el.querySelector('.file-description'); this.$displayName = this.$el.querySelector('.display-name');
this.$fileSizeNode = this.$el.querySelector('.file-size'); this.$fileStem = this.$el.querySelector('.file-stem');
this.$previewBox = this.$el.querySelector('.file-preview') this.$fileExtension = this.$el.querySelector('.file-extension');
this.$fileOther = this.$el.querySelector('.file-other');
this.$fileSize = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview');
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
} }
_formatFileSize(bytes) { _formatFileSize(bytes) {
@@ -493,6 +496,26 @@ class ReceiveDialog extends Dialog {
return bytes + ' Bytes'; return bytes + ' Bytes';
} }
} }
_parseFileData(displayName, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
fileOtherText += imagesOnly ? 'image' : 'file';
} else {
fileOtherText += imagesOnly ? 'images' : 'files';
}
this.$fileOther.innerText = fileOtherText;
}
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
} }
class ReceiveFileDialog extends ReceiveDialog { class ReceiveFileDialog extends ReceiveDialog {
@@ -500,24 +523,25 @@ class ReceiveFileDialog extends ReceiveDialog {
constructor() { constructor() {
super('receive-file-dialog'); super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download'); this.$downloadBtn = this.$el.querySelector('#download-btn');
this.$receiveTitleNode = this.$el.querySelector('#receive-title') this.$shareBtn = this.$el.querySelector('#share-btn');
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request)); Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
this._filesQueue = []; this._filesQueue = [];
} }
_onFilesReceived(sender, files, request) { _onFilesReceived(sender, files, imagesOnly, totalSize) {
this._nextFiles(sender, files, request); const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play(); window.blop.play();
} }
_nextFiles(sender, nextFiles, nextRequest) { _nextFiles() {
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
if (this._busy) return; if (this._busy) return;
this._busy = true; this._busy = true;
const {peerId, files, request} = this._filesQueue.shift(); const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peerId, files, request); this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
} }
_dequeueFile() { _dequeueFile() {
@@ -548,7 +572,6 @@ class ReceiveFileDialog extends ReceiveDialog {
let element = document.createElement(previewElement[mime]); let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file); element.src = URL.createObjectURL(file);
element.controls = true; element.controls = true;
element.classList.add('element-preview');
element.onload = _ => { element.onload = _ => {
this.$previewBox.appendChild(element); this.$previewBox.appendChild(element);
resolve(true) resolve(true)
@@ -559,30 +582,32 @@ class ReceiveFileDialog extends ReceiveDialog {
}); });
} }
async _displayFiles(peerId, files, request) { async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback); this._parseFileData(displayName, files, imagesOnly, totalSize);
let url;
let title;
let filenameDownload;
let descriptor = request.imagesOnly ? "Image" : "File";
let size = this._formatFileSize(request.totalSize);
let description = files[0].name;
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
url = URL.createObjectURL(files[0]) descriptor = imagesOnly ? 'Image' : 'File';
title = `PairDrop - ${descriptor} Received`
filenameDownload = files[0].name;
} else { } else {
title = `PairDrop - ${files.length} ${descriptor}s Received` descriptor = imagesOnly ? 'Images' : 'Files';
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`; }
if(files.length>2) description += "s"; this.$receiveTitle.innerText = `${descriptor} Received`;
if(!shareInsteadOfDownload) { const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
if (canShare) {
this.$shareBtn.removeAttribute('hidden');
this.$shareBtn.onclick = _ => {
navigator.share({files: files})
.catch(err => {
console.error(err);
});
}
}
let downloadZipped = false;
if (files.length > 1) {
downloadZipped = true;
try {
let bytesCompleted = 0; let bytesCompleted = 0;
zipper.createNewZipWriter(); zipper.createNewZipWriter();
for (let i=0; i<files.length; i++) { for (let i=0; i<files.length; i++) {
@@ -590,7 +615,7 @@ class ReceiveFileDialog extends ReceiveDialog {
onprogress: (progress) => { onprogress: (progress) => {
Events.fire('set-progress', { Events.fire('set-progress', {
peerId: peerId, peerId: peerId,
progress: (bytesCompleted + progress) / request.totalSize, progress: (bytesCompleted + progress) / totalSize,
status: 'process' status: 'process'
}) })
} }
@@ -610,49 +635,58 @@ class ReceiveFileDialog extends ReceiveDialog {
let minutes = now.getMinutes().toString(); let minutes = now.getMinutes().toString();
minutes = minutes.length < 2 ? "0" + minutes : minutes; minutes = minutes.length < 2 ? "0" + minutes : minutes;
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`; filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
} catch (e) {
console.error(e);
downloadZipped = false;
} }
} }
this.$receiveTitleNode.textContent = title; this.$downloadBtn.innerText = "Download";
this.$fileDescriptionNode.textContent = description; this.$downloadBtn.onclick = _ => {
this.$fileSizeNode.textContent = size; if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
if (shareInsteadOfDownload) { tmpZipBtn.download = filenameDownload;
this.$shareOrDownloadBtn.innerText = "Share"; tmpZipBtn.href = url;
this.continue = _ => { tmpZipBtn.click();
navigator.share({files: files})
.catch(err => console.error(err));
}
this.continueCallback = _ => this.continue();
} else { } else {
this.$shareOrDownloadBtn.innerText = "Download again"; this._downloadFilesIndividually(files);
this.continue = _ => {
let tmpBtn = document.createElement("a");
tmpBtn.download = filenameDownload;
tmpBtn.href = url;
tmpBtn.click();
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
} }
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
if (!canShare) {
this.$downloadBtn.innerText = "Download again";
}
Events.fire('notify-user', `${descriptor} downloaded successfully`);
this.$downloadBtn.style.pointerEvents = "none";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => { this.createPreviewElement(files[0]).finally(_ => {
document.title = files.length === 1 document.title = files.length === 1
? 'File received - PairDrop' ? 'File received - PairDrop'
: `(${files.length}) Files received - PairDrop`; : `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.continue(); this.show();
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r)); }).catch(r => console.error(r));
} }
_downloadFilesIndividually(files) {
let tmpBtn = document.createElement("a");
for (let i=0; i<files.length; i++) {
tmpBtn.download = files[i].name;
tmpBtn.href = URL.createObjectURL(files[i]);
tmpBtn.click();
}
}
hide() { hide() {
this.$shareOrDownloadBtn.removeAttribute('href'); this.$shareBtn.setAttribute('hidden', '');
this.$shareOrDownloadBtn.removeAttribute('download');
this.$previewBox.innerHTML = ''; this.$previewBox.innerHTML = '';
super.hide(); super.hide();
this._dequeueFile(); this._dequeueFile();
@@ -664,11 +698,6 @@ class ReceiveRequestDialog extends ReceiveDialog {
constructor() { constructor() {
super('receive-request-dialog'); super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name');
this.$fileStemNode = this.$el.querySelector('#file-stem');
this.$fileExtensionNode = this.$el.querySelector('#file-extension');
this.$fileOtherNode = this.$el.querySelector('#file-other');
this.$acceptRequestBtn = this.$el.querySelector('#accept-request'); this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
this.$declineRequestBtn = this.$el.querySelector('#decline-request'); this.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true)); this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
@@ -700,32 +729,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
_showRequestDialog(request, peerId) { _showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
const fileName = request.header[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtensionNode.innerText = fileExtension
if (request.header.length >= 2) {
let fileOtherText = ` and ${request.header.length - 1} other `;
fileOtherText += request.imagesOnly ? 'image' : 'file';
if (request.header.length > 2) fileOtherText += "s";
this.$fileOtherNode.innerText = fileOtherText;
}
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img'); let element = document.createElement('img');
element.src = request.thumbnailDataUrl; element.src = request.thumbnailDataUrl;
element.classList.add('element-preview');
this.$previewBox.appendChild(element) this.$previewBox.appendChild(element)
} }
document.title = 'File Transfer Requested - PairDrop'; this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show(); this.show();
} }
@@ -760,7 +775,7 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn = $('clear-pair-devices'); this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form'); let createJoinForm = this.$el.querySelector('form');
createJoinForm.addEventListener('submit', _ => this._onSubmit()); createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel()) this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e))); this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
@@ -839,7 +854,7 @@ class PairDeviceDialog extends Dialog {
}) })
this.$submitBtn.removeAttribute("disabled"); this.$submitBtn.removeAttribute("disabled");
if (document.activeElement === this.$inputRoomKeyChars[5]) { if (document.activeElement === this.$inputRoomKeyChars[5]) {
this._onSubmit(); this._pairDeviceJoin(this.inputRoomKey);
} }
} }
} }
@@ -889,7 +904,8 @@ class PairDeviceDialog extends Dialog {
return url.href; return url.href;
} }
_onSubmit() { _onSubmit(e) {
e.preventDefault();
this._pairDeviceJoin(this.inputRoomKey); this._pairDeviceJoin(this.inputRoomKey);
} }
@@ -976,14 +992,19 @@ class ClearDevicesDialog extends Dialog {
super('clear-devices-dialog'); super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form'); let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
} }
_onClearPairDevices() { _onClearPairDevices() {
this.show(); this.show();
} }
_onSubmit() { _onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets'); Events.fire('clear-room-secrets');
this.hide(); this.hide();
} }
@@ -994,10 +1015,10 @@ class SendTextDialog extends Dialog {
super('send-text-dialog'); super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input'); this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name'); this.$peerDisplayName = this.$el.querySelector('.display-name');
this.$form = this.$el.querySelector('form'); this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]'); this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send()); this.$form.addEventListener('submit', e => this._onSubmit(e));
this.$text.addEventListener('input', e => this._onChange(e)); this.$text.addEventListener('input', e => this._onChange(e));
Events.on("keydown", e => this._onKeyDown(e)); Events.on("keydown", e => this._onKeyDown(e));
} }
@@ -1039,6 +1060,11 @@ class SendTextDialog extends Dialog {
sel.addRange(range); sel.addRange(range);
} }
_onSubmit(e) {
e.preventDefault();
this._send();
}
_send() { _send() {
Events.fire('send-text', { Events.fire('send-text', {
to: this.correspondingPeerId, to: this.correspondingPeerId,
@@ -1062,7 +1088,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e)); Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name'); this.$displayNameNode = this.$el.querySelector('.display-name');
this._receiveTextQueue = []; this._receiveTextQueue = [];
} }
@@ -1092,18 +1118,23 @@ class ReceiveTextDialog extends Dialog {
} }
_showReceiveTextDialog(text, peerId) { _showReceiveTextDialog(text, peerId) {
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); this.$displayNameNode.innerText = $(peerId).ui._displayName();
if (isURL(text)) { this.$text.innerText = text;
const $a = document.createElement('a'); this.$text.classList.remove('text-center');
$a.href = text;
$a.target = '_blank'; // Beautify text if text is short
$a.textContent = text; if (text.length < 2000) {
this.$text.innerHTML = ''; // replace urls with actual links
this.$text.appendChild($a); this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
} else { return `<a href="${url}" target="_blank">${url}</a>`;
this.$text.textContent = text; });
if (!/\s/.test(text)) {
this.$text.classList.add('text-center');
} }
}
this._setDocumentTitleMessages(); this._setDocumentTitleMessages();
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
@@ -1113,7 +1144,7 @@ class ReceiveTextDialog extends Dialog {
_setDocumentTitleMessages() { _setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop' ? 'Message Received - PairDrop'
: `(${this._receiveTextQueue.length + 1}) Messages Received - PairDrop`; : `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
} }
async _onCopy() { async _onCopy() {
@@ -1188,7 +1219,7 @@ class Base64ZipDialog extends Dialog {
} }
_setPasteBtnToProcessing() { _setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none"; this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = "Processing...";
} }
@@ -1333,7 +1364,7 @@ class Notifications {
_messageNotification(message, peerId) { _messageNotification(message, peerId) {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName(); const peerDisplayName = $(peerId).ui._displayName();
if (isURL(message)) { if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
this._bind(notification, _ => window.open(message, '_blank', null, true)); this._bind(notification, _ => window.open(message, '_blank', null, true));
} else { } else {
@@ -1394,7 +1425,7 @@ class NetworkStatusUI {
constructor() { constructor() {
Events.on('offline', _ => this._showOfflineMessage()); Events.on('offline', _ => this._showOfflineMessage());
Events.on('online', _ => this._showOnlineMessage()); Events.on('online', _ => this._showOnlineMessage());
Events.on('ws-connected', _ => this._showOnlineMessage()); Events.on('ws-connected', _ => this._onWsConnected());
Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-disconnected', _ => this._onWsDisconnected());
if (!navigator.onLine) this._showOfflineMessage(); if (!navigator.onLine) this._showOfflineMessage();
} }
@@ -1405,17 +1436,16 @@ class NetworkStatusUI {
} }
_showOnlineMessage() { _showOnlineMessage() {
window.animateBackground(true);
if (!this.firstConnect) {
this.firstConnect = true;
return;
}
Events.fire('notify-user', 'You are back online'); Events.fire('notify-user', 'You are back online');
window.animateBackground(true);
}
_onWsConnected() {
window.animateBackground(true);
} }
_onWsDisconnected() { _onWsDisconnected() {
window.animateBackground(false); window.animateBackground(false);
if (!this.firstConnect) this.firstConnect = true;
} }
} }
@@ -1753,8 +1783,8 @@ Events.on('load', () => {
let x0, y0, w, h, dw, offset; let x0, y0, w, h, dw, offset;
function init() { function init() {
w = window.innerWidth; w = document.documentElement.clientWidth;
h = window.innerHeight; h = document.documentElement.clientHeight;
c.width = w; c.width = w;
c.height = h; c.height = h;
offset = $$('footer').offsetHeight - 32; offset = $$('footer').offsetHeight - 32;
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.2.0'; const cacheVersion = 'v1.3.0';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',
+56 -63
View File
@@ -590,7 +590,7 @@ x-dialog x-background {
z-index: 10; z-index: 10;
transition: opacity 300ms; transition: opacity 300ms;
will-change: opacity; will-change: opacity;
padding: 35px; padding: 15px;
overflow: overlay; overflow: overlay;
} }
@@ -601,19 +601,20 @@ x-dialog x-paper {
padding: 16px 24px; padding: 16px 24px;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
transition: transform 300ms; transition: transform 300ms;
will-change: transform; will-change: transform;
} }
#pair-device-dialog x-paper { #pair-device-dialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
width: calc(100vw - 20px);
height: 625px;
} }
x-dialog:not([show]) { x-dialog:not([show]) {
@@ -628,12 +629,6 @@ x-dialog:not([show]) x-background {
opacity: 0; opacity: 0;
} }
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a { x-dialog a {
color: var(--primary-color); color: var(--primary-color);
@@ -672,7 +667,7 @@ x-dialog .font-subheading {
} }
#key-input-container>input:nth-of-type(4) { #key-input-container>input:nth-of-type(4) {
margin-left: 18px; margin-left: 5%;
} }
#room-key { #room-key {
@@ -684,16 +679,11 @@ x-dialog .font-subheading {
} }
#room-key-qr-code { #room-key-qr-code {
padding: inherit; margin: 16px;
margin: auto;
width: 150px;
height: 150px;
} }
#pair-device-dialog hr { #pair-device-dialog hr {
margin-top: 40px; margin: 40px -24px;
margin-bottom: 40px;
width: 100%;
} }
#pair-device-dialog x-background { #pair-device-dialog x-background {
@@ -707,29 +697,24 @@ x-dialog .row {
margin-bottom: 8px; margin-bottom: 8px;
} }
x-dialog h2 { /* button row*/
margin-top: 1rem; x-paper > div:last-child {
} margin: auto -24px -15px;
#receive-request-dialog h2,
#receive-file-dialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px 0;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
height: 50px;
} }
.separator { x-paper > div:last-child > .button {
border: solid 1.25px var(--border-color); height: 100%;
margin-bottom: -16px; width: 50%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
} }
.file-description { .file-description {
word-break: break-word; margin-bottom: 25px;
width: 80%;
margin: auto;
} }
.file-description .row { .file-description .row {
@@ -741,26 +726,26 @@ x-dialog .row-reverse {
word-break: normal; word-break: normal;
} }
#file-name { .file-name {
font-style: italic; font-style: italic;
max-width: 100%;
} }
#file-stem { .file-stem {
max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-all; white-space: nowrap;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
} }
/* Send Text Dialog */ /* Send Text Dialog */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input { #text-input {
min-height: 120px; min-height: 200px;
margin: 14px auto;
} }
/* Receive Text Dialog */ /* Receive Text Dialog */
@@ -768,14 +753,14 @@ x-dialog .row-reverse {
#receive-text-dialog #text { #receive-text-dialog #text {
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: calc(100vh - 393px);
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-user-select: all; -webkit-user-select: all;
-moz-user-select: all; -moz-user-select: all;
user-select: all; user-select: all;
white-space: pre-wrap; white-space: pre-wrap;
margin-top:36px; padding: 15px 0;
} }
#receive-text-dialog #text a { #receive-text-dialog #text a {
@@ -794,11 +779,7 @@ x-dialog .row-reverse {
.row-separator { .row-separator {
border-bottom: solid 2.5px var(--border-color); border-bottom: solid 2.5px var(--border-color);
margin: auto -25px; margin: auto -24px;
}
#receive-text-description-container {
margin-bottom: 25px;
} }
#base64-paste-btn { #base64-paste-btn {
@@ -826,7 +807,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0; padding: 2px 16px 0;
box-sizing: border-box; box-sizing: border-box;
min-height: 36px; min-height: 36px;
min-width: 100px;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
font-weight: 700; font-weight: 700;
@@ -837,6 +817,7 @@ x-dialog .row-reverse {
user-select: none; user-select: none;
background: inherit; background: inherit;
color: var(--primary-color); color: var(--primary-color);
overflow: hidden;
} }
.button[disabled] { .button[disabled] {
@@ -874,7 +855,7 @@ x-dialog .row-reverse {
opacity: 0.1; opacity: 0.1;
} }
#cancel-paste-mode-btn { #cancel-paste-mode {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -901,7 +882,6 @@ button::-moz-focus-inner {
/* Icon Button */ /* Icon Button */
.icon-button { .icon-button {
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -911,10 +891,7 @@ button::-moz-focus-inner {
border-radius: 50%; border-radius: 50%;
} }
/* Text Input */ /* Text Input */
.textarea { .textarea {
box-sizing: border-box; box-sizing: border-box;
border: none; border: none;
@@ -928,9 +905,8 @@ button::-moz-focus-inner {
display: block; display: block;
overflow: auto; overflow: auto;
resize: none; resize: none;
min-height: 40px;
line-height: 16px; line-height: 16px;
max-height: 300px; max-height: calc(100vh - 254px);
white-space: pre; white-space: pre;
} }
@@ -989,6 +965,13 @@ button::-moz-focus-inner {
margin: 8px 8px -16px; margin: 8px 8px -16px;
} }
#about section {
flex-grow: 1;
}
#about header {
align-self: end;
}
/* Loading Indicator */ /* Loading Indicator */
@@ -1120,6 +1103,14 @@ x-peers:empty~x-instructions {
} }
/* Responsive Styles */ /* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
padding: 15px;
}
x-paper > div:last-child {
margin: auto -15px -15px;
}
}
@media screen and (min-height: 800px) { @media screen and (min-height: 800px) {
#websocket-fallback { #websocket-fallback {
@@ -1192,7 +1183,9 @@ x-dialog x-paper {
display: none; display: none;
} }
.element-preview { .file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%; max-width: 100%;
max-height: 40vh; max-height: 40vh;
margin: auto; margin: auto;
+16
View File
@@ -0,0 +1,16 @@
{
"sdpSemantics": "unified-plan",
"iceServers": [
{
"urls": "stun:stun.l.google.com:19302"
},
{
"urls": "stun:openrelay.metered.ca:80"
},
{
"urls": "turn:openrelay.metered.ca:443",
"username": "openrelayproject",
"credential": "openrelayproject"
}
]
}