Compare commits

..

52 Commits

Author SHA1 Message Date
schlagmichdoch fea15d3ee1 increase version to v1.4.5 2023-03-13 00:05:57 +01:00
schlagmichdoch 028752a809 fixes #76. 'File received' dialog not showing on iOS when big videos are sent. 2023-03-13 00:04:48 +01:00
schlagmichdoch 1093f4d246 log error onicecandidateerror 2023-03-10 22:21:19 +01:00
schlagmichdoch 7ddd600b0c fix display name hidden on Firefox for Android 2023-03-10 20:01:59 +01:00
schlagmichdoch 715356aafb Fix AirDrop typo 2023-03-08 11:35:37 +01:00
schlagmichdoch 490e4db380 Add information about specifying TURN servers 2023-03-07 18:25:25 +01:00
schlagmichdoch 11a988e550 increase version to v1.4.4 2023-03-06 16:05:58 +01:00
schlagmichdoch ff8f28660a prevent buttons from submitting form by adding type="button" 2023-03-06 16:03:34 +01:00
schlagmichdoch 5fc8e85f75 increase version to 1.4.3 2023-03-06 15:40:09 +01:00
schlagmichdoch 5eeaae01fe add connection hash to title of display-name of receive dialogs 2023-03-06 15:39:24 +01:00
schlagmichdoch 660e523263 prevent sending of displayName if RTCPeer is not connected 2023-03-06 15:33:22 +01:00
schlagmichdoch cdfbc7a2df add missing removal of event listener to ws fallback ui.js 2023-03-06 15:32:58 +01:00
schlagmichdoch c9dca7e083 fix download notification and add request notification 2023-03-06 15:32:42 +01:00
schlagmichdoch 79af04d95a increase version to v1.4.2 2023-03-06 12:31:44 +01:00
schlagmichdoch 954e9c7c3a Merge pull request #65 from schlagmichdoch/pairdrop_cli_add_firefox_fallback
pairdrop-cli: add fallback if navigator.clipboard.readText() is not available
2023-03-06 12:25:54 +01:00
schlagmichdoch c0d504f6a8 remove base64 event listeners manually on hide instead of once: true 2023-03-06 12:20:30 +01:00
schlagmichdoch 36e152dc7c add { once: true } to deactivate-paste-mode event listener 2023-03-06 11:59:56 +01:00
schlagmichdoch fdf024f378 pairdrop-cli: add fallback if navigator.clipboard.readText() is not available 2023-03-06 11:56:17 +01:00
schlagmichdoch 9f2e4c5f8f fix displayName sometimes not exchanged on reload 2023-03-06 11:24:19 +01:00
schlagmichdoch 8e219914ec Merge pull request #66 from schlagmichdoch/dependabot/npm_and_yarn/ua-parser-js-1.0.34
Bump ua-parser-js from 1.0.33 to 1.0.34
2023-03-06 10:53:35 +01:00
dependabot[bot] d1273ef9cc Bump ua-parser-js from 1.0.33 to 1.0.34
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.33 to 1.0.34.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/1.0.34/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/1.0.33...1.0.34)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 05:12:00 +00:00
schlagmichdoch 27ac7786d0 increase version to v1.4.1 2023-03-06 03:48:23 +01:00
schlagmichdoch edf2ab5eb3 revert some changes to regain stability 2023-03-06 03:47:24 +01:00
schlagmichdoch c3863a9dd3 increase version to v1.4.0 2023-03-06 02:19:41 +01:00
schlagmichdoch 5934e94761 edit some styling of the display-name edit field 2023-03-06 00:40:53 +01:00
schlagmichdoch 1bc23dc4b3 fix read rtcConfig.json must be parsed as JSON.. 2023-03-06 00:16:33 +01:00
schlagmichdoch cc78b34d2e Revert making peerId ephemeral to prevent duplication of shown peers on reconnect. Implement peerIdHash to prevent rogue users from overtaking peerIds 2023-03-06 00:07:21 +01:00
schlagmichdoch f34f5bd4b2 tidy up code, add tooltip to device name and change color and bg-color of device-name 2023-03-06 00:07:21 +01:00
schlagmichdoch b2f6a75c99 Merge pull request #60 from ChaosExAnima/master
Updates CLI to work with OSX base64
2023-03-05 00:43:37 +01:00
Echo 82138c06f3 Updates CLI to work with OSX base64 2023-03-04 15:53:13 -05:00
schlagmichdoch ee820ed6e0 Merge pull request #57 from schlagmichdoch/enable_renaming
New feature: You can now change your display name
2023-03-04 20:53:41 +01:00
schlagmichdoch b7e7fd1b68 Merge branch 'master' into enable_renaming 2023-03-04 20:52:10 +01:00
schlagmichdoch 96ed0e53b1 apply styling to clarify that the display-name is editable 2023-03-04 20:50:52 +01:00
schlagmichdoch 77b76a3b8d reduce reconnect timers to 1s 2023-03-04 15:46:26 +01:00
schlagmichdoch e37f9bd9fb fix display name offset in styles.css 2023-03-04 15:44:42 +01:00
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 451173caac Add possibility to change the display name to the README.md 2023-03-03 19:10:24 +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 460e8ec79c change cursor to clarify that the display name is editable 2023-03-03 17:43:03 +01:00
schlagmichdoch 002b31a113 merge master into branch 2023-03-03 17:40:10 +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 1eb53498b1 add localStorage fallback to fix renaming on private tabs and fix Firefox inserting linebreaks into edited divs 2023-03-02 15:06:22 +01:00
schlagmichdoch de76da52fe merge master into branch 2023-03-01 21:55:50 +01:00
schlagmichdoch d56ee87437 - Enable renaming of own display name permanently via UI
- Make peerId completely ephemeral
- Stabilize RTCConnection by closing connections cleanly
2023-03-01 21:38:36 +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
18 changed files with 1102 additions and 405 deletions
+8 -5
View File
@@ -6,7 +6,7 @@
<h1>PairDrop</h1> <h1>PairDrop</h1>
<p> <p>
Local file sharing in your browser. Inspired by Apple's Airdrop. Local file sharing in your browser. Inspired by Apple's AirDrop.
<br /> <br />
<a href="https://pairdrop.net"><strong>Explore »</strong></a> <a href="https://pairdrop.net"><strong>Explore »</strong></a>
<br /> <br />
@@ -42,9 +42,9 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* Paired devices outside your local network that are behind a NAT are connected automatically via [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/) * Paired devices outside your local network that are behind a NAT are connected automatically via [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/)
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560) ### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
* Files are transferred only after a request is accepted first. On transfer completion they are downloaded automatically if possible. * Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
* Multiple files are downloaded as ZIP file * Multiple files are downloaded as a ZIP file
* On iOS and Android the devices share menu is opened instead of downloading the files * On iOS and Android, in addition to downloading, files can be shared or saved to the gallery via the Share menu.
* Multiple files are transferred at once with an overall progress indicator * Multiple files are transferred at once with an overall progress indicator
### Send Files or Text Directly From Share Menu, Context Menu or CLI ### Send Files or Text Directly From Share Menu, Context Menu or CLI
@@ -54,7 +54,8 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) * [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
### Other changes ### Other changes
* [Paste Mode](https://github.com/RobinLinus/snapdrop/pull/534) * Change your display name permanently to easily differentiate your devices
* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413) * [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
* Warn user before PairDrop is closed on file transfer * Warn user before PairDrop is closed on file transfer
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101)) * Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
@@ -63,6 +64,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">
@@ -78,6 +80,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App) * [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) * [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
* [zip.js](https://gildas-lormeau.github.io/zip.js/) * [zip.js](https://gildas-lormeau.github.io/zip.js/)
* [cyrb53](https://github.com/bryc) super fast hash function
Have any questions? Read our [FAQ](/docs/faq.md). Have any questions? Read our [FAQ](/docs/faq.md).
+5 -2
View File
@@ -58,8 +58,11 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection. Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
### Transferring many files with paired devices takes too long ### Transferring many files with paired devices takes too long
Naturally, if traffic needs to be routed through the turn server transfer speed decreases. Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases.
As a workaround you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster.
As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance.
Alternatively, you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster as no TURN server is needed.
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11) - [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) - [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)
+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).
+91 -17
View File
@@ -2,6 +2,12 @@ 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 WebSocket = require('ws');
const fs = require('fs');
const parser = require('ua-parser-js');
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
const express = require('express');
const RateLimit = require('express-rate-limit');
const http = require('http');
// Handle SIGINT // Handle SIGINT
process.on('SIGINT', () => { process.on('SIGINT', () => {
@@ -50,9 +56,24 @@ if (process.argv.includes('--auto-restart')) {
); );
} }
const express = require('express'); const rtcConfig = process.env.RTC_CONFIG
const RateLimit = require('express-rate-limit'); ? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
const http = require('http'); : {
"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 app = express(); const app = express();
@@ -94,9 +115,6 @@ if (process.argv.includes('--localhost-only')) {
server.listen(port); server.listen(port);
} }
const parser = require('ua-parser-js');
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
class PairDropServer { class PairDropServer {
constructor() { constructor() {
@@ -113,6 +131,10 @@ class PairDropServer {
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); this._joinRoom(peer);
// send displayName // send displayName
@@ -121,7 +143,8 @@ class PairDropServer {
message: { message: {
displayName: peer.name.displayName, displayName: peer.name.displayName,
deviceName: peer.name.deviceName, deviceName: peer.name.deviceName,
peerId: peer.id peerId: peer.id,
peerIdHash: peer.id.hashCode128BitSalted()
} }
}); });
} }
@@ -345,6 +368,10 @@ 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];
@@ -526,9 +553,11 @@ class Peer {
} }
_setPeerId(request) { _setPeerId(request) {
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id"); const searchParams = new URL(request.url, "http://server").searchParams;
if (peer_id && Peer.isValidUuid(peer_id)) { let peerId = searchParams.get("peer_id");
this.id = peer_id; let peerIdHash = searchParams.get("peer_id_hash");
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
this.id = peerId;
} else { } else {
this.id = crypto.randomUUID(); this.id = crypto.randomUUID();
} }
@@ -587,6 +616,10 @@ class Peer {
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid); return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
} }
isPeerIdHashValid(peerId, peerIdHash) {
return peerIdHash === peerId.hashCode128BitSalted();
}
addRoomSecret(roomSecret) { addRoomSecret(roomSecret) {
if (!(roomSecret in this.roomSecrets)) { if (!(roomSecret in this.roomSecrets)) {
this.roomSecrets.push(roomSecret); this.roomSecrets.push(roomSecret);
@@ -602,14 +635,55 @@ class Peer {
Object.defineProperty(String.prototype, 'hashCode', { Object.defineProperty(String.prototype, 'hashCode', {
value: function() { value: function() {
var hash = 0, i, chr; return cyrb53(this);
for (i = 0; i < this.length; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
} }
}); });
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
value: function() {
return hasher.hashCode128BitSalted(this);
}
});
const hasher = (() => {
let seeds;
return {
hashCode128BitSalted(str) {
if (!seeds) {
// seeds are created on first call to salt hash.
seeds = [4];
for (let i=0; i<4; i++) {
const randomBuffer = new Uint32Array(1);
crypto.webcrypto.getRandomValues(randomBuffer);
seeds[i] = randomBuffer[0];
}
}
let hashCode = "";
for (let i=0; i<4; i++) {
hashCode += cyrb53(str, seeds[i]);
}
return hashCode;
}
}
})()
/*
cyrb53 (c) 2018 bryc (github.com/bryc)
A fast and simple hash function with decent collision resistance.
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
Public domain. Attribution appreciated.
*/
const cyrb53 = function(str, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
new PairDropServer(); new PairDropServer();
+9 -9
View File
@@ -1,17 +1,17 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.1", "version": "1.4.5",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.1", "version": "1.4.5",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.34",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.12.1" "ws": "^8.12.1"
}, },
@@ -583,9 +583,9 @@
} }
}, },
"node_modules/ua-parser-js": { "node_modules/ua-parser-js": {
"version": "1.0.33", "version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", "integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -1070,9 +1070,9 @@
} }
}, },
"ua-parser-js": { "ua-parser-js": {
"version": "1.0.33", "version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==" "integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
}, },
"unique-names-generator": { "unique-names-generator": {
"version": "4.7.1", "version": "4.7.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.2.1", "version": "1.4.5",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.34",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.12.1" "ws": "^8.12.1"
}, },
+10 -2
View File
@@ -106,7 +106,11 @@ sendFiles()
zip -q -b /tmp/ -r "$zipPath" "$path" zip -q -b /tmp/ -r "$zipPath" "$path"
zip -q -b /tmp/ "$zipPathTemp" "$zipPath" zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
hash=$(base64 -w 0 "$zipPathTemp") if [[ $OS == "Mac" ]];then
hash=$(base64 -i "$zipPathTemp")
else
hash=$(base64 -w 0 "$zipPathTemp")
fi
# remove temporary temp file # remove temporary temp file
rm "$zipPathTemp" rm "$zipPathTemp"
@@ -116,7 +120,11 @@ sendFiles()
# Create zip file temporarily to send file # Create zip file temporarily to send file
zip -q -b /tmp/ "$zipPath" "$path" zip -q -b /tmp/ "$zipPath" "$path"
hash=$(base64 -w 0 "$zipPath") if [[ $OS == "Mac" ]];then
hash=$(base64 -i "$zipPath")
else
hash=$(base64 -w 0 "$zipPath")
fi
fi fi
# remove temporary temp file # remove temporary temp file
+23 -12
View File
@@ -59,7 +59,7 @@
<use xlink:href="#homescreen" /> <use xlink:href="#homescreen" />
</svg> </svg>
</a> </a>
<a id="pair-device" class="icon-button" title="Pair Device" > <a id="pair-device" class="icon-button" title="Pair Device" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#pair-device-icon" /> <use xlink:href="#pair-device-icon" />
</svg> </svg>
@@ -89,7 +89,13 @@
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
<div id="display-name" placeholder="&nbsp;"></div> <div>
<span>You are known as:</span>
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" />
</svg>
</div>
<div class="font-body2"> <div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span> You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span> <span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
@@ -116,7 +122,7 @@
<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="center row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" disabled>Pair</button>
<button class="button" close>Cancel</button> <button class="button" type="button" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -131,7 +137,7 @@
<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="center row-reverse"> <div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button> <button class="button" type="submit">Unpair Devices</button>
<button class="button" close>Cancel</button> <button class="button" type="button" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -203,7 +209,7 @@
<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="center 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>
<button class="button" title="ESCAPE" close>Cancel</button> <button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -232,6 +238,7 @@
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button> <button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button> <button class="button center" close>Close</button>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -242,14 +249,14 @@
</div> </div>
<!-- About Page --> <!-- About Page -->
<x-about id="about" class="full center column"> <x-about id="about" class="full center column">
<header class="row-reverse fade-in">
<a href="#" class="close icon-button">
<svg class="icon">
<use xlink:href="#close-icon" />
</svg>
</a>
</header>
<section class="center column fade-in"> <section class="center column fade-in">
<header class="row-reverse">
<a href="#" class="close icon-button">
<svg class="icon">
<use xlink:href="#close-icon" />
</svg>
</a>
</header>
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
@@ -332,6 +339,10 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/> <path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
</symbol> </symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
</symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/util.js"></script> <script src="scripts/util.js"></script>
+96 -66
View File
@@ -21,10 +21,10 @@ class ServerConnection {
Events.on('online', _ => this._connect()); Events.on('online', _ => this._connect());
} }
async _connect() { _connect() {
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return; if (this._isConnected() || this._isConnecting()) return;
const ws = new WebSocket(await this._endpoint()); const ws = new WebSocket(this._endpoint());
ws.binaryType = 'arraybuffer'; ws.binaryType = 'arraybuffer';
ws.onopen = _ => this._onOpen(); ws.onopen = _ => this._onOpen();
ws.onmessage = e => this._onMessage(e.data); ws.onmessage = e => this._onMessage(e.data);
@@ -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) {
@@ -52,16 +53,23 @@ class ServerConnection {
_onPairDeviceJoin(roomKey) { _onPairDeviceJoin(roomKey) {
if (!this._isConnected()) { if (!this._isConnected()) {
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000); setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
return; return;
} }
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;
@@ -110,34 +118,24 @@ class ServerConnection {
_onDisplayName(msg) { _onDisplayName(msg) {
sessionStorage.setItem("peerId", msg.message.peerId); sessionStorage.setItem("peerId", msg.message.peerId);
PersistentStorage.get('peerId').then(peerId => { sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
if (!peerId) {
// save peerId to indexedDB to retrieve after PWA is installed
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
console.log(`peerId saved to indexedDB: ${peerId}`);
});
}
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
Events.fire('display-name', msg); Events.fire('display-name', msg);
} }
async _endpoint() { _endpoint() {
// hack to detect if deployment or development environment // hack to detect if deployment or development environment
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws'; const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback'; const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc); let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
const peerId = await this._peerId(); const peerId = sessionStorage.getItem("peerId");
if (peerId) ws_url.searchParams.append('peer_id', peerId) const peerIdHash = sessionStorage.getItem("peerIdHash");
if (peerId && peerIdHash) {
ws_url.searchParams.append('peer_id', peerId);
ws_url.searchParams.append('peer_id_hash', peerIdHash);
}
return ws_url.toString(); return ws_url.toString();
} }
async _peerId() {
// make peerId persistent when pwa is installed
return window.matchMedia('(display-mode: minimal-ui)').matches
? await PersistentStorage.get('peerId')
: sessionStorage.getItem("peerId");
}
_disconnect() { _disconnect() {
this.send({ type: 'disconnect' }); this.send({ type: 'disconnect' });
if (this._socket) { if (this._socket) {
@@ -145,15 +143,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(), 1000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
_onVisibilityChange() { _onVisibilityChange() {
@@ -194,6 +194,10 @@ class Peer {
this._send(JSON.stringify(message)); this._send(JSON.stringify(message));
} }
sendDisplayName(displayName) {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) { async createHeader(file) {
return { return {
name: file.name, name: file.name,
@@ -319,25 +323,25 @@ class Peer {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
message = JSON.parse(message); const messageJSON = JSON.parse(message);
switch (message.type) { switch (messageJSON.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();
@@ -346,7 +350,10 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(message); this._onTextReceived(messageJSON);
break;
case 'display-name-changed':
this._onDisplayNameChanged(messageJSON);
break; break;
} }
} }
@@ -485,6 +492,12 @@ class Peer {
Events.fire('text-received', { text: escaped, peerId: this._peerId }); Events.fire('text-received', { text: escaped, peerId: this._peerId });
this.sendJSON({ type: 'message-transfer-complete' }); this.sendJSON({ type: 'message-transfer-complete' });
} }
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
}
} }
class RTCPeer extends Peer { class RTCPeer extends Peer {
@@ -509,8 +522,9 @@ 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.onicecandidateerror = e => this._onError(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);
} }
@@ -558,20 +572,19 @@ class RTCPeer extends Peer {
_onChannelOpened(event) { _onChannelOpened(event) {
console.log('RTC: channel opened with', this._peerId); console.log('RTC: channel opened with', this._peerId);
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
const channel = event.channel || event.target; const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
channel.onclose = _ => this._onChannelClosed(); channel.onclose = _ => this._onChannelClosed();
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._closeChannel());
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide());
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
} }
_onMessage(message) { _onMessage(message) {
if (typeof message === 'string') { if (typeof message === 'string') {
message = JSON.parse(message); console.log('RTC:', JSON.parse(message));
console.log('RTC:', message);
} }
super._onMessage(message); super._onMessage(message);
} }
@@ -609,10 +622,16 @@ class RTCPeer extends Peer {
} }
} }
_closeChannel() { _onPageHide() {
if (this._channel) this._channel.onclose = null; this._disconnect();
if (this._conn) this._conn.close(); }
this._conn = null;
_disconnect() {
if (this._conn && this._channel) {
this._channel.onclose = null;
this._channel.close();
}
Events.fire('peer-disconnected', this._peerId);
} }
_onChannelClosed() { _onChannelClosed() {
@@ -626,9 +645,11 @@ class RTCPeer extends Peer {
console.log('RTC: state changed:', this._conn.connectionState); console.log('RTC: state changed:', this._conn.connectionState);
switch (this._conn.connectionState) { switch (this._conn.connectionState) {
case 'disconnected': case 'disconnected':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection disconnected'); this._onError('rtc connection disconnected');
break; break;
case 'failed': case 'failed':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection failed'); this._onError('rtc connection failed');
break; break;
} }
@@ -674,6 +695,11 @@ class RTCPeer extends Peer {
_isConnecting() { _isConnecting() {
return this._channel && this._channel.readyState === 'connecting'; return this._channel && this._channel.readyState === 'connecting';
} }
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
}
} }
class PeersManager { class PeersManager {
@@ -687,8 +713,12 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail)); Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail)); Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
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('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
} }
_onMessage(message) { _onMessage(message) {
@@ -712,10 +742,6 @@ class PeersManager {
}) })
} }
sendTo(peerId, message) {
this.peers[peerId].send(message);
}
_onRespondToFileTransferRequest(detail) { _onRespondToFileTransferRequest(detail) {
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
} }
@@ -747,6 +773,10 @@ class PeersManager {
} }
} }
_onPeerConnected(peerId) {
this._notifyPeerDisplayNameChanged(peerId);
}
_onPeerDisconnected(peerId) { _onPeerDisconnected(peerId) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
delete this.peers[peerId]; delete this.peers[peerId];
@@ -764,6 +794,23 @@ class PeersManager {
} }
} }
} }
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
this._notifyPeerDisplayNameChanged(peerId);
}
}
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer) return;
this.peers[peerId].sendDisplayName(this._displayName);
}
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
}
} }
class FileChunker { class FileChunker {
@@ -852,28 +899,11 @@ class Events {
window.dispatchEvent(new CustomEvent(type, { detail: detail })); window.dispatchEvent(new CustomEvent(type, { detail: detail }));
} }
static on(type, callback) { static on(type, callback, options = false) {
return window.addEventListener(type, callback, false); return window.addEventListener(type, callback, options);
} }
static off(type, callback) { static off(type, callback, options = false) {
return window.removeEventListener(type, callback, false); return window.removeEventListener(type, callback, options);
} }
} }
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',
},
]
}
+248 -91
View File
@@ -9,9 +9,8 @@ window.pasteMode.activated = false;
// set display name // set display name
Events.on('display-name', e => { Events.on('display-name', e => {
const me = e.detail.message; const me = e.detail.message;
const $displayName = $('display-name') const $displayName = $('display-name');
$displayName.textContent = 'You are known as ' + me.displayName; $displayName.setAttribute('placeholder', me.displayName);
$displayName.title = me.deviceName;
}); });
class PeersUI { class PeersUI {
@@ -43,6 +42,80 @@ class PeersUI {
Events.on('peer-added', _ => this.evaluateOverflowing()); Events.on('peer-added', _ => this.evaluateOverflowing());
Events.on('bg-resize', _ => this.evaluateOverflowing()); Events.on('bg-resize', _ => this.evaluateOverflowing());
this.$displayName = $('display-name');
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
// Load saved display name on page load
this._getSavedDisplayName().then(displayName => {
console.log("Retrieved edited display name:", displayName)
if (displayName) Events.fire('self-display-name-changed', displayName);
});
}
_insertDisplayName(displayName) {
this.$displayName.textContent = displayName;
}
_onKeyDownDisplayName(e) {
if (e.key === "Enter" || e.key === "Escape") {
e.preventDefault();
e.target.blur();
}
}
_onKeyUpDisplayName(e) {
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
}
async _saveDisplayName(newDisplayName) {
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
const savedDisplayName = await this._getSavedDisplayName();
if (newDisplayName === savedDisplayName) return;
if (newDisplayName) {
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.');
}).catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', 'Device name is changed only for this session.');
}).finally(_ => {
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
});
} else {
PersistentStorage.delete('editedDisplayName').catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
}).finally(_ => {
Events.fire('notify-user', 'Device name is randomly generated again.');
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
}
}
_getSavedDisplayName() {
return new Promise((resolve) => {
PersistentStorage.get('editedDisplayName')
.then(displayName => resolve(displayName ?? ""))
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
});
}
_changePeerDisplayName(peerId, displayName) {
this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
} }
_onKeyDown(e) { _onKeyDown(e) {
@@ -168,7 +241,7 @@ class PeersUI {
const _callback = (e) => this._sendClipboardData(e, files, text); const _callback = (e) => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback); Events.on('paste-pointerdown', _callback);
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback)); Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
this.$cancelPasteModeBtn.removeAttribute('hidden'); this.$cancelPasteModeBtn.removeAttribute('hidden');
@@ -227,7 +300,8 @@ class PeerUI {
constructor(peer, connectionHash) { constructor(peer, connectionHash) {
this._peer = peer; this._peer = peer;
this._connectionHash = connectionHash; this._connectionHash =
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
this._initDom(); this._initDom();
this._bindListeners(); this._bindListeners();
@@ -272,8 +346,7 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName(); this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent = this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
} }
_initDom() { _initDom() {
@@ -496,7 +569,7 @@ class ReceiveDialog extends Dialog {
} }
} }
_parseFileData(displayName, files, imagesOnly, totalSize) { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) { if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `; let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) { if (files.length === 2) {
@@ -513,6 +586,7 @@ class ReceiveDialog extends Dialog {
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension; this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName; this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$fileSize.innerText = this._formatFileSize(totalSize); this.$fileSize.innerText = this._formatFileSize(totalSize);
} }
} }
@@ -530,8 +604,9 @@ class ReceiveFileDialog extends ReceiveDialog {
} }
_onFilesReceived(sender, files, imagesOnly, totalSize) { _onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName() const displayName = $(sender).ui._displayName();
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); const connectionHash = $(sender).ui._connectionHash;
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles(); this._nextFiles();
window.blop.play(); window.blop.play();
} }
@@ -539,8 +614,8 @@ class ReceiveFileDialog extends ReceiveDialog {
_nextFiles() { _nextFiles() {
if (this._busy) return; if (this._busy) return;
this._busy = true; this._busy = true;
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift(); const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize); this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
} }
_dequeueFile() { _dequeueFile() {
@@ -557,32 +632,40 @@ class ReceiveFileDialog extends ReceiveDialog {
createPreviewElement(file) { createPreviewElement(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let mime = file.type.split('/')[0] try {
let previewElement = { let mime = file.type.split('/')[0]
image: 'img', let previewElement = {
audio: 'audio', image: 'img',
video: 'video' audio: 'audio',
} video: 'video'
}
if (Object.keys(previewElement).indexOf(mime) === -1) { if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false); resolve(false);
} else { } else {
console.log('the file is able to preview'); let element = document.createElement(previewElement[mime]);
let element = document.createElement(previewElement[mime]); element.controls = true;
element.src = URL.createObjectURL(file); element.onload = _ => {
element.controls = true; this.$previewBox.appendChild(element);
element.onload = _ => { resolve(true);
this.$previewBox.appendChild(element); };
resolve(true) element.onloadeddata = _ => {
}; this.$previewBox.appendChild(element);
element.addEventListener('loadeddata', _ => resolve(true)); resolve(true);
element.onerror = _ => reject(`${mime} preview could not be loaded from type ${file.type}`); };
element.onerror = _ => {
reject(`${mime} preview could not be loaded from type ${file.type}`);
};
element.src = URL.createObjectURL(file);
}
} catch (e) {
reject(`preview could not be loaded from type ${file.type}`);
} }
}); });
} }
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) { async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize); this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload; let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
@@ -659,20 +742,30 @@ class ReceiveFileDialog extends ReceiveDialog {
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
}; };
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"); Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) this.show();
this.show();
setTimeout(_ => {
if (canShare) { if (canShare) {
this.$shareBtn.click(); this.$shareBtn.click();
} else { } else {
this.$downloadBtn.click(); this.$downloadBtn.click();
} }
}).catch(r => console.error(r)); }, 500);
this.createPreviewElement(files[0])
.then(canPreview => {
if (canPreview) {
console.log('the file is able to preview');
} else {
console.log('the file is not able to preview');
}
})
.catch(r => console.error(r));
} }
_downloadFilesIndividually(files) { _downloadFilesIndividually(files) {
@@ -729,7 +822,8 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
const displayName = $(peerId).ui._displayName(); const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize); const connectionHash = $(peerId).ui._connectionHash;
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, 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');
@@ -766,15 +860,17 @@ class ReceiveRequestDialog extends ReceiveDialog {
class PairDeviceDialog extends Dialog { class PairDeviceDialog extends Dialog {
constructor() { constructor() {
super('pair-device-dialog'); super('pair-device-dialog');
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input'); this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
this.$submitBtn = this.$el.querySelector('button[type="submit"]'); this.$submitBtn = this.$el.querySelector('button[type="submit"]');
this.$roomKey = this.$el.querySelector('#room-key'); this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#room-key-qr-code'); this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$pairDeviceBtn = $('pair-device');
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'); this.$createJoinForm = this.$el.querySelector('form');
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate());
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)));
@@ -867,6 +963,7 @@ class PairDeviceDialog extends Dialog {
} }
_onWsConnected() { _onWsConnected() {
this.$pairDeviceBtn.removeAttribute('hidden');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => { PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets); Events.fire('room-secrets', roomSecrets);
this._evaluateNumberRoomSecrets(); this._evaluateNumberRoomSecrets();
@@ -1143,7 +1240,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() {
@@ -1168,21 +1265,21 @@ class Base64ZipDialog extends Dialog {
const base64Hash = window.location.hash.substring(1); const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea');
if (base64Text) { if (base64Text) {
this.show(); this.show();
if (base64Text === "paste") { if (base64Text === "paste") {
// ?base64text=paste // ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard // base64 encoded string is ready to be pasted from clipboard
this.$pasteBtn.innerText = 'Tap here to paste text'; this.preparePasting("text");
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
} else if (base64Text === "hash") { } else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED // ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended) // base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash) this.processBase64Text(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.") console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
@@ -1192,7 +1289,7 @@ class Base64ZipDialog extends Dialog {
this.processBase64Text(base64Text) this.processBase64Text(base64Text)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.") console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
@@ -1205,14 +1302,13 @@ class Base64ZipDialog extends Dialog {
this.processBase64Zip(base64Hash) this.processBase64Zip(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'File content is incorrect.'); Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.") console.log("File content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
} else { } else {
// ?base64zip=paste || ?base64zip=true // ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files'; this.preparePasting('files');
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
} }
} }
} }
@@ -1222,39 +1318,60 @@ class Base64ZipDialog extends Dialog {
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = "Processing...";
} }
async processClipboard(type) { preparePasting(type) {
if (!navigator.clipboard.readText) { if (navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.'); this.$pasteBtn.innerText = `Tap here to paste ${type}`;
console.log("navigator.clipboard.readText() is not available on your browser.") this._clickCallback = _ => this.processClipboard(type);
this.hide(); this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
return;
}
this._setPasteBtnToProcessing();
const base64 = await navigator.clipboard.readText();
if (!base64) return;
if (type === "text") {
this.processBase64Text(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
} else { } else {
this.processBase64Zip(base64) console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
.catch(_ => { this.$pasteBtn.setAttribute('hidden', '');
Events.fire('notify-user', 'Clipboard content is incorrect.'); this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
console.log("Clipboard content is incorrect.") this.$fallbackTextarea.removeAttribute('hidden');
}).finally(_ => { this._inputCallback = _ => this.processInput(type);
this.hide(); this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
}); this.$fallbackTextarea.focus();
} }
} }
async processInput(type) {
const base64 = this.$fallbackTextarea.textContent;
this.$fallbackTextarea.textContent = '';
await this.processBase64(type, base64);
}
async processClipboard(type) {
const base64 = await navigator.clipboard.readText();
await this.processBase64(type, base64);
}
isValidBase64(base64) {
try {
// check if input is base64 encoded
window.atob(base64);
return true;
} catch (e) {
// input is not base64 string.
return false;
}
}
async processBase64(type, base64) {
if (!base64 || !this.isValidBase64(base64)) return;
this._setPasteBtnToProcessing();
try {
if (type === "text") {
await this.processBase64Text(base64);
} else {
await this.processBase64Zip(base64);
}
} catch(_) {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}
this.hide();
}
processBase64Text(base64Text){ processBase64Text(base64Text){
return new Promise((resolve) => { return new Promise((resolve) => {
this._setPasteBtnToProcessing(); this._setPasteBtnToProcessing();
@@ -1288,6 +1405,8 @@ class Base64ZipDialog extends Dialog {
hide() { hide() {
this.clearBrowserHistory(); this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide(); super.hide();
} }
} }
@@ -1318,9 +1437,9 @@ class Notifications {
this.$button.removeAttribute('hidden'); this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission()); this.$button.addEventListener('click', _ => this._requestPermission());
} }
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files)); Events.on('files-received', e => this._downloadNotification(e.detail.files));
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
} }
_requestPermission() { _requestPermission() {
@@ -1393,8 +1512,29 @@ class Notifications {
} }
} }
_requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') {
let imagesOnly = true;
for(let i=0; i<request.header.length; i++) {
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
let displayName = $(peerId).querySelector('.name').textContent
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
const notification = this._notify(title, 'Click to show');
}
}
_download(notification) { _download(notification) {
$('share-or-download').click(); $('download-btn').click();
notification.close(); notification.close();
} }
@@ -1424,7 +1564,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();
} }
@@ -1435,17 +1575,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;
} }
} }
@@ -1724,6 +1863,23 @@ class PersistentStorage {
} }
} }
class Broadcast {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
}
_broadcastMessage(message) {
this.bc.postMessage(message);
}
_onMessage(e) {
console.log('Broadcast message received:', e.data)
Events.fire(e.data.type, e.data.detail);
}
}
class PairDrop { class PairDrop {
constructor() { constructor() {
Events.on('load', _ => { Events.on('load', _ => {
@@ -1743,6 +1899,7 @@ class PairDrop {
const webShareTargetUI = new WebShareTargetUI(); const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI(); const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI(); const noSleepUI = new NoSleepUI();
const broadCast = new Broadcast();
}); });
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.2.1'; const cacheVersion = 'v1.4.5';
const cacheTitle = `pairdrop-cache-${cacheVersion}`; const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',
+74 -8
View File
@@ -22,13 +22,18 @@ body {
} }
body { body {
min-height: 100vh; height: 100%;
/* mobile viewport bug fix */ /* mobile viewport bug fix */
min-height: -webkit-fill-available; min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
} }
html { html {
height: -webkit-fill-available; height: 100%;
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
} }
.row-reverse { .row-reverse {
@@ -450,6 +455,7 @@ x-peer[status] x-icon {
} }
.device-descriptor { .device-descriptor {
width: 100%;
text-align: center; text-align: center;
} }
@@ -533,6 +539,7 @@ footer {
padding: 0 0 16px 0; padding: 0 0 16px 0;
text-align: center; text-align: center;
transition: color 300ms; transition: color 300ms;
cursor: default;
} }
footer .logo { footer .logo {
@@ -557,6 +564,39 @@ footer .font-body2 {
padding-bottom: 1px; padding-bottom: 1px;
} }
#display-name {
display: inline-block;
text-align: left;
border: none;
outline: none;
max-width: 15em;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
margin-left: -1rem;
margin-bottom: -6px;
padding-right: 0.3rem;
padding-left: 0.3em;
padding-bottom: 0.1rem;
border-radius: 1.3rem/30%;
border-right: solid 1rem transparent;
border-left: solid 1rem transparent;
background-clip: padding-box;
background-color: rgba(var(--text-color), 43%);
color: white;
transition: background-color 0.5s ease;
overflow: hidden;
}
#edit-pen {
width: 1rem;
height: 1rem;
margin-left: -1rem;
margin-bottom: -2px;
position: relative;
z-index: -1;
}
/* Dialog */ /* Dialog */
x-dialog x-background { x-dialog x-background {
@@ -756,10 +796,29 @@ x-dialog .dialog-subheader {
margin: auto -24px; margin: auto -24px;
} }
#base64-paste-btn { #base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; border: solid 12px #438cff;
text-align: center;
}
#base64-paste-dialog .textarea {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
#base64-paste-dialog .textarea::before {
font-size: 15px;
letter-spacing: 0.12em;
color: var(--primary-color);
font-weight: 700;
text-transform: uppercase;
content: attr(placeholder);
} }
#base64-paste-dialog button { #base64-paste-dialog button {
@@ -939,6 +998,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 */
@@ -988,11 +1054,11 @@ button::-moz-focus-inner {
x-toast { x-toast {
position: absolute; position: absolute;
min-height: 48px; min-height: 48px;
bottom: 24px; top: 50px;
width: 100%; width: 100%;
max-width: 344px; max-width: 344px;
background-color: #323232; background-color: rgb(var(--text-color));
color: rgba(255, 255, 255, 0.95); color: rgb(var(--bg-color));
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
padding: 8px 24px; padding: 8px 24px;
@@ -1006,7 +1072,7 @@ x-toast {
x-toast:not([show]):not(:hover) { x-toast:not([show]):not(:hover) {
opacity: 0; opacity: 0;
transform: translateY(100px); transform: translateY(-100px);
} }
+23 -12
View File
@@ -59,7 +59,7 @@
<use xlink:href="#homescreen" /> <use xlink:href="#homescreen" />
</svg> </svg>
</a> </a>
<a id="pair-device" class="icon-button" title="Pair Device" > <a id="pair-device" class="icon-button" title="Pair Device" hidden>
<svg class="icon"> <svg class="icon">
<use xlink:href="#pair-device-icon" /> <use xlink:href="#pair-device-icon" />
</svg> </svg>
@@ -89,7 +89,13 @@
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
<div id="display-name" placeholder="&nbsp;"></div> <div>
<span>You are known as:</span>
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon">
<use xlink:href="#edit-pen-icon" />
</svg>
</div>
<div class="font-body2"> <div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span> You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span> <span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
@@ -119,7 +125,7 @@
<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="center row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" disabled>Pair</button>
<button class="button" close>Cancel</button> <button class="button" type="button" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -134,7 +140,7 @@
<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="center row-reverse"> <div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button> <button class="button" type="submit">Unpair Devices</button>
<button class="button" close>Cancel</button> <button class="button" type="button" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -206,7 +212,7 @@
<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="center 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>
<button class="button" title="ESCAPE" close>Cancel</button> <button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -235,6 +241,7 @@
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button> <button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button> <button class="button center" close>Close</button>
</x-paper> </x-paper>
</x-background> </x-background>
@@ -245,14 +252,14 @@
</div> </div>
<!-- About Page --> <!-- About Page -->
<x-about id="about" class="full center column"> <x-about id="about" class="full center column">
<header class="row-reverse fade-in">
<a href="#" class="close icon-button">
<svg class="icon">
<use xlink:href="#close-icon" />
</svg>
</a>
</header>
<section class="center column fade-in"> <section class="center column fade-in">
<header class="row-reverse">
<a href="#" class="close icon-button">
<svg class="icon">
<use xlink:href="#close-icon" />
</svg>
</a>
</header>
<svg class="icon logo"> <svg class="icon logo">
<use xlink:href="#wifi-tethering" /> <use xlink:href="#wifi-tethering" />
</svg> </svg>
@@ -335,6 +342,10 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> <!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/> <path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
</symbol> </symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
</symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/util.js"></script> <script src="scripts/util.js"></script>
+100 -67
View File
@@ -19,10 +19,10 @@ class ServerConnection {
Events.on('online', _ => this._connect()); Events.on('online', _ => this._connect());
} }
async _connect() { _connect() {
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return; if (this._isConnected() || this._isConnecting()) return;
const ws = new WebSocket(await this._endpoint()); const ws = new WebSocket(this._endpoint());
ws.binaryType = 'arraybuffer'; ws.binaryType = 'arraybuffer';
ws.onopen = _ => this._onOpen(); ws.onopen = _ => this._onOpen();
ws.onmessage = e => this._onMessage(e.data); ws.onmessage = e => this._onMessage(e.data);
@@ -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) {
@@ -50,16 +51,23 @@ class ServerConnection {
_onPairDeviceJoin(roomKey) { _onPairDeviceJoin(roomKey) {
if (!this._isConnected()) { if (!this._isConnected()) {
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000); setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
return; return;
} }
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;
@@ -105,6 +113,7 @@ class ServerConnection {
case 'file-transfer-complete': case 'file-transfer-complete':
case 'message-transfer-complete': case 'message-transfer-complete':
case 'text': case 'text':
case 'display-name-changed':
case 'ws-chunk': case 'ws-chunk':
Events.fire('ws-relay', JSON.stringify(msg)); Events.fire('ws-relay', JSON.stringify(msg));
break; break;
@@ -120,34 +129,24 @@ class ServerConnection {
_onDisplayName(msg) { _onDisplayName(msg) {
sessionStorage.setItem("peerId", msg.message.peerId); sessionStorage.setItem("peerId", msg.message.peerId);
PersistentStorage.get('peerId').then(peerId => { sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
if (!peerId) {
// save peerId to indexedDB to retrieve after PWA is installed
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
console.log(`peerId saved to indexedDB: ${peerId}`);
});
}
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
Events.fire('display-name', msg); Events.fire('display-name', msg);
} }
async _endpoint() { _endpoint() {
// hack to detect if deployment or development environment // hack to detect if deployment or development environment
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws'; const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback'; const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc); let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
const peerId = await this._peerId(); const peerId = sessionStorage.getItem("peerId");
if (peerId) ws_url.searchParams.append('peer_id', peerId) const peerIdHash = sessionStorage.getItem("peerIdHash");
if (peerId && peerIdHash) {
ws_url.searchParams.append('peer_id', peerId);
ws_url.searchParams.append('peer_id_hash', peerIdHash);
}
return ws_url.toString(); return ws_url.toString();
} }
async _peerId() {
// make peerId persistent when pwa is installed
return window.matchMedia('(display-mode: minimal-ui)').matches
? await PersistentStorage.get('peerId')
: sessionStorage.getItem("peerId");
}
_disconnect() { _disconnect() {
this.send({ type: 'disconnect' }); this.send({ type: 'disconnect' });
if (this._socket) { if (this._socket) {
@@ -155,15 +154,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(), 1000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
_onVisibilityChange() { _onVisibilityChange() {
@@ -204,6 +205,10 @@ class Peer {
this._send(JSON.stringify(message)); this._send(JSON.stringify(message));
} }
sendDisplayName(displayName) {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) { async createHeader(file) {
return { return {
name: file.name, name: file.name,
@@ -329,25 +334,25 @@ class Peer {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
message = JSON.parse(message); const messageJSON = JSON.parse(message);
switch (message.type) { switch (messageJSON.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();
@@ -356,7 +361,10 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(message); this._onTextReceived(messageJSON);
break;
case 'display-name-changed':
this._onDisplayNameChanged(messageJSON);
break; break;
} }
} }
@@ -495,6 +503,12 @@ class Peer {
Events.fire('text-received', { text: escaped, peerId: this._peerId }); Events.fire('text-received', { text: escaped, peerId: this._peerId });
this.sendJSON({ type: 'message-transfer-complete' }); this.sendJSON({ type: 'message-transfer-complete' });
} }
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
}
} }
class RTCPeer extends Peer { class RTCPeer extends Peer {
@@ -519,8 +533,9 @@ 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.onicecandidateerror = e => this._onError(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,20 +583,19 @@ class RTCPeer extends Peer {
_onChannelOpened(event) { _onChannelOpened(event) {
console.log('RTC: channel opened with', this._peerId); console.log('RTC: channel opened with', this._peerId);
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
const channel = event.channel || event.target; const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
channel.onclose = _ => this._onChannelClosed(); channel.onclose = _ => this._onChannelClosed();
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._closeChannel());
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide());
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
} }
_onMessage(message) { _onMessage(message) {
if (typeof message === 'string') { if (typeof message === 'string') {
message = JSON.parse(message); console.log('RTC:', JSON.parse(message));
console.log('RTC:', message);
} }
super._onMessage(message); super._onMessage(message);
} }
@@ -619,10 +633,16 @@ class RTCPeer extends Peer {
} }
} }
_closeChannel() { _onPageHide() {
if (this._channel) this._channel.onclose = null; this._disconnect();
if (this._conn) this._conn.close(); }
this._conn = null;
_disconnect() {
if (this._conn && this._channel) {
this._channel.onclose = null;
this._channel.close();
}
Events.fire('peer-disconnected', this._peerId);
} }
_onChannelClosed() { _onChannelClosed() {
@@ -636,9 +656,11 @@ class RTCPeer extends Peer {
console.log('RTC: state changed:', this._conn.connectionState); console.log('RTC: state changed:', this._conn.connectionState);
switch (this._conn.connectionState) { switch (this._conn.connectionState) {
case 'disconnected': case 'disconnected':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection disconnected'); this._onError('rtc connection disconnected');
break; break;
case 'failed': case 'failed':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection failed'); this._onError('rtc connection failed');
break; break;
} }
@@ -684,6 +706,11 @@ class RTCPeer extends Peer {
_isConnecting() { _isConnecting() {
return this._channel && this._channel.readyState === 'connecting'; return this._channel && this._channel.readyState === 'connecting';
} }
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
}
} }
class WSPeer extends Peer { class WSPeer extends Peer {
@@ -692,6 +719,7 @@ class WSPeer extends Peer {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = false; this.rtcSupported = false;
if (!peerId) return; // we will listen for a caller if (!peerId) return; // we will listen for a caller
this._isCaller = true;
this._sendSignal(); this._sendSignal();
} }
@@ -703,6 +731,7 @@ class WSPeer extends Peer {
} }
sendJSON(message) { sendJSON(message) {
console.debug(message)
message.to = this._peerId; message.to = this._peerId;
message.roomType = this._roomType; message.roomType = this._roomType;
message.roomSecret = this._roomSecret; message.roomSecret = this._roomSecret;
@@ -714,9 +743,9 @@ class WSPeer extends Peer {
} }
onServerMessage(message) { onServerMessage(message) {
this._peerId = message.sender.id;
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()}) Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (message.connected) return; if (message.connected) return;
this._peerId = message.sender.id;
this._sendSignal(true); this._sendSignal(true);
} }
@@ -737,8 +766,12 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail)); Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail)); Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
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('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail)); Events.on('ws-relay', e => this._onWsRelay(e.detail));
} }
@@ -778,10 +811,6 @@ class PeersManager {
}) })
} }
sendTo(peerId, message) {
this.peers[peerId].send(message);
}
_onRespondToFileTransferRequest(detail) { _onRespondToFileTransferRequest(detail) {
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
} }
@@ -816,6 +845,10 @@ class PeersManager {
} }
} }
_onPeerConnected(peerId) {
this._notifyPeerDisplayNameChanged(peerId);
}
_onWsDisconnected() { _onWsDisconnected() {
for (const peerId in this.peers) { for (const peerId in this.peers) {
console.debug(this.peers[peerId].rtcSupported); console.debug(this.peers[peerId].rtcSupported);
@@ -842,6 +875,23 @@ class PeersManager {
} }
} }
} }
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
this._notifyPeerDisplayNameChanged(peerId);
}
}
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer) return;
this.peers[peerId].sendDisplayName(this._displayName);
}
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
}
} }
class FileChunker { class FileChunker {
@@ -930,28 +980,11 @@ class Events {
window.dispatchEvent(new CustomEvent(type, { detail: detail })); window.dispatchEvent(new CustomEvent(type, { detail: detail }));
} }
static on(type, callback) { static on(type, callback, options = false) {
return window.addEventListener(type, callback, false); return window.addEventListener(type, callback, options);
} }
static off(type, callback) { static off(type, callback, options = false) {
return window.removeEventListener(type, callback, false); return window.removeEventListener(type, callback, options);
} }
} }
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',
},
]
}
+248 -91
View File
@@ -9,9 +9,8 @@ window.pasteMode.activated = false;
// set display name // set display name
Events.on('display-name', e => { Events.on('display-name', e => {
const me = e.detail.message; const me = e.detail.message;
const $displayName = $('display-name') const $displayName = $('display-name');
$displayName.textContent = 'You are known as ' + me.displayName; $displayName.setAttribute('placeholder', me.displayName);
$displayName.title = me.deviceName;
}); });
class PeersUI { class PeersUI {
@@ -43,6 +42,80 @@ class PeersUI {
Events.on('peer-added', _ => this.evaluateOverflowing()); Events.on('peer-added', _ => this.evaluateOverflowing());
Events.on('bg-resize', _ => this.evaluateOverflowing()); Events.on('bg-resize', _ => this.evaluateOverflowing());
this.$displayName = $('display-name');
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
// Load saved display name on page load
this._getSavedDisplayName().then(displayName => {
console.log("Retrieved edited display name:", displayName)
if (displayName) Events.fire('self-display-name-changed', displayName);
});
}
_insertDisplayName(displayName) {
this.$displayName.textContent = displayName;
}
_onKeyDownDisplayName(e) {
if (e.key === "Enter" || e.key === "Escape") {
e.preventDefault();
e.target.blur();
}
}
_onKeyUpDisplayName(e) {
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
}
async _saveDisplayName(newDisplayName) {
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
const savedDisplayName = await this._getSavedDisplayName();
if (newDisplayName === savedDisplayName) return;
if (newDisplayName) {
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.');
}).catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', 'Device name is changed only for this session.');
}).finally(_ => {
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
});
} else {
PersistentStorage.delete('editedDisplayName').catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
}).finally(_ => {
Events.fire('notify-user', 'Device name is randomly generated again.');
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
}
}
_getSavedDisplayName() {
return new Promise((resolve) => {
PersistentStorage.get('editedDisplayName')
.then(displayName => resolve(displayName ?? ""))
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
});
}
_changePeerDisplayName(peerId, displayName) {
this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
} }
_onKeyDown(e) { _onKeyDown(e) {
@@ -168,7 +241,7 @@ class PeersUI {
const _callback = (e) => this._sendClipboardData(e, files, text); const _callback = (e) => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback); Events.on('paste-pointerdown', _callback);
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback)); Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
this.$cancelPasteModeBtn.removeAttribute('hidden'); this.$cancelPasteModeBtn.removeAttribute('hidden');
@@ -227,7 +300,8 @@ class PeerUI {
constructor(peer, connectionHash) { constructor(peer, connectionHash) {
this._peer = peer; this._peer = peer;
this._connectionHash = connectionHash; this._connectionHash =
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
this._initDom(); this._initDom();
this._bindListeners(); this._bindListeners();
@@ -272,8 +346,7 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName(); this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent = this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
} }
_initDom() { _initDom() {
@@ -497,7 +570,7 @@ class ReceiveDialog extends Dialog {
} }
} }
_parseFileData(displayName, files, imagesOnly, totalSize) { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) { if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `; let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) { if (files.length === 2) {
@@ -514,6 +587,7 @@ class ReceiveDialog extends Dialog {
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension; this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName; this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$fileSize.innerText = this._formatFileSize(totalSize); this.$fileSize.innerText = this._formatFileSize(totalSize);
} }
} }
@@ -531,8 +605,9 @@ class ReceiveFileDialog extends ReceiveDialog {
} }
_onFilesReceived(sender, files, imagesOnly, totalSize) { _onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName() const displayName = $(sender).ui._displayName();
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); const connectionHash = $(sender).ui._connectionHash;
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles(); this._nextFiles();
window.blop.play(); window.blop.play();
} }
@@ -540,8 +615,8 @@ class ReceiveFileDialog extends ReceiveDialog {
_nextFiles() { _nextFiles() {
if (this._busy) return; if (this._busy) return;
this._busy = true; this._busy = true;
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift(); const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize); this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
} }
_dequeueFile() { _dequeueFile() {
@@ -558,32 +633,40 @@ class ReceiveFileDialog extends ReceiveDialog {
createPreviewElement(file) { createPreviewElement(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let mime = file.type.split('/')[0] try {
let previewElement = { let mime = file.type.split('/')[0]
image: 'img', let previewElement = {
audio: 'audio', image: 'img',
video: 'video' audio: 'audio',
} video: 'video'
}
if (Object.keys(previewElement).indexOf(mime) === -1) { if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false); resolve(false);
} else { } else {
console.log('the file is able to preview'); let element = document.createElement(previewElement[mime]);
let element = document.createElement(previewElement[mime]); element.controls = true;
element.src = URL.createObjectURL(file); element.onload = _ => {
element.controls = true; this.$previewBox.appendChild(element);
element.onload = _ => { resolve(true);
this.$previewBox.appendChild(element); };
resolve(true) element.onloadeddata = _ => {
}; this.$previewBox.appendChild(element);
element.addEventListener('loadeddata', _ => resolve(true)); resolve(true);
element.onerror = _ => reject(`${mime} preview could not be loaded from type ${file.type}`); };
element.onerror = _ => {
reject(`${mime} preview could not be loaded from type ${file.type}`);
};
element.src = URL.createObjectURL(file);
}
} catch (e) {
reject(`preview could not be loaded from type ${file.type}`);
} }
}); });
} }
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) { async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize); this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload; let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
@@ -660,20 +743,30 @@ class ReceiveFileDialog extends ReceiveDialog {
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
}; };
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"); Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) this.show();
this.show();
setTimeout(_ => {
if (canShare) { if (canShare) {
this.$shareBtn.click(); this.$shareBtn.click();
} else { } else {
this.$downloadBtn.click(); this.$downloadBtn.click();
} }
}).catch(r => console.error(r)); }, 500);
this.createPreviewElement(files[0])
.then(canPreview => {
if (canPreview) {
console.log('the file is able to preview');
} else {
console.log('the file is not able to preview');
}
})
.catch(r => console.error(r));
} }
_downloadFilesIndividually(files) { _downloadFilesIndividually(files) {
@@ -730,7 +823,8 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
const displayName = $(peerId).ui._displayName(); const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize); const connectionHash = $(peerId).ui._connectionHash;
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, 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');
@@ -767,15 +861,17 @@ class ReceiveRequestDialog extends ReceiveDialog {
class PairDeviceDialog extends Dialog { class PairDeviceDialog extends Dialog {
constructor() { constructor() {
super('pair-device-dialog'); super('pair-device-dialog');
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input'); this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
this.$submitBtn = this.$el.querySelector('button[type="submit"]'); this.$submitBtn = this.$el.querySelector('button[type="submit"]');
this.$roomKey = this.$el.querySelector('#room-key'); this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#room-key-qr-code'); this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$pairDeviceBtn = $('pair-device');
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'); this.$createJoinForm = this.$el.querySelector('form');
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate());
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)));
@@ -868,6 +964,7 @@ class PairDeviceDialog extends Dialog {
} }
_onWsConnected() { _onWsConnected() {
this.$pairDeviceBtn.removeAttribute('hidden');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => { PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets); Events.fire('room-secrets', roomSecrets);
this._evaluateNumberRoomSecrets(); this._evaluateNumberRoomSecrets();
@@ -1144,7 +1241,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() {
@@ -1169,21 +1266,21 @@ class Base64ZipDialog extends Dialog {
const base64Hash = window.location.hash.substring(1); const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea');
if (base64Text) { if (base64Text) {
this.show(); this.show();
if (base64Text === "paste") { if (base64Text === "paste") {
// ?base64text=paste // ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard // base64 encoded string is ready to be pasted from clipboard
this.$pasteBtn.innerText = 'Tap here to paste text'; this.preparePasting("text");
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
} else if (base64Text === "hash") { } else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED // ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended) // base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash) this.processBase64Text(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.") console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
@@ -1193,7 +1290,7 @@ class Base64ZipDialog extends Dialog {
this.processBase64Text(base64Text) this.processBase64Text(base64Text)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.") console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
@@ -1206,14 +1303,13 @@ class Base64ZipDialog extends Dialog {
this.processBase64Zip(base64Hash) this.processBase64Zip(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'File content is incorrect.'); Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.") console.log("File content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
} else { } else {
// ?base64zip=paste || ?base64zip=true // ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files'; this.preparePasting('files');
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
} }
} }
} }
@@ -1223,39 +1319,60 @@ class Base64ZipDialog extends Dialog {
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = "Processing...";
} }
async processClipboard(type) { preparePasting(type) {
if (!navigator.clipboard.readText) { if (navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.'); this.$pasteBtn.innerText = `Tap here to paste ${type}`;
console.log("navigator.clipboard.readText() is not available on your browser.") this._clickCallback = _ => this.processClipboard(type);
this.hide(); this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
return;
}
this._setPasteBtnToProcessing();
const base64 = await navigator.clipboard.readText();
if (!base64) return;
if (type === "text") {
this.processBase64Text(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
} else { } else {
this.processBase64Zip(base64) console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
.catch(_ => { this.$pasteBtn.setAttribute('hidden', '');
Events.fire('notify-user', 'Clipboard content is incorrect.'); this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
console.log("Clipboard content is incorrect.") this.$fallbackTextarea.removeAttribute('hidden');
}).finally(_ => { this._inputCallback = _ => this.processInput(type);
this.hide(); this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
}); this.$fallbackTextarea.focus();
} }
} }
async processInput(type) {
const base64 = this.$fallbackTextarea.textContent;
this.$fallbackTextarea.textContent = '';
await this.processBase64(type, base64);
}
async processClipboard(type) {
const base64 = await navigator.clipboard.readText();
await this.processBase64(type, base64);
}
isValidBase64(base64) {
try {
// check if input is base64 encoded
window.atob(base64);
return true;
} catch (e) {
// input is not base64 string.
return false;
}
}
async processBase64(type, base64) {
if (!base64 || !this.isValidBase64(base64)) return;
this._setPasteBtnToProcessing();
try {
if (type === "text") {
await this.processBase64Text(base64);
} else {
await this.processBase64Zip(base64);
}
} catch(_) {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}
this.hide();
}
processBase64Text(base64Text){ processBase64Text(base64Text){
return new Promise((resolve) => { return new Promise((resolve) => {
this._setPasteBtnToProcessing(); this._setPasteBtnToProcessing();
@@ -1289,6 +1406,8 @@ class Base64ZipDialog extends Dialog {
hide() { hide() {
this.clearBrowserHistory(); this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide(); super.hide();
} }
} }
@@ -1319,9 +1438,9 @@ class Notifications {
this.$button.removeAttribute('hidden'); this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission()); this.$button.addEventListener('click', _ => this._requestPermission());
} }
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files)); Events.on('files-received', e => this._downloadNotification(e.detail.files));
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
} }
_requestPermission() { _requestPermission() {
@@ -1394,8 +1513,29 @@ class Notifications {
} }
} }
_requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') {
let imagesOnly = true;
for(let i=0; i<request.header.length; i++) {
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
let displayName = $(peerId).querySelector('.name').textContent
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
const notification = this._notify(title, 'Click to show');
}
}
_download(notification) { _download(notification) {
$('share-or-download').click(); $('download-btn').click();
notification.close(); notification.close();
} }
@@ -1425,7 +1565,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();
} }
@@ -1436,17 +1576,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;
} }
} }
@@ -1725,6 +1864,23 @@ class PersistentStorage {
} }
} }
class Broadcast {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
}
_broadcastMessage(message) {
this.bc.postMessage(message);
}
_onMessage(e) {
console.log('Broadcast message received:', e.data)
Events.fire(e.data.type, e.data.detail);
}
}
class PairDrop { class PairDrop {
constructor() { constructor() {
Events.on('load', _ => { Events.on('load', _ => {
@@ -1744,6 +1900,7 @@ class PairDrop {
const webShareTargetUI = new WebShareTargetUI(); const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI(); const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI(); const noSleepUI = new NoSleepUI();
const broadCast = new Broadcast();
}); });
} }
} }
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.2.1'; const cacheVersion = 'v1.4.5';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',
+74 -8
View File
@@ -23,13 +23,18 @@ body {
} }
body { body {
min-height: 100vh; height: 100%;
/* mobile viewport bug fix */ /* mobile viewport bug fix */
min-height: -webkit-fill-available; min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
} }
html { html {
height: -webkit-fill-available; height: 100%;
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
} }
.row-reverse { .row-reverse {
@@ -477,6 +482,7 @@ x-peer.ws-peer .highlight-wrapper {
} }
.device-descriptor { .device-descriptor {
width: 100%;
text-align: center; text-align: center;
} }
@@ -559,6 +565,7 @@ footer {
align-items: center; align-items: center;
text-align: center; text-align: center;
transition: color 300ms; transition: color 300ms;
cursor: default;
} }
footer .logo { footer .logo {
@@ -583,6 +590,39 @@ footer .font-body2 {
padding-bottom: 1px; padding-bottom: 1px;
} }
#display-name {
display: inline-block;
text-align: left;
border: none;
outline: none;
max-width: 15em;
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
margin-left: -1rem;
margin-bottom: -6px;
padding-right: 0.3rem;
padding-left: 0.3em;
padding-bottom: 0.1rem;
border-radius: 1.3rem/30%;
border-right: solid 1rem transparent;
border-left: solid 1rem transparent;
background-clip: padding-box;
background-color: rgba(var(--text-color), 43%);
color: white;
transition: background-color 0.5s ease;
overflow: hidden;
}
#edit-pen {
width: 1rem;
height: 1rem;
margin-left: -1rem;
margin-bottom: -2px;
position: relative;
z-index: -1;
}
/* Dialog */ /* Dialog */
x-dialog x-background { x-dialog x-background {
@@ -782,10 +822,29 @@ x-dialog .dialog-subheader {
margin: auto -24px; margin: auto -24px;
} }
#base64-paste-btn { #base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; border: solid 12px #438cff;
text-align: center;
}
#base64-paste-dialog .textarea {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
#base64-paste-dialog .textarea::before {
font-size: 15px;
letter-spacing: 0.12em;
color: var(--primary-color);
font-weight: 700;
text-transform: uppercase;
content: attr(placeholder);
} }
#base64-paste-dialog button { #base64-paste-dialog button {
@@ -965,6 +1024,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 */
@@ -1014,11 +1080,11 @@ button::-moz-focus-inner {
x-toast { x-toast {
position: absolute; position: absolute;
min-height: 48px; min-height: 48px;
bottom: 24px; top: 50px;
width: 100%; width: 100%;
max-width: 344px; max-width: 344px;
background-color: #323232; background-color: rgb(var(--text-color));
color: rgba(255, 255, 255, 0.95); color: rgb(var(--bg-color));
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
padding: 8px 24px; padding: 8px 24px;
@@ -1032,7 +1098,7 @@ x-toast {
x-toast:not([show]):not(:hover) { x-toast:not([show]):not(:hover) {
opacity: 0; opacity: 0;
transform: translateY(100px); transform: translateY(-100px);
} }
+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"
}
]
}