Compare commits

...

36 Commits

Author SHA1 Message Date
schlagmichdoch
29b91cb17a increase version to v1.7.6 2023-06-01 01:51:51 +02:00
schlagmichdoch
26bf4d6dc3 ensure that otherPeers never receive peer-left after peer-joined on reconnect by leaving room before rejoining it 2023-06-01 01:49:07 +02:00
schlagmichdoch
f195c686e7 increase version to v1.7.5 2023-06-01 01:32:06 +02:00
schlagmichdoch
3505f161c6 strip 'NO-BREAK SPACE' (U+00A0) of received text as some browsers seem to add them when pasting text 2023-06-01 01:29:00 +02:00
schlagmichdoch
3e2368c0c9 stabilize connection on reconnect by terminating websocket only on timeout and not always when peer leaves its ip room 2023-06-01 01:26:53 +02:00
schlagmichdoch
d36cd3524c Fix clearBrowserHistory: url should not always be replaced by "/" as PairDrop might not always be hosted at domain root 2023-05-30 02:34:50 +02:00
schlagmichdoch
a3a8228327 increase version to v1.7.4 2023-05-26 20:37:38 +02:00
schlagmichdoch
520b772bc8 fix #112 and differentiate between textContent and innerText 2023-05-26 20:36:12 +02:00
schlagmichdoch
27bf0fa74f fix #113 2023-05-26 20:36:12 +02:00
schlagmichdoch
e9f3c39f38 Merge pull request #114 from fm-sys/patch-1
Add 'files-sent' event used by 'Snapdrop & PairDrop for Android' app to support vibrations
2023-05-26 20:35:54 +02:00
fm-sys
58b7f6bb7c Add 'files-sent' event 2023-05-26 09:52:17 +02:00
schlagmichdoch
b5987cf017 increase version to v1.7.3 2023-05-23 02:45:29 +02:00
schlagmichdoch
4433e1c58f add version number to about page 2023-05-23 02:44:25 +02:00
schlagmichdoch
b106d90b64 Fix ReferenceError: ipv6_lcl is not defined 2023-05-23 02:43:17 +02:00
schlagmichdoch
c9e1c2504a Merge pull request #110 from luckman212/luckman212-patch-1
Add new env var `IPV6_LOCALIZE` to enable auto discovery for IPv6 addresses
2023-05-23 01:53:23 +02:00
luckman212
32e909b8c2 fixes for https://github.com/schlagmichdoch/PairDrop/issues/69
(squashed, docs updated, IPV6_LOCALIZE input validation)
2023-05-19 12:18:20 -04:00
schlagmichdoch
a444be226f Fix canvas selector 2023-05-16 19:15:47 +02:00
schlagmichdoch
df778ba42c Speed up canvas by removing fade-in animation 2023-05-16 19:09:59 +02:00
schlagmichdoch
8a17b82fa4 Fix _textInputEmpty() for Chromium based browsers
Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com>
2023-05-16 02:53:56 +02:00
schlagmichdoch
56eb29c91b increase version to v1.7.2 2023-05-16 02:35:03 +02:00
schlagmichdoch
6e4bda0adf Fix message sending via submit button.
Co-authored-by: luckman212 <1992842+luckman212@users.noreply.github.com>
2023-05-16 02:25:50 +02:00
Lopolin-LP
0baced640a Fix About Background not filling up full viewport under certain circumstances (#109)
* Fix About Background Not filling up full viewport under certain circumstances

It is now based on vw/vh instead of px. It can also easily be adjusted, mostly. There is no way it will not fill up the viewport.

* add fix for about bg size to websocket fallback too and tidy up

---------

Co-authored-by: schlagmichdoch <schlagmichdoch@users.noreply.github.com>
2023-05-16 01:50:12 +02:00
schlagmichdoch
3c2e73fc0c fix position of about background circle 2023-05-12 04:59:44 +02:00
schlagmichdoch
c629d7cd88 increase version to v1.7.1 2023-05-12 01:41:10 +02:00
schlagmichdoch
ba20c72026 fix error on empty roomSecrets 2023-05-12 01:16:37 +02:00
schlagmichdoch
347f9b87c0 fix check whether peer is same browser 2023-05-12 01:16:37 +02:00
schlagmichdoch
ae9909f596 fix notification "Key null invalidated" on cancel device pairing 2023-05-11 19:56:47 +02:00
schlagmichdoch
26c1878bb9 increase version to v1.7.0 2023-05-11 19:23:39 +02:00
schlagmichdoch
de0afce4ea Merge pull request #107 from schlagmichdoch/add_auto_accept
Add auto accept functionality via Edit Paired Devices Dialog + implement pair secret regeneration functionality
2023-05-11 19:21:26 +02:00
schlagmichdoch
2a837eb195 add 'visbilitychange' event support for older browsers 2023-05-10 21:59:45 +02:00
schlagmichdoch
fdf20cfdd9 save roomSecret and notify user that the pairing is successful only after the corresponding pairPeer has joined. 2023-05-10 21:59:45 +02:00
schlagmichdoch
7606fb398b Fix: notify user that "Selected peer left." only if dialog is shown. 2023-05-10 21:59:45 +02:00
schlagmichdoch
8d640be3a2 increase roomSecret length to 264 chars and implement roomSecret regeneration functionality 2023-05-10 21:59:45 +02:00
schlagmichdoch
241ea4f988 implement auto_accept (#91) and manual unpairing via new Edit Paired Devices Dialog and a BrowserTabsConnector 2023-05-10 21:59:43 +02:00
schlagmichdoch
0ac3c5a11f remove debugging logs 2023-05-04 17:39:40 +02:00
schlagmichdoch
f39bfedf98 use sha3-512 hash instead of cyrb53 to authenticate peerIds on reconnect 2023-05-04 17:34:33 +02:00
16 changed files with 1944 additions and 669 deletions

View File

@@ -35,6 +35,14 @@ Set options by using the following flags in the `docker run` command:
```
> Limits clients to 1000 requests per 5 min
##### IPv6 Localization
```bash
-e IPV6_LOCALIZE=4
```
> To enable Peer Discovery among IPv6 peers, you can specify a reduced number of segments of the client IPv6 address to be evaluated as the peer's IP. This can be especially useful when using Cloudflare as a proxy.
>
> The flag must be set to an **integer** between `1` and `7`. The number represents the number of IPv6 [hextets](https://en.wikipedia.org/wiki/IPv6#Address_representation) to match the client IP against. The most common value would be `4`, which will group peers within the same `/64` subnet.
##### Websocket Fallback (for VPN)
```bash
-e WS_FALLBACK=true
@@ -200,6 +208,12 @@ $env:PORT=3010; npm start
```
> Specify the port PairDrop is running on. (Default: 3000)
#### IPv6 Localization
```bash
IPV6_LOCALIZE=4
```
> Truncate a portion of the client IPv6 address to make peers more discoverable. See [Options/Flags](#options--flags) above.
#### Specify STUN/TURN Server
On Unix based systems
```bash

198
index.js
View File

@@ -96,6 +96,17 @@ if (debugMode) {
console.log("DEBUG_MODE is active. To protect privacy, do not use in production.")
}
let ipv6_lcl;
if (process.env.IPV6_LOCALIZE) {
ipv6_lcl = parseInt(process.env.IPV6_LOCALIZE);
if (!ipv6_lcl || !(0 < ipv6_lcl && ipv6_lcl < 8)) {
console.error("IPV6_LOCALIZE must be an integer between 1 and 7");
return;
}
console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl === 1 ? "segment" : "segments");
}
app.use(function(req, res) {
res.redirect('/');
});
@@ -133,7 +144,6 @@ class PairDropServer {
type: 'rtc-config',
config: rtcConfig
});
this._joinRoom(peer);
// send displayName
this._send(peer, {
@@ -142,7 +152,7 @@ class PairDropServer {
displayName: peer.name.displayName,
deviceName: peer.name.deviceName,
peerId: peer.id,
peerIdHash: peer.id.hashCode128BitSalted()
peerIdHash: hasher.hashCodeSalted(peer.id)
}
});
}
@@ -162,14 +172,14 @@ class PairDropServer {
case 'pong':
sender.lastBeat = Date.now();
break;
case 'join-ip-room':
this._joinRoom(sender);
break;
case 'room-secrets':
this._onRoomSecrets(sender, message);
break;
case 'room-secret-deleted':
this._onRoomSecretDeleted(sender, message);
break;
case 'room-secrets-cleared':
this._onRoomSecretsCleared(sender, message);
case 'room-secrets-deleted':
this._onRoomSecretsDeleted(sender, message);
break;
case 'pair-device-initiate':
this._onPairDeviceInitiate(sender);
@@ -180,6 +190,9 @@ class PairDropServer {
case 'pair-device-cancel':
this._onPairDeviceCancel(sender);
break;
case 'regenerate-room-secret':
this._onRegenerateRoomSecret(sender, message);
break
case 'resend-peers':
this._notifyPeers(sender);
break;
@@ -206,64 +219,53 @@ class PairDropServer {
}
_onDisconnect(sender) {
this._disconnect(sender);
}
_disconnect(sender) {
this._leaveRoom(sender, 'ip', '', true);
this._leaveAllSecretRooms(sender, true);
this._removeRoomKey(sender.roomKey);
sender.roomKey = null;
sender.socket.terminate();
}
_onRoomSecrets(sender, message) {
if (!message.roomSecrets) return;
const roomSecrets = message.roomSecrets.filter(roomSecret => {
return /^[\x00-\x7F]{64}$/.test(roomSecret);
return /^[\x00-\x7F]{64,256}$/.test(roomSecret);
})
if (!roomSecrets) return;
this._joinSecretRooms(sender, roomSecrets);
}
_onRoomSecretDeleted(sender, message) {
this._deleteSecretRoom(sender, message.roomSecret)
}
_onRoomSecretsCleared(sender, message) {
_onRoomSecretsDeleted(sender, message) {
for (let i = 0; i<message.roomSecrets.length; i++) {
this._deleteSecretRoom(sender, message.roomSecrets[i]);
this._deleteSecretRoom(message.roomSecrets[i]);
}
}
_deleteSecretRoom(sender, roomSecret) {
_deleteSecretRoom(roomSecret) {
const room = this._rooms[roomSecret];
if (room) {
for (const peerId in room) {
const peer = room[peerId];
this._leaveRoom(peer, 'secret', roomSecret);
this._send(peer, {
type: 'secret-room-deleted',
roomSecret: roomSecret,
});
}
}
this._notifyPeers(sender);
}
if (!room) return;
getRandomString(length) {
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a propability bias, so I suppose we better skip this character */
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
for (const peerId in room) {
const peer = room[peerId];
this._leaveRoom(peer, 'secret', roomSecret);
this._send(peer, {
type: 'secret-room-deleted',
roomSecret: roomSecret,
});
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
_onPairDeviceInitiate(sender) {
let roomSecret = this.getRandomString(64);
let roomSecret = randomizer.getRandomString(256);
let roomKey = this._createRoomKey(sender, roomSecret);
if (sender.roomKey) this._removeRoomKey(sender.roomKey);
sender.roomKey = roomKey;
@@ -276,16 +278,19 @@ class PairDropServer {
}
_onPairDeviceJoin(sender, message) {
// rate limit implementation: max 10 attempts every 10s
if (sender.roomKeyRate >= 10) {
this._send(sender, { type: 'pair-device-join-key-rate-limit' });
return;
}
sender.roomKeyRate += 1;
setTimeout(_ => sender.roomKeyRate -= 1, 10000);
if (!this._roomSecrets[message.roomKey] || sender.id === this._roomSecrets[message.roomKey].creator.id) {
this._send(sender, { type: 'pair-device-join-key-invalid' });
return;
}
const roomSecret = this._roomSecrets[message.roomKey].roomSecret;
const creator = this._roomSecrets[message.roomKey].creator;
this._removeRoomKey(message.roomKey);
@@ -304,13 +309,32 @@ class PairDropServer {
}
_onPairDeviceCancel(sender) {
if (sender.roomKey) {
this._send(sender, {
type: 'pair-device-canceled',
roomKey: sender.roomKey,
const roomKey = sender.roomKey
if (!roomKey) return;
this._removeRoomKey(roomKey);
this._send(sender, {
type: 'pair-device-canceled',
roomKey: roomKey,
});
}
_onRegenerateRoomSecret(sender, message) {
const oldRoomSecret = message.roomSecret;
const newRoomSecret = randomizer.getRandomString(256);
// notify all other peers
for (const peerId in this._rooms[oldRoomSecret]) {
const peer = this._rooms[oldRoomSecret][peerId];
this._send(peer, {
type: 'room-secret-regenerated',
oldRoomSecret: oldRoomSecret,
newRoomSecret: newRoomSecret,
});
this._removeRoomKey(sender.roomKey);
peer.removeRoomSecret(oldRoomSecret);
}
delete this._rooms[oldRoomSecret];
}
_createRoomKey(creator, roomSecret) {
@@ -339,6 +363,7 @@ class PairDropServer {
const room = roomType === 'ip' ? peer.ip : roomSecret;
if (this._rooms[room] && this._rooms[room][peer.id]) {
// ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect.
this._leaveRoom(peer, roomType, roomSecret);
}
@@ -366,10 +391,6 @@ class PairDropServer {
// delete the peer
delete this._rooms[room][peer.id];
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room];
@@ -449,8 +470,7 @@ class PairDropServer {
peer.lastBeat = Date.now();
}
if (Date.now() - peer.lastBeat > 2 * timeout) {
this._leaveRoom(peer);
this._leaveAllSecretRooms(peer);
this._disconnect(peer);
return;
}
@@ -478,7 +498,7 @@ class Peer {
this._setIP(request);
// set peer id
this._setPeerId(request)
this._setPeerId(request);
// is WebRTC supported ?
this.rtcSupported = request.url.indexOf('webrtc') > -1;
@@ -508,11 +528,19 @@ class Peer {
if (this.ip.substring(0,7) === "::ffff:")
this.ip = this.ip.substring(7);
let ipv6_was_localized = false;
if (ipv6_lcl && this.ip.includes(':')) {
this.ip = this.ip.split(':',ipv6_lcl).join(':');
ipv6_was_localized = true;
}
if (debugMode) {
console.debug("----DEBUGGING-PEER-IP-START----");
console.debug("remoteAddress:", request.connection.remoteAddress);
console.debug("x-forwarded-for:", request.headers['x-forwarded-for']);
console.debug("cf-connecting-ip:", request.headers['cf-connecting-ip']);
if (ipv6_was_localized)
console.debug("IPv6 client IP was localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment");
console.debug("PairDrop uses:", this.ip);
console.debug("IP is private:", this.ipIsPrivate(this.ip));
console.debug("if IP is private, '127.0.0.1' is used instead");
@@ -600,7 +628,7 @@ class Peer {
separator: ' ',
dictionaries: [colors, animals],
style: 'capital',
seed: this.id.hashCode()
seed: cyrb53(this.id)
})
this.name = {
@@ -626,7 +654,7 @@ class Peer {
}
isPeerIdHashValid(peerId, peerIdHash) {
return peerIdHash === peerId.hashCode128BitSalted();
return peerIdHash === hasher.hashCodeSalted(peerId);
}
addRoomSecret(roomSecret) {
@@ -642,39 +670,43 @@ class Peer {
}
}
Object.defineProperty(String.prototype, 'hashCode', {
value: function() {
return cyrb53(this);
}
});
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
value: function() {
return hasher.hashCode128BitSalted(this);
}
});
const hasher = (() => {
let seeds;
let password;
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];
}
hashCodeSalted(salt) {
if (!password) {
// password is created on first call.
password = randomizer.getRandomString(128);
}
let hashCode = "";
for (let i=0; i<4; i++) {
hashCode += cyrb53(str, seeds[i]);
}
return hashCode;
return crypto.createHash("sha3-512")
.update(password)
.update(crypto.createHash("sha3-512").update(salt, "utf8").digest("hex"))
.digest("hex");
}
}
})()
const randomizer = (() => {
return {
getRandomString(length) {
let string = "";
while (string.length < length) {
let arr = new Uint16Array(length);
crypto.webcrypto.getRandomValues(arr);
arr = Array.apply([], arr); /* turn into non-typed array */
arr = arr.map(function (r) {
return r % 128
})
arr = arr.filter(function (r) {
/* strip non-printables: if we transform into desirable range we have a probability bias, so I suppose we better skip this character */
return r === 45 || r >= 47 && r <= 57 || r >= 64 && r <= 90 || r >= 97 && r <= 122;
});
string += String.fromCharCode.apply(String, arr);
}
return string.substring(0, length)
}
}
})()
/*

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "pairdrop",
"version": "1.6.3",
"version": "1.7.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.6.3",
"version": "1.7.6",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",

View File

@@ -1,6 +1,6 @@
{
"name": "pairdrop",
"version": "1.6.3",
"version": "1.7.6",
"description": "",
"main": "index.js",
"scripts": {

View File

@@ -78,9 +78,9 @@
<use xlink:href="#pair-device-icon" />
</svg>
</div>
<div id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
<svg class="icon">
<use xlink:href="#clear-pair-devices-icon" />
<use xlink:href="#edit-pair-devices-icon" />
</svg>
</div>
<div id="cancel-paste-mode" class="button" hidden>Done</div>
@@ -142,16 +142,18 @@
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clear-devices-dialog">
<!-- Edit Paired Devices Dialog -->
<x-dialog id="edit-paired-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<h2 class="center">Edit Paired Devices</h2>
<div class="paired-devices-wrapper"></div>
<div class="font-subheading center">
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" type="button" close>Cancel</button>
<button class="button" type="button" close>Close</button>
</div>
</x-paper>
</x-background>
@@ -222,7 +224,7 @@
<div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
@@ -274,7 +276,10 @@
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<h1>PairDrop</h1>
<div class="title-wrapper">
<h1>PairDrop</h1>
<div class="font-subheading">v1.7.6</div>
</div>
<div class="font-subheading">The easiest way to transfer files across devices</div>
<div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
@@ -301,6 +306,7 @@
</section>
<x-background></x-background>
</x-about>
<canvas class="circles"></canvas>
<!-- SVG Icon Library -->
<svg style="display: none;">
<symbol id=wifi-tethering viewBox="0 0 24 24">
@@ -355,9 +361,11 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.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 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.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.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
</symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
<!--! 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"/>
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.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.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-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.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
</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. -->

View File

@@ -3,16 +3,26 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
@@ -39,10 +49,6 @@ class ServerConnection {
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
}
_sendRoomSecrets(roomSecrets) {
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
}
_onPairDeviceInitiate() {
if (!this._isConnected()) {
Events.fire('notify-user', 'You need to be online to pair devices.');
@@ -65,13 +71,13 @@ class ServerConnection {
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers':
Events.fire('peers', msg);
this._onPeers(msg);
break;
case 'peer-joined':
Events.fire('peer-joined', msg);
@@ -106,19 +112,41 @@ class ServerConnection {
case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret);
break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
default:
console.error('WS: unknown message type', msg);
console.error('WS receive: unknown message type', msg);
}
}
send(msg) {
if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg));
}
_onPeers(msg) {
Events.fire('peers', msg);
}
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
Events.fire('display-name', msg);
}
@@ -138,13 +166,19 @@ class ServerConnection {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
const peerId = sessionStorage.getItem("peerId");
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onDisconnect() {
@@ -157,7 +191,7 @@ class ServerConnection {
}
_onVisibilityChange() {
if (document.hidden) return;
if (window.hiddenProperty) return;
this._connect();
}
@@ -181,13 +215,18 @@ class ServerConnection {
class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
this._server = serverConnection;
this._isCaller = isCaller;
this._peerId = peerId;
this._roomType = roomType;
this._roomSecret = roomSecret;
this._updateRoomSecret(roomSecret);
this._filesQueue = [];
this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
}
sendJSON(message) {
@@ -198,12 +237,47 @@ class Peer {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
mime: file.type,
size: file.size,
};
_isSameBrowser() {
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
}
_updateRoomSecret(roomSecret) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
// remove old roomSecrets to prevent multiple pairings with same peer
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
})
}
this._roomSecret = roomSecret;
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
// increase security by increasing roomSecret length
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._roomSecret);
}
}
_evaluateAutoAccept() {
if (!this._roomSecret) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._roomSecret)
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = autoAccept;
}
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@@ -248,7 +322,11 @@ class Peer {
let imagesOnly = true
for (let i=0; i<files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push(await this.createHeader(files[i]));
header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
}
@@ -360,7 +438,7 @@ class Peer {
_onFilesTransferRequest(request) {
if (this._requestPending) {
// Only accept one request at a time
// Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false});
return;
}
@@ -372,6 +450,14 @@ class Peer {
}
this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
@@ -443,7 +529,7 @@ class Peer {
this._abortTransfer();
}
// include for compatibility with Snapdrop for Android app
// include for compatibility with 'Snapdrop & PairDrop for Android' app
Events.fire('file-received', fileBlob);
this._filesReceived.push(fileBlob);
@@ -461,6 +547,7 @@ class Peer {
if (!this._filesQueue.length) {
this._busy = false;
Events.fire('notify-user', 'File transfer completed.');
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} else {
this._dequeueFile();
}
@@ -497,34 +584,39 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
}
class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, isCaller, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
if (!this._isCaller) return; // we will listen for a caller
this._connect();
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
_connect() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
if (isCaller) {
if (this._isCaller) {
this._openChannel();
} else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
this._peerId = peerId;
_openConnection() {
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
@@ -556,7 +648,7 @@ class RTCPeer extends Peer {
}
onServerMessage(message) {
if (!this._conn) this._connect(message.sender.id, false);
if (!this._conn) this._connect();
if (message.sdp) {
this._conn.setRemoteDescription(message.sdp)
@@ -641,7 +733,7 @@ class RTCPeer extends Peer {
console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId);
if (!this._isCaller) return;
this._connect(this._peerId, true); // reopen the channel
this._connect(); // reopen the channel
}
_onConnectionStateChange() {
@@ -688,7 +780,11 @@ class RTCPeer extends Peer {
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
}
_isConnected() {
@@ -716,32 +812,63 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(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('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
}
_onMessage(message) {
// if different roomType -> abort
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return;
if (!this.peers[message.sender.id]) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
}
this.peers[message.sender.id].onServerMessage(message);
const peerId = message.sender.id;
this.peers[peerId].onServerMessage(message);
}
_onPeers(msg) {
msg.peers.forEach(peer => {
if (this.peers[peer.id]) {
// if different roomType -> abort
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
this.peers[peer.id].refresh();
return;
}
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
_refreshPeer(peer, roomType, roomSecret) {
if (!peer) return false;
const roomTypeIsSecret = roomType === "secret";
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
if (roomTypeIsSecret && roomSecretsDiffer) {
peer._updateRoomSecret(roomSecret);
peer._evaluateAutoAccept();
return true;
}
const roomTypesDiffer = peer._roomType !== roomType;
// if roomTypes differ peer is already connected -> abort
if (roomTypesDiffer) return true;
peer.refresh();
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomSecret);
return;
}
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomSecret);
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret);
})
}
@@ -769,10 +896,19 @@ class PeersManager {
this.peers[message.to].sendText(message.text);
}
_onPeerLeft(msg) {
if (msg.disconnect === true) {
// if user actively disconnected from PairDrop disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
_onPeerLeft(message) {
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', message.peerId);
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
if (!peerIds) return;
console.log("successfully removed other peerIds from localStorage");
});
}
}
}
@@ -792,12 +928,19 @@ class PeersManager {
_onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId);
}
}
}
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
@@ -813,6 +956,25 @@ class PeersManager {
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdFromRoomSecret(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
// peer must have same roomSecret and not be on the same browser.
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
return peer._peerId;
}
}
return false;
}
}

View File

@@ -50,7 +50,7 @@ class PeersUI {
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));
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
// Load saved display name on page load
this._getSavedDisplayName().then(displayName => {
@@ -87,26 +87,31 @@ class PeersUI {
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});
});
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: ''});
});
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: ''});
});
}
}
@@ -129,6 +134,12 @@ class PeersUI {
this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
this._redrawPeerRoomTypes(peerId);
}
_onPeerDisplayNameChanged(e) {
if (!e.detail.displayName) return;
this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);
}
_onKeyDown(e) {
@@ -142,26 +153,47 @@ class PeersUI {
}
_joinPeer(peer, roomType, roomSecret) {
peer.roomTypes = [roomType];
peer.roomSecret = roomSecret;
if (this.peers[peer.id]) {
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
this._redrawPeer(this.peers[peer.id]);
return; // peer already exists
const existingPeer = this.peers[peer.id];
if (existingPeer) {
// peer already exists. Abort but add roomType to GUI and update roomSecret
// skip if peer is a tab on the same browser
if (!existingPeer.sameBrowser()) {
// add roomType to PeerUI
if (!existingPeer.roomTypes.includes(roomType)) {
existingPeer.roomTypes.push(roomType);
}
this._redrawPeerRoomTypes(peer.id);
if (roomType === "secret") existingPeer.roomSecret = roomSecret;
}
return;
}
peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id);
if (!(roomType === "secret" && peer.sameBrowser())) {
peer.roomTypes = [roomType];
peer.roomSecret = roomSecret;
}
this.peers[peer.id] = peer;
}
_onPeerConnected(peerId, connectionHash) {
if(this.peers[peerId] && !$(peerId))
new PeerUI(this.peers[peerId], connectionHash);
if (!this.peers[peerId] || $(peerId)) return;
const peer = this.peers[peerId];
new PeerUI(peer, connectionHash);
}
_redrawPeer(peer) {
const peerNode = $(peer.id);
_redrawPeerRoomTypes(peerId) {
const peer = this.peers[peerId]
const peerNode = $(peerId);
if (!peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret');
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
if (!peer.sameBrowser()) {
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
}
}
evaluateOverflowing() {
@@ -187,7 +219,17 @@ class PeersUI {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer.roomSecret === roomSecret) {
let index = peer.roomTypes.indexOf('secret');
peer.roomTypes.splice(index, 1);
peer.roomSecret = "";
if (peer.roomTypes.length) {
this._redrawPeerRoomTypes(peerId)
return;
}
this._onPeerDisconnected(peerId);
return;
}
}
}
@@ -364,8 +406,8 @@ class PeerUI {
this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id;
this.$el.ui = this;
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.$el.classList.add('center');
if (!this._peer.sameBrowser()) this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.html();
this._callbackInput = e => this._onFilesSelected(e)
@@ -535,6 +577,10 @@ class Dialog {
if (this.$autoFocus) this.$autoFocus.focus();
}
isShown() {
return !!this.$el.attributes["show"];
}
hide() {
this.$el.removeAttribute('show');
if (this.$autoFocus) {
@@ -543,10 +589,11 @@ class Dialog {
}
document.title = 'PairDrop';
document.changeFavicon("images/favicon-96x96.png");
this.correspondingPeerId = undefined;
}
_onPeerDisconnected(peerId) {
if (this.correspondingPeerId === peerId) {
if (this.isShown() && this.correspondingPeerId === peerId) {
this.hide();
Events.fire('notify-user', 'Selected peer left.')
}
@@ -812,14 +859,14 @@ class ReceiveRequestDialog extends ReceiveDialog {
}
_onKeyDown(e) {
if (this.$el.attributes["show"] && e.code === "Escape") {
if (this.isShown() && e.code === "Escape") {
this._respondToFileTransferRequest(false);
}
}
_onRequestFileTransfer(request, peerId) {
this._filesTransferRequestQueue.push({request: request, peerId: peerId});
if (this.$el.attributes["show"]) return;
if (this.isShown()) return;
this._dequeueRequests();
}
@@ -862,8 +909,12 @@ class ReceiveRequestDialog extends ReceiveDialog {
}
hide() {
this.$previewBox.innerHTML = '';
// clear previewBox after dialog is closed
setTimeout(_ => this.$previewBox.innerHTML = '', 300);
super.hide();
// show next request
setTimeout(_ => this._dequeueRequests(), 500);
}
}
@@ -876,7 +927,7 @@ class PairDeviceDialog extends Dialog {
this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$pairDeviceBtn = $('pair-device');
this.$clearSecretsBtn = $('clear-pair-devices');
this.$editPairedDevicesBtn = $('edit-paired-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
this.$createJoinForm = this.$el.querySelector('form');
@@ -894,14 +945,18 @@ class PairDeviceDialog extends Dialog {
Events.on('ws-disconnected', _ => this.hide());
Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail));
Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail.peerId, e.detail.roomSecret));
Events.on('peers', e => this._onPeers(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid());
Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail));
Events.on('clear-room-secrets', e => this._onClearRoomSecrets(e.detail))
Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets())
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
this.$el.addEventListener('paste', e => this._onPaste(e));
this.evaluateRoomKeyChars();
this.evaluateUrlAttributes();
this.pairPeer = {};
}
_onCharsInput(e) {
@@ -917,7 +972,7 @@ class PairDeviceDialog extends Dialog {
}
_onKeyDown(e) {
if (this.$el.attributes["show"] && e.code === "Escape") {
if (this.isShown() && e.code === "Escape") {
// Timeout to prevent paste mode from getting cancelled simultaneously
setTimeout(_ => this._pairDeviceCancel(), 50);
}
@@ -969,16 +1024,14 @@ class PairDeviceDialog extends Dialog {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('room_key')) {
this._pairDeviceJoin(urlParams.get('room_key'));
window.history.replaceState({}, "title**", '/'); //remove room_key from url
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url); //remove room_key from url
}
}
_onWsConnected() {
this.$pairDeviceBtn.removeAttribute('hidden');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
this._evaluateNumberRoomSecrets();
}).catch(_ => PersistentStorage.logBrowserNotCapable());
this._evaluateNumberRoomSecrets();
}
_pairDeviceInitiate() {
@@ -1026,22 +1079,69 @@ class PairDeviceDialog extends Dialog {
}
_pairDeviceJoined(peerId, roomSecret) {
this.hide();
PersistentStorage.addRoomSecret(roomSecret).then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
const oldRoomSecret = $(peerId).ui.roomSecret;
if (oldRoomSecret) PersistentStorage.deleteRoomSecret(oldRoomSecret);
$(peerId).ui.roomSecret = roomSecret;
this._evaluateNumberRoomSecrets();
}).finally(_ => {
// skip adding to IndexedDB if peer is another tab on the same browser
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
this._cleanUp();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
PersistentStorage.logBrowserNotCapable();
this.hide();
Events.fire('notify-user', 'Pairing of two browser tabs is not possible.');
return;
}
// save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret
this.pairPeer = {
"peerId": peerId,
"roomSecret": roomSecret
};
}
_onPeers(message) {
if (!Object.keys(this.pairPeer)) return;
message.peers.forEach(messagePeer => {
this._evaluateJoinedPeer(messagePeer.id, message.roomSecret);
});
}
_onPeerJoined(message) {
if (!Object.keys(this.pairPeer)) return;
this._evaluateJoinedPeer(message.peer.id, message.roomSecret);
}
_evaluateJoinedPeer(peerId, roomSecret) {
const samePeerId = peerId === this.pairPeer.peerId;
const sameRoomSecret = roomSecret === this.pairPeer.roomSecret;
if (!peerId || !roomSecret || !samePeerId || !sameRoomSecret) return;
this._onPairPeerJoined(peerId, roomSecret);
this.pairPeer = {};
}
_onPairPeerJoined(peerId, roomSecret) {
// if devices are paired that are already connected we must save the names at this point
const $peer = $(peerId);
let displayName, deviceName;
if ($peer) {
displayName = $peer.ui._peer.name.displayName;
deviceName = $peer.ui._peer.name.deviceName;
}
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
.then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
this._evaluateNumberRoomSecrets();
})
.finally(_ => {
this._cleanUp();
this.hide();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
PersistentStorage.logBrowserNotCapable();
});
}
_pairDeviceJoinKeyInvalid() {
Events.fire('notify-user', 'Key not valid');
}
@@ -1062,58 +1162,123 @@ class PairDeviceDialog extends Dialog {
this.inputRoomKey = '';
this.$inputRoomKeyChars.forEach(el => el.value = '');
this.$inputRoomKeyChars.forEach(el => el.setAttribute("disabled", ""));
}
_onClearRoomSecrets() {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets-cleared', roomSecrets);
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('notify-user', 'All Devices unpaired.')
this._evaluateNumberRoomSecrets();
})
}).catch(_ => PersistentStorage.logBrowserNotCapable());
this.pairPeer = {};
}
_onSecretRoomDeleted(roomSecret) {
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
this._evaluateNumberRoomSecrets();
}).catch(e => console.error(e));
});
}
_evaluateNumberRoomSecrets() {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
if (roomSecrets.length > 0) {
this.$clearSecretsBtn.removeAttribute('hidden');
this.$editPairedDevicesBtn.removeAttribute('hidden');
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
} else {
this.$clearSecretsBtn.setAttribute('hidden', '');
this.$editPairedDevicesBtn.setAttribute('hidden', '');
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
}
Events.fire('bg-resize');
}).catch(_ => PersistentStorage.logBrowserNotCapable());
});
}
}
class ClearDevicesDialog extends Dialog {
class EditPairedDevicesDialog extends Dialog {
constructor() {
super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
super('edit-paired-devices-dialog');
this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');
$('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices());
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
Events.on('keydown', e => this._onKeyDown(e));
}
_onClearPairDevices() {
this.show();
_onKeyDown(e) {
if (this.isShown() && e.code === "Escape") {
this.hide();
}
}
_onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
async _initDOM() {
const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries();
roomSecretsEntries.forEach(roomSecretsEntry => {
let $pairedDevice = document.createElement('div');
$pairedDevice.classList = ["paired-device"];
$pairedDevice.innerHTML = `
<div class="display-name">
<span>${roomSecretsEntry.display_name}</span>
</div>
<div class="device-name">
<span>${roomSecretsEntry.device_name}</span>
</div>
<div class="button-wrapper">
<label class="auto-accept">auto-accept
<input type="checkbox" ${roomSecretsEntry.auto_accept ? "checked" : ""}>
</label>
<button class="button" type="button">unpair</button>
</div>`
$pairedDevice.querySelector('input[type="checkbox"]').addEventListener('click', e => {
PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked).then(roomSecretsEntry => {
Events.fire('auto-accept-updated', {
'roomSecret': roomSecretsEntry.entry.secret,
'autoAccept': e.target.checked
});
});
});
$pairedDevice.querySelector('button').addEventListener('click', e => {
PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret).then(roomSecret => {
Events.fire('room-secrets-deleted', [roomSecret]);
Events.fire('evaluate-number-room-secrets');
e.target.parentNode.parentNode.remove();
});
})
this.$pairedDevicesWrapper.appendChild($pairedDevice)
})
}
hide() {
super.hide();
setTimeout(_ => {
this.$pairedDevicesWrapper.innerHTML = ""
}, 300);
}
_onEditPairedDevices() {
this._initDOM().then(_ => this.show());
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets');
this.hide();
PersistentStorage.getAllRoomSecrets()
.then(roomSecrets => {
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('room-secrets-deleted', roomSecrets);
Events.fire('evaluate-number-room-secrets');
Events.fire('notify-user', 'All Devices unpaired.');
this.hide();
})
});
}
_onPeerDisplayNameChanged(e) {
const peerId = e.detail.peerId;
const peerNode = $(peerId);
if (!peerNode) return;
const peer = peerNode.ui._peer;
if (!peer.roomSecret) return;
PersistentStorage.updateRoomSecretNames(peer.roomSecret, peer.name.displayName, peer.name.deviceName).then(roomSecretEntry => {
console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`);
})
}
}
@@ -1131,18 +1296,18 @@ class SendTextDialog extends Dialog {
}
async _onKeyDown(e) {
if (this.$el.attributes["show"]) {
if (e.code === "Escape") {
this.hide();
} else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
if (this._textInputEmpty()) return;
this._send();
}
if (!this.isShown()) return;
if (e.code === "Escape") {
this.hide();
} else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
if (this._textInputEmpty()) return;
this._send();
}
}
_textInputEmpty() {
return this.$text.innerText === "\n";
return !this.$text.innerText || this.$text.innerText === "\n";
}
_onChange(e) {
@@ -1200,7 +1365,7 @@ class ReceiveTextDialog extends Dialog {
}
async _onKeyDown(e) {
if (this.$el.attributes["show"]) {
if (this.isShown()) {
if (e.code === "KeyC" && (e.ctrlKey || e.metaKey)) {
await this._onCopy()
this.hide();
@@ -1214,7 +1379,7 @@ class ReceiveTextDialog extends Dialog {
window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return;
if (this.isShown()) return;
this._dequeueRequests();
}
@@ -1255,7 +1420,8 @@ class ReceiveTextDialog extends Dialog {
}
async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent);
const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' ');
await navigator.clipboard.writeText(sanitizedText);
Events.fire('notify-user', 'Copied to clipboard');
this.hide();
}
@@ -1411,7 +1577,8 @@ class Base64ZipDialog extends Dialog {
}
clearBrowserHistory() {
window.history.replaceState({}, "Rewrite URL", '/');
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
}
hide() {
@@ -1430,7 +1597,7 @@ class Toast extends Dialog {
_onNotify(message) {
if (this.hideTimeout) clearTimeout(this.hideTimeout);
this.$el.textContent = message;
this.$el.innerText = message;
this.show();
this.hideTimeout = setTimeout(_ => this.hide(), 5000);
}
@@ -1627,7 +1794,8 @@ class WebShareTargetUI {
}
}
}
window.history.replaceState({}, "Rewrite URL", '/');
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
}
}
}
@@ -1651,7 +1819,8 @@ class WebFileHandlersUI {
Events.fire('activate-paste-mode', {files: files, text: ""})
launchParams = null;
});
window.history.replaceState({}, "Rewrite URL", '/');
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
}
}
}
@@ -1682,7 +1851,7 @@ class PersistentStorage {
PersistentStorage.logBrowserNotCapable();
return;
}
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 4);
DBOpenRequest.onerror = (e) => {
PersistentStorage.logBrowserNotCapable();
console.log('Error initializing database: ');
@@ -1693,27 +1862,32 @@ class PersistentStorage {
};
DBOpenRequest.onupgradeneeded = (e) => {
const db = e.target.result;
const txn = e.target.transaction;
db.onerror = e => console.log('Error loading database: ' + e);
try {
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
if (e.oldVersion === 0) {
// initiate v1
db.createObjectStore('keyval');
} catch (error) {
console.log("Object store named 'keyval' already exists")
let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });
}
try {
const roomSecretsObjectStore = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore.createIndex('secret', 'secret', { unique: true });
} catch (error) {
console.log("Object store named 'room_secrets' already exists")
if (e.oldVersion <= 1) {
// migrate to v2
db.createObjectStore('share_target_files');
}
try {
if (db.objectStoreNames.contains('share_target_files')) {
db.deleteObjectStore('share_target_files');
}
if (e.oldVersion <= 2) {
// migrate to v3
db.deleteObjectStore('share_target_files');
db.createObjectStore('share_target_files', {autoIncrement: true});
} catch (error) {
console.log("Object store named 'share_target_files' already exists")
}
if (e.oldVersion <= 3) {
// migrate to v4
let roomSecretsObjectStore4 = txn.objectStore('room_secrets');
roomSecretsObjectStore4.createIndex('display_name', 'display_name');
roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');
}
}
}
@@ -1746,7 +1920,7 @@ class PersistentStorage {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('keyval', 'readwrite');
const transaction = db.transaction('keyval', 'readonly');
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.get(key);
objectStoreRequest.onsuccess = _ => {
@@ -1779,16 +1953,21 @@ class PersistentStorage {
})
}
static addRoomSecret(roomSecret) {
static addRoomSecret(roomSecret, displayName, deviceName) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.add({'secret': roomSecret});
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. RoomSecret added: ${roomSecret}`);
const objectStoreRequest = objectStore.add({
'secret': roomSecret,
'display_name': displayName,
'device_name': deviceName,
'auto_accept': false
});
objectStoreRequest.onsuccess = e => {
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
resolve();
}
}
@@ -1798,21 +1977,31 @@ class PersistentStorage {
})
}
static getAllRoomSecrets() {
static async getAllRoomSecrets() {
try {
const roomSecrets = await this.getAllRoomSecretEntries();
let secrets = [];
for (let i = 0; i < roomSecrets.length; i++) {
secrets.push(roomSecrets[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
return(secrets);
} catch (e) {
this.logBrowserNotCapable();
return false;
}
}
static getAllRoomSecretEntries() {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.getAll();
objectStoreRequest.onsuccess = e => {
let secrets = [];
for (let i=0; i<e.target.result.length; i++) {
secrets.push(e.target.result[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
resolve(secrets);
resolve(e.target.result);
}
}
DBOpenRequest.onerror = (e) => {
@@ -1821,24 +2010,59 @@ class PersistentStorage {
});
}
static deleteRoomSecret(room_secret) {
static getRoomSecretEntry(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
const key = e.target.result;
if (!key) {
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestRetrieval = objectStore.get(key);
objectStoreRequestRetrieval.onsuccess = e => {
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
resolve({
"entry": e.target.result,
"key": key
});
}
objectStoreRequestRetrieval.onerror = (e) => {
reject(e);
}
};
}
DBOpenRequest.onerror = (e) => {
reject(e);
}
});
}
static deleteRoomSecret(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(room_secret);
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
if (!e.target.result) {
console.log(`Nothing to delete. room_secret not existing: ${room_secret}`);
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestDeletion = objectStore.delete(e.target.result);
const key = e.target.result;
const objectStoreRequestDeletion = objectStore.delete(key);
objectStoreRequestDeletion.onsuccess = _ => {
console.log(`Request successful. Deleted room_secret: ${room_secret}`);
resolve();
console.log(`Request successful. Deleted room_secret: ${key}`);
resolve(roomSecret);
}
objectStoreRequestDeletion.onerror = (e) => {
reject(e);
@@ -1869,22 +2093,116 @@ class PersistentStorage {
}
})
}
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
}
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
}
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
this.getRoomSecretEntry(roomSecret)
.then(roomSecretEntry => {
if (!roomSecretEntry) {
resolve(false);
return;
}
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
const updatedRoomSecretEntry = {
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
};
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
objectStoreRequestUpdate.onsuccess = e => {
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
resolve({
"entry": updatedRoomSecretEntry,
"key": roomSecretEntry.key
});
}
objectStoreRequestUpdate.onerror = (e) => {
reject(e);
}
})
.catch(e => reject(e));
};
DBOpenRequest.onerror = e => reject(e);
})
}
}
class Broadcast {
class BrowserTabsConnector {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
}
_broadcastMessage(message) {
_broadcastSend(message) {
this.bc.postMessage(message);
}
_onMessage(e) {
console.log('Broadcast message received:', e.data)
Events.fire(e.data.type, e.data.detail);
console.log('Broadcast:', e.data)
switch (e.data.type) {
case 'self-display-name-changed':
Events.fire('self-display-name-changed', e.data.detail);
break;
}
}
static peerIsSameBrowser(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
return peerIdsBrowser
? peerIdsBrowser.indexOf(peerId) !== -1
: false;
}
static async addPeerIdToLocalStorage() {
const peerId = sessionStorage.getItem("peerId");
if (!peerId) return false;
let peerIdsBrowser = [];
let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peerIdsBrowser"));
if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);
peerIdsBrowser.push(peerId);
peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerIdsBrowser;
}
static async removePeerIdFromLocalStorage(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
const index = peerIdsBrowser.indexOf(peerId);
peerIdsBrowser.splice(index, 1);
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerId;
}
static async removeOtherPeerIdsFromLocalStorage() {
const peerId = sessionStorage.getItem("peerId");
if (!peerId) return false;
let peerIdsBrowser = [peerId];
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerIdsBrowser;
}
}
@@ -1899,7 +2217,7 @@ class PairDrop {
const sendTextDialog = new SendTextDialog();
const receiveTextDialog = new ReceiveTextDialog();
const pairDeviceDialog = new PairDeviceDialog();
const clearDevicesDialog = new ClearDevicesDialog();
const clearDevicesDialog = new EditPairedDevicesDialog();
const base64ZipDialog = new Base64ZipDialog();
const toast = new Toast();
const notifications = new Notifications();
@@ -1907,7 +2225,7 @@ class PairDrop {
const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI();
const broadCast = new Broadcast();
const broadCast = new BrowserTabsConnector();
});
}
}
@@ -1936,14 +2254,7 @@ window.addEventListener('beforeinstallprompt', e => {
// Background Circles
Events.on('load', () => {
let c = document.createElement('canvas');
let style = c.style;
style.width = '100%';
style.position = 'absolute';
style.zIndex = -1;
style.top = 0;
style.left = 0;
style.animation = "fade-in 800ms";
let c = $$('canvas');
let cCtx = c.getContext('2d');
let x0, y0, w, h, dw, offset;
@@ -1964,11 +2275,7 @@ Events.on('load', () => {
y0 = h - offset;
dw = Math.round(Math.max(w, h, 1000) / 13);
if (document.body.contains(c)) {
document.body.removeChild(c);
}
drawCircles(cCtx, dw);
document.body.appendChild(c);
}
Events.on('bg-resize', _ => init());
@@ -1984,6 +2291,7 @@ Events.on('load', () => {
}
function drawCircles(ctx, frame) {
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < 13; i++) {
drawCircle(ctx, dw * i + frame + 33);
}

View File

@@ -5,7 +5,7 @@ if (!navigator.clipboard) {
// A <span> contains the text to copy
const span = document.createElement('span');
span.textContent = text;
span.innerText = text;
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
// Paint the span outside the viewport
@@ -398,3 +398,11 @@ const cyrb53 = function(str, seed = 0) {
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
function getUrlWithoutArguments() {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.6.3';
const cacheVersion = 'v1.7.6';
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',
@@ -72,8 +72,7 @@ self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") {
// Requests related to Web Share Target.
event.respondWith((async () => {
let share_url = await evaluateRequestData(event.request);
share_url = event.request.url + share_url;
const share_url = await evaluateRequestData(event.request);
return Response.redirect(encodeURI(share_url), 302);
})());
} else {
@@ -101,15 +100,16 @@ self.addEventListener('activate', evt =>
)
);
const evaluateRequestData = async function (request) {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const evaluateRequestData = function (request) {
return new Promise(async (resolve) => {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const pairDropUrl = request.url;
if (files && files.length > 0) {
let fileObjects = [];
for (let i=0; i<files.length; i++) {
@@ -128,21 +128,21 @@ const evaluateRequestData = async function (request) {
const objectStoreRequest = objectStore.add(fileObjects[i]);
objectStoreRequest.onsuccess = _ => {
if (i === fileObjects.length - 1) resolve('?share-target=files');
if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share-target=files');
}
}
}
DBOpenRequest.onerror = _ => {
resolve('');
resolve(pairDropUrl);
}
} else {
let share_url = '?share-target=text';
let urlArgument = '?share-target=text';
if (title) share_url += `&title=${title}`;
if (text) share_url += `&text=${text}`;
if (url) share_url += `&url=${url}`;
if (title) urlArgument += `&title=${title}`;
if (text) urlArgument += `&text=${text}`;
if (url) urlArgument += `&url=${url}`;
resolve(share_url);
resolve(pairDropUrl + urlArgument);
}
});
}

View File

@@ -19,6 +19,10 @@ body {
overflow-x: hidden;
overscroll-behavior: none;
overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
body {
@@ -215,6 +219,14 @@ hr {
color: white;
}
input {
cursor: pointer;
}
input[type="checkbox"] {
min-width: 13px;
}
x-noscript {
background: var(--primary-color);
color: white;
@@ -273,7 +285,7 @@ x-noscript {
}
@media screen and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center {
header:has(#edit-pair-devices:not([hidden]))~#center {
--footer-height: 150px;
}
}
@@ -442,8 +454,6 @@ x-no-peers[drop-bg] * {
/* Peer */
x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
@@ -530,7 +540,6 @@ x-peer[status] x-icon {
.status,
.device-name,
.connection-hash {
height: 18px;
opacity: 0.7;
}
@@ -691,6 +700,12 @@ x-dialog x-paper {
height: 625px;
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
}
x-dialog:not([show]) {
pointer-events: none;
}
@@ -712,7 +727,7 @@ x-dialog .font-subheading {
margin-bottom: 5px;
}
/* PairDevicesDialog */
/* Pair Devices Dialog */
#key-input-container {
width: 100%;
@@ -720,7 +735,7 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input {
#key-input-container > input {
width: 45px;
height: 45px;
font-size: 30px;
@@ -736,15 +751,18 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input + * {
#key-input-container > input + * {
margin-left: 6px;
}
#key-input-container>input:nth-of-type(4) {
#key-input-container > input:nth-of-type(4) {
margin-left: 5%;
}
#room-key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block;
@@ -756,14 +774,106 @@ x-dialog .font-subheading {
margin: 16px;
}
#pair-device-dialog hr {
margin: 40px -24px;
x-dialog hr {
margin: 40px -24px 30px -24px;
border: solid 1.25px var(--border-color);
}
#pair-device-dialog x-background {
padding: 16px!important;
}
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: "No paired devices.";
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
.paired-device > .auto-accept {
cursor: pointer;
}
/* Receive Dialog */
x-dialog .row {
@@ -812,7 +922,7 @@ x-paper > div:last-child > .button:not(:last-child) {
}
/* Send Text Dialog */
/* Todo: add pair underline to send / receive dialogs displayName */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
@@ -830,9 +940,9 @@ x-dialog .dialog-subheader {
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
white-space: pre-wrap;
padding: 15px 0;
}
@@ -1014,11 +1124,14 @@ button::-moz-focus-inner {
text-align: center;
}
#about header {
z-index: 1;
}
#about .fade-in {
transition: opacity 300ms;
will-change: opacity;
transition-delay: 300ms;
z-index: 11;
pointer-events: all;
}
@@ -1032,12 +1145,23 @@ button::-moz-focus-inner {
--icon-size: 96px;
}
#about .title-wrapper {
display: flex;
align-items: baseline;
}
#about .title-wrapper > div {
margin-left: 0.5em;
}
#about x-background {
position: absolute;
top: calc(28px - 250px);
right: calc(36px - 250px);
width: 500px;
height: 500px;
--size: max(max(230vw, 230vh), calc(150vh + 150vw));
--size-half: calc(var(--size)/2);
top: calc(28px - var(--size-half));
right: calc(36px - var(--size-half));
width: var(--size);
height: var(--size);
border-radius: 50%;
background: var(--primary-color);
transform: scale(0);
@@ -1051,7 +1175,7 @@ button::-moz-focus-inner {
}
#about:target x-background {
transform: scale(10);
transform: scale(1);
}
#about .row a {
@@ -1066,6 +1190,14 @@ button::-moz-focus-inner {
align-self: end;
}
canvas.circles {
width: 100vw;
position: absolute;
z-index: -10;
top: 0;
left: 0;
}
/* Loading Indicator */
.progress {
@@ -1174,27 +1306,10 @@ x-peers:empty~x-instructions {
@media (hover: none) and (pointer: coarse) {
x-peer {
transform: scale(0.95);
padding: 4px 0;
padding: 4px;
}
}
#websocket-fallback {
margin-left: 5px;
margin-right: 5px;
padding: 5px;
text-align: center;
opacity: 0.5;
transition: opacity 300ms;
}
#websocket-fallback>span {
margin: 2px;
}
#websocket-fallback > span > span {
border-bottom: solid 4px var(--ws-peer-color);
}
/* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
@@ -1337,3 +1452,9 @@ x-dialog x-paper {
background: #bfbfbf;
border-radius: 4px;
}
::-moz-selection,
::selection {
color: black;
background: var(--primary-color);
}

View File

@@ -78,9 +78,9 @@
<use xlink:href="#pair-device-icon" />
</svg>
</div>
<div id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
<svg class="icon">
<use xlink:href="#clear-pair-devices-icon" />
<use xlink:href="#edit-pair-devices-icon" />
</svg>
</div>
<div id="cancel-paste-mode" class="button" hidden>Done</div>
@@ -145,16 +145,18 @@
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clear-devices-dialog">
<!-- Edit Paired Devices Dialog -->
<x-dialog id="edit-paired-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<h2 class="center">Edit Paired Devices</h2>
<div class="paired-devices-wrapper"></div>
<div class="font-subheading center">
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" type="button" close>Cancel</button>
<button class="button" type="button" close>Close</button>
</div>
</x-paper>
</x-background>
@@ -225,7 +227,7 @@
<div class="row-separator"></div>
<div id="text-input" title="Message" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" type="submit" title="CTRL/⌘ + ENTER" disabled>Send</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
@@ -277,7 +279,10 @@
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<h1>PairDrop</h1>
<div class="title-wrapper">
<h1>PairDrop</h1>
<div class="font-subheading">v1.7.6</div>
</div>
<div class="font-subheading">The easiest way to transfer files across devices</div>
<div class="row">
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop" title="PairDrop on Github" rel="noreferrer">
@@ -304,6 +309,7 @@
</section>
<x-background></x-background>
</x-about>
<canvas class="circles"></canvas>
<!-- SVG Icon Library -->
<svg style="display: none;">
<symbol id=wifi-tethering viewBox="0 0 24 24">
@@ -358,9 +364,11 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.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 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.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.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
</symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
<!--! 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"/>
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.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.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-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.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
</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. -->

View File

@@ -1,16 +1,26 @@
window.URL = window.URL || window.webkitURL;
window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
Events.on('room-secrets', e => this.send({ type: 'room-secrets', roomSecrets: e.detail }));
Events.on('join-ip-room', e => this.send({ type: 'join-ip-room'}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
@@ -37,10 +47,6 @@ class ServerConnection {
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
}
_sendRoomSecrets(roomSecrets) {
this.send({ type: 'room-secrets', roomSecrets: roomSecrets });
}
_onPairDeviceInitiate() {
if (!this._isConnected()) {
Events.fire('notify-user', 'You need to be online to pair devices.');
@@ -63,13 +69,13 @@ class ServerConnection {
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers':
Events.fire('peers', msg);
this._onPeers(msg);
break;
case 'peer-joined':
Events.fire('peer-joined', msg);
@@ -104,6 +110,9 @@ class ServerConnection {
case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret);
break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
case 'request':
case 'header':
case 'partition':
@@ -118,18 +127,43 @@ class ServerConnection {
Events.fire('ws-relay', JSON.stringify(msg));
break;
default:
console.error('WS: unknown message type', msg);
console.error('WS receive: unknown message type', msg);
}
}
send(msg) {
if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg));
}
_onPeers(msg) {
Events.fire('peers', msg);
if (msg.roomType === "ip" && msg.peers.length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully removed other peerIds from localStorage");
});
}
}
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId to localStorage to mark it for other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (!peerId) return;
console.log("successfully added peerId to localStorage");
// Only now join rooms
Events.fire('join-ip-room');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
});
});
Events.fire('display-name', msg);
}
@@ -149,13 +183,19 @@ class ServerConnection {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
const peerId = sessionStorage.getItem("peerId");
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onDisconnect() {
@@ -168,7 +208,7 @@ class ServerConnection {
}
_onVisibilityChange() {
if (document.hidden) return;
if (window.hiddenProperty) return;
this._connect();
}
@@ -192,13 +232,18 @@ class ServerConnection {
class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
this._server = serverConnection;
this._isCaller = isCaller;
this._peerId = peerId;
this._roomType = roomType;
this._roomSecret = roomSecret;
this._updateRoomSecret(roomSecret);
this._filesQueue = [];
this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
}
sendJSON(message) {
@@ -209,12 +254,47 @@ class Peer {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
mime: file.type,
size: file.size,
};
_isSameBrowser() {
return BrowserTabsConnector.peerIsSameBrowser(this._peerId);
}
_updateRoomSecret(roomSecret) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> do not delete duplicates and do not regenerate room secrets
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret !== roomSecret) {
// remove old roomSecrets to prevent multiple pairings with same peer
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
})
}
this._roomSecret = roomSecret;
if (!this._isSameBrowser() && this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
// increase security by increasing roomSecret length
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._roomSecret);
}
}
_evaluateAutoAccept() {
if (!this._roomSecret) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._roomSecret)
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = autoAccept;
}
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@@ -259,7 +339,11 @@ class Peer {
let imagesOnly = true
for (let i=0; i<files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push(await this.createHeader(files[i]));
header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
}
@@ -371,7 +455,7 @@ class Peer {
_onFilesTransferRequest(request) {
if (this._requestPending) {
// Only accept one request at a time
// Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false});
return;
}
@@ -383,6 +467,14 @@ class Peer {
}
this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
@@ -508,34 +600,39 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
}
class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, isCaller, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
if (!this._isCaller) return; // we will listen for a caller
this._connect();
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
_connect() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection();
if (isCaller) {
if (this._isCaller) {
this._openChannel();
} else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
this._peerId = peerId;
_openConnection() {
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
@@ -567,7 +664,7 @@ class RTCPeer extends Peer {
}
onServerMessage(message) {
if (!this._conn) this._connect(message.sender.id, false);
if (!this._conn) this._connect();
if (message.sdp) {
this._conn.setRemoteDescription(message.sdp)
@@ -652,7 +749,7 @@ class RTCPeer extends Peer {
console.log('RTC: channel closed', this._peerId);
Events.fire('peer-disconnected', this._peerId);
if (!this._isCaller) return;
this._connect(this._peerId, true); // reopen the channel
this._connect(); // reopen the channel
}
_onConnectionStateChange() {
@@ -699,7 +796,11 @@ class RTCPeer extends Peer {
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
}
_isConnected() {
@@ -718,11 +819,10 @@ class RTCPeer extends Peer {
class WSPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
constructor(serverConnection, isCaller, peerId, roomType, roomSecret) {
super(serverConnection, isCaller, peerId, roomType, roomSecret);
this.rtcSupported = false;
if (!peerId) return; // we will listen for a caller
this._isCaller = true;
if (!this._isCaller) return; // we will listen for a caller
this._sendSignal();
}
@@ -734,7 +834,6 @@ class WSPeer extends Peer {
}
sendJSON(message) {
console.debug(message)
message.to = this._peerId;
message.roomType = this._roomType;
message.roomSecret = this._roomSecret;
@@ -769,49 +868,76 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(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('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail));
}
_onMessage(message) {
// if different roomType -> abort
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return;
if (!this.peers[message.sender.id]) {
if (window.isRtcSupported && message.sender.rtcSupported) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
} else {
this.peers[message.sender.id] = new WSPeer(this._server, undefined, message.roomType, message.roomSecret);
}
const peerId = message.sender.id;
this.peers[peerId].onServerMessage(message);
}
_refreshPeer(peer, roomType, roomSecret) {
if (!peer) return false;
const roomTypeIsSecret = roomType === "secret";
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
if (roomTypeIsSecret && roomSecretsDiffer) {
peer._updateRoomSecret(roomSecret);
peer._evaluateAutoAccept();
return true;
}
this.peers[message.sender.id].onServerMessage(message);
const roomTypesDiffer = peer._roomType !== roomType;
// if roomTypes differ peer is already connected -> abort
if (roomTypesDiffer) return true;
peer.refresh();
return true;
}
_createOrRefreshPeer(isCaller, peerId, roomType, roomSecret, rtcSupported) {
const peer = this.peers[peerId];
if (peer) {
this._refreshPeer(peer, roomType, roomSecret);
return;
}
if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomSecret);
} else {
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomSecret);
}
}
_onPeerJoined(message) {
this._createOrRefreshPeer(false, message.peer.id, message.roomType, message.roomSecret, message.peer.rtcSupported);
}
_onPeers(message) {
message.peers.forEach(peer => {
this._createOrRefreshPeer(true, peer.id, message.roomType, message.roomSecret, peer.rtcSupported);
})
}
_onWsRelay(message) {
const messageJSON = JSON.parse(message)
const messageJSON = JSON.parse(message);
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message)
}
_onPeers(msg) {
msg.peers.forEach(peer => {
if (this.peers[peer.id]) {
// if different roomType -> abort
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
this.peers[peer.id].refresh();
return;
}
if (window.isRtcSupported && peer.rtcSupported) {
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
} else {
this.peers[peer.id] = new WSPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
}
})
this.peers[messageJSON.sender.id]._onMessage(message);
}
_onRespondToFileTransferRequest(detail) {
@@ -838,13 +964,22 @@ class PeersManager {
this.peers[message.to].sendText(message.text);
}
_onPeerLeft(msg) {
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', msg.peerId);
Events.fire('peer-disconnected', msg.peerId);
} else if (msg.disconnect === true) {
_onPeerLeft(message) {
if (this.peers[message.peerId] && (!this.peers[message.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', message.peerId);
}
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
Events.fire('peer-disconnected', message.peerId);
// If no peers are connected anymore, we can safely assume that no other tab on the same browser is connected:
// Tidy up peerIds in localStorage
if (Object.keys(this.peers).length === 0) {
BrowserTabsConnector.removeOtherPeerIdsFromLocalStorage().then(peerIds => {
if (!peerIds) return;
console.log("successfully removed other peerIds from localStorage");
});
}
}
}
@@ -854,7 +989,6 @@ class PeersManager {
_onWsDisconnected() {
for (const peerId in this.peers) {
console.debug(this.peers[peerId].rtcSupported);
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
Events.fire('peer-disconnected', peerId);
}
@@ -873,12 +1007,19 @@ class PeersManager {
_onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId);
}
}
}
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
@@ -894,6 +1035,25 @@ class PeersManager {
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdFromRoomSecret(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
// peer must have same roomSecret and not be on the same browser.
if (peer._roomSecret === roomSecret && !peer._isSameBrowser()) {
return peer._peerId;
}
}
return false;
}
}

View File

@@ -50,7 +50,7 @@ class PeersUI {
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));
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
// Load saved display name on page load
this._getSavedDisplayName().then(displayName => {
@@ -87,26 +87,31 @@ class PeersUI {
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});
});
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: ''});
});
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: ''});
});
}
}
@@ -129,6 +134,12 @@ class PeersUI {
this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
this._redrawPeerRoomTypes(peerId);
}
_onPeerDisplayNameChanged(e) {
if (!e.detail.displayName) return;
this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);
}
_onKeyDown(e) {
@@ -142,26 +153,47 @@ class PeersUI {
}
_joinPeer(peer, roomType, roomSecret) {
peer.roomTypes = [roomType];
peer.roomSecret = roomSecret;
if (this.peers[peer.id]) {
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
this._redrawPeer(this.peers[peer.id]);
return; // peer already exists
const existingPeer = this.peers[peer.id];
if (existingPeer) {
// peer already exists. Abort but add roomType to GUI and update roomSecret
// skip if peer is a tab on the same browser
if (!existingPeer.sameBrowser()) {
// add roomType to PeerUI
if (!existingPeer.roomTypes.includes(roomType)) {
existingPeer.roomTypes.push(roomType);
}
this._redrawPeerRoomTypes(peer.id);
if (roomType === "secret") existingPeer.roomSecret = roomSecret;
}
return;
}
peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id);
if (!(roomType === "secret" && peer.sameBrowser())) {
peer.roomTypes = [roomType];
peer.roomSecret = roomSecret;
}
this.peers[peer.id] = peer;
}
_onPeerConnected(peerId, connectionHash) {
if(this.peers[peerId] && !$(peerId))
new PeerUI(this.peers[peerId], connectionHash);
if (!this.peers[peerId] || $(peerId)) return;
const peer = this.peers[peerId];
new PeerUI(peer, connectionHash);
}
_redrawPeer(peer) {
const peerNode = $(peer.id);
_redrawPeerRoomTypes(peerId) {
const peer = this.peers[peerId]
const peerNode = $(peerId);
if (!peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret');
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
if (!peer.sameBrowser()) {
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
}
}
evaluateOverflowing() {
@@ -187,7 +219,17 @@ class PeersUI {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer.roomSecret === roomSecret) {
let index = peer.roomTypes.indexOf('secret');
peer.roomTypes.splice(index, 1);
peer.roomSecret = "";
if (peer.roomTypes.length) {
this._redrawPeerRoomTypes(peerId)
return;
}
this._onPeerDisconnected(peerId);
return;
}
}
}
@@ -364,8 +406,9 @@ class PeerUI {
this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id;
this.$el.ui = this;
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.$el.classList.add('center');
if (!this._peer.sameBrowser()) this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer');
this.html();
this._callbackInput = e => this._onFilesSelected(e)
@@ -535,6 +578,10 @@ class Dialog {
if (this.$autoFocus) this.$autoFocus.focus();
}
isShown() {
return !!this.$el.attributes["show"];
}
hide() {
this.$el.removeAttribute('show');
if (this.$autoFocus) {
@@ -543,10 +590,11 @@ class Dialog {
}
document.title = 'PairDrop';
document.changeFavicon("images/favicon-96x96.png");
this.correspondingPeerId = undefined;
}
_onPeerDisconnected(peerId) {
if (this.correspondingPeerId === peerId) {
if (this.isShown() && this.correspondingPeerId === peerId) {
this.hide();
Events.fire('notify-user', 'Selected peer left.')
}
@@ -812,14 +860,14 @@ class ReceiveRequestDialog extends ReceiveDialog {
}
_onKeyDown(e) {
if (this.$el.attributes["show"] && e.code === "Escape") {
if (this.isShown() && e.code === "Escape") {
this._respondToFileTransferRequest(false);
}
}
_onRequestFileTransfer(request, peerId) {
this._filesTransferRequestQueue.push({request: request, peerId: peerId});
if (this.$el.attributes["show"]) return;
if (this.isShown()) return;
this._dequeueRequests();
}
@@ -862,8 +910,12 @@ class ReceiveRequestDialog extends ReceiveDialog {
}
hide() {
this.$previewBox.innerHTML = '';
// clear previewBox after dialog is closed
setTimeout(_ => this.$previewBox.innerHTML = '', 300);
super.hide();
// show next request
setTimeout(_ => this._dequeueRequests(), 500);
}
}
@@ -876,7 +928,7 @@ class PairDeviceDialog extends Dialog {
this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$pairDeviceBtn = $('pair-device');
this.$clearSecretsBtn = $('clear-pair-devices');
this.$editPairedDevicesBtn = $('edit-paired-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
this.$createJoinForm = this.$el.querySelector('form');
@@ -894,14 +946,18 @@ class PairDeviceDialog extends Dialog {
Events.on('ws-disconnected', _ => this.hide());
Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail));
Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail.peerId, e.detail.roomSecret));
Events.on('peers', e => this._onPeers(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid());
Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail));
Events.on('clear-room-secrets', e => this._onClearRoomSecrets(e.detail))
Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets())
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
this.$el.addEventListener('paste', e => this._onPaste(e));
this.evaluateRoomKeyChars();
this.evaluateUrlAttributes();
this.pairPeer = {};
}
_onCharsInput(e) {
@@ -917,7 +973,7 @@ class PairDeviceDialog extends Dialog {
}
_onKeyDown(e) {
if (this.$el.attributes["show"] && e.code === "Escape") {
if (this.isShown() && e.code === "Escape") {
// Timeout to prevent paste mode from getting cancelled simultaneously
setTimeout(_ => this._pairDeviceCancel(), 50);
}
@@ -969,16 +1025,14 @@ class PairDeviceDialog extends Dialog {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('room_key')) {
this._pairDeviceJoin(urlParams.get('room_key'));
window.history.replaceState({}, "title**", '/'); //remove room_key from url
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url); //remove room_key from url
}
}
_onWsConnected() {
this.$pairDeviceBtn.removeAttribute('hidden');
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
this._evaluateNumberRoomSecrets();
}).catch(_ => PersistentStorage.logBrowserNotCapable());
this._evaluateNumberRoomSecrets();
}
_pairDeviceInitiate() {
@@ -1026,22 +1080,69 @@ class PairDeviceDialog extends Dialog {
}
_pairDeviceJoined(peerId, roomSecret) {
this.hide();
PersistentStorage.addRoomSecret(roomSecret).then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
const oldRoomSecret = $(peerId).ui.roomSecret;
if (oldRoomSecret) PersistentStorage.deleteRoomSecret(oldRoomSecret);
$(peerId).ui.roomSecret = roomSecret;
this._evaluateNumberRoomSecrets();
}).finally(_ => {
// skip adding to IndexedDB if peer is another tab on the same browser
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
this._cleanUp();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
PersistentStorage.logBrowserNotCapable();
this.hide();
Events.fire('notify-user', 'Pairing of two browser tabs is not possible.');
return;
}
// save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret
this.pairPeer = {
"peerId": peerId,
"roomSecret": roomSecret
};
}
_onPeers(message) {
if (!Object.keys(this.pairPeer)) return;
message.peers.forEach(messagePeer => {
this._evaluateJoinedPeer(messagePeer.id, message.roomSecret);
});
}
_onPeerJoined(message) {
if (!Object.keys(this.pairPeer)) return;
this._evaluateJoinedPeer(message.peer.id, message.roomSecret);
}
_evaluateJoinedPeer(peerId, roomSecret) {
const samePeerId = peerId === this.pairPeer.peerId;
const sameRoomSecret = roomSecret === this.pairPeer.roomSecret;
if (!peerId || !roomSecret || !samePeerId || !sameRoomSecret) return;
this._onPairPeerJoined(peerId, roomSecret);
this.pairPeer = {};
}
_onPairPeerJoined(peerId, roomSecret) {
// if devices are paired that are already connected we must save the names at this point
const $peer = $(peerId);
let displayName, deviceName;
if ($peer) {
displayName = $peer.ui._peer.name.displayName;
deviceName = $peer.ui._peer.name.deviceName;
}
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
.then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
this._evaluateNumberRoomSecrets();
})
.finally(_ => {
this._cleanUp();
this.hide();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
PersistentStorage.logBrowserNotCapable();
});
}
_pairDeviceJoinKeyInvalid() {
Events.fire('notify-user', 'Key not valid');
}
@@ -1062,58 +1163,123 @@ class PairDeviceDialog extends Dialog {
this.inputRoomKey = '';
this.$inputRoomKeyChars.forEach(el => el.value = '');
this.$inputRoomKeyChars.forEach(el => el.setAttribute("disabled", ""));
}
_onClearRoomSecrets() {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets-cleared', roomSecrets);
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('notify-user', 'All Devices unpaired.')
this._evaluateNumberRoomSecrets();
})
}).catch(_ => PersistentStorage.logBrowserNotCapable());
this.pairPeer = {};
}
_onSecretRoomDeleted(roomSecret) {
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
this._evaluateNumberRoomSecrets();
}).catch(e => console.error(e));
});
}
_evaluateNumberRoomSecrets() {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
if (roomSecrets.length > 0) {
this.$clearSecretsBtn.removeAttribute('hidden');
this.$editPairedDevicesBtn.removeAttribute('hidden');
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
} else {
this.$clearSecretsBtn.setAttribute('hidden', '');
this.$editPairedDevicesBtn.setAttribute('hidden', '');
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
}
Events.fire('bg-resize');
}).catch(_ => PersistentStorage.logBrowserNotCapable());
});
}
}
class ClearDevicesDialog extends Dialog {
class EditPairedDevicesDialog extends Dialog {
constructor() {
super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
super('edit-paired-devices-dialog');
this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');
$('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices());
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
Events.on('keydown', e => this._onKeyDown(e));
}
_onClearPairDevices() {
this.show();
_onKeyDown(e) {
if (this.isShown() && e.code === "Escape") {
this.hide();
}
}
_onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
async _initDOM() {
const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries();
roomSecretsEntries.forEach(roomSecretsEntry => {
let $pairedDevice = document.createElement('div');
$pairedDevice.classList = ["paired-device"];
$pairedDevice.innerHTML = `
<div class="display-name">
<span>${roomSecretsEntry.display_name}</span>
</div>
<div class="device-name">
<span>${roomSecretsEntry.device_name}</span>
</div>
<div class="button-wrapper">
<label class="auto-accept">auto-accept
<input type="checkbox" ${roomSecretsEntry.auto_accept ? "checked" : ""}>
</label>
<button class="button" type="button">unpair</button>
</div>`
$pairedDevice.querySelector('input[type="checkbox"]').addEventListener('click', e => {
PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked).then(roomSecretsEntry => {
Events.fire('auto-accept-updated', {
'roomSecret': roomSecretsEntry.entry.secret,
'autoAccept': e.target.checked
});
});
});
$pairedDevice.querySelector('button').addEventListener('click', e => {
PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret).then(roomSecret => {
Events.fire('room-secrets-deleted', [roomSecret]);
Events.fire('evaluate-number-room-secrets');
e.target.parentNode.parentNode.remove();
});
})
this.$pairedDevicesWrapper.appendChild($pairedDevice)
})
}
hide() {
super.hide();
setTimeout(_ => {
this.$pairedDevicesWrapper.innerHTML = ""
}, 300);
}
_onEditPairedDevices() {
this._initDOM().then(_ => this.show());
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets');
this.hide();
PersistentStorage.getAllRoomSecrets()
.then(roomSecrets => {
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('room-secrets-deleted', roomSecrets);
Events.fire('evaluate-number-room-secrets');
Events.fire('notify-user', 'All Devices unpaired.');
this.hide();
})
});
}
_onPeerDisplayNameChanged(e) {
const peerId = e.detail.peerId;
const peerNode = $(peerId);
if (!peerNode) return;
const peer = peerNode.ui._peer;
if (!peer.roomSecret) return;
PersistentStorage.updateRoomSecretNames(peer.roomSecret, peer.name.displayName, peer.name.deviceName).then(roomSecretEntry => {
console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`);
})
}
}
@@ -1131,18 +1297,18 @@ class SendTextDialog extends Dialog {
}
async _onKeyDown(e) {
if (this.$el.attributes["show"]) {
if (e.code === "Escape") {
this.hide();
} else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
if (this._textInputEmpty()) return;
this._send();
}
if (!this.isShown()) return;
if (e.code === "Escape") {
this.hide();
} else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
if (this._textInputEmpty()) return;
this._send();
}
}
_textInputEmpty() {
return this.$text.innerText === "\n";
return !this.$text.innerText || this.$text.innerText === "\n";
}
_onChange(e) {
@@ -1200,7 +1366,7 @@ class ReceiveTextDialog extends Dialog {
}
async _onKeyDown(e) {
if (this.$el.attributes["show"]) {
if (this.isShown()) {
if (e.code === "KeyC" && (e.ctrlKey || e.metaKey)) {
await this._onCopy()
this.hide();
@@ -1214,7 +1380,7 @@ class ReceiveTextDialog extends Dialog {
window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return;
if (this.isShown()) return;
this._dequeueRequests();
}
@@ -1255,7 +1421,8 @@ class ReceiveTextDialog extends Dialog {
}
async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent);
const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' ');
await navigator.clipboard.writeText(sanitizedText);
Events.fire('notify-user', 'Copied to clipboard');
this.hide();
}
@@ -1411,7 +1578,8 @@ class Base64ZipDialog extends Dialog {
}
clearBrowserHistory() {
window.history.replaceState({}, "Rewrite URL", '/');
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
}
hide() {
@@ -1430,7 +1598,7 @@ class Toast extends Dialog {
_onNotify(message) {
if (this.hideTimeout) clearTimeout(this.hideTimeout);
this.$el.textContent = message;
this.$el.innerText = message;
this.show();
this.hideTimeout = setTimeout(_ => this.hide(), 5000);
}
@@ -1627,7 +1795,8 @@ class WebShareTargetUI {
}
}
}
window.history.replaceState({}, "Rewrite URL", '/');
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
}
}
}
@@ -1651,7 +1820,8 @@ class WebFileHandlersUI {
Events.fire('activate-paste-mode', {files: files, text: ""})
launchParams = null;
});
window.history.replaceState({}, "Rewrite URL", '/');
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
}
}
}
@@ -1682,7 +1852,7 @@ class PersistentStorage {
PersistentStorage.logBrowserNotCapable();
return;
}
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 4);
DBOpenRequest.onerror = (e) => {
PersistentStorage.logBrowserNotCapable();
console.log('Error initializing database: ');
@@ -1693,27 +1863,32 @@ class PersistentStorage {
};
DBOpenRequest.onupgradeneeded = (e) => {
const db = e.target.result;
const txn = e.target.transaction;
db.onerror = e => console.log('Error loading database: ' + e);
try {
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
if (e.oldVersion === 0) {
// initiate v1
db.createObjectStore('keyval');
} catch (error) {
console.log("Object store named 'keyval' already exists")
let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });
}
try {
const roomSecretsObjectStore = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore.createIndex('secret', 'secret', { unique: true });
} catch (error) {
console.log("Object store named 'room_secrets' already exists")
if (e.oldVersion <= 1) {
// migrate to v2
db.createObjectStore('share_target_files');
}
try {
if (db.objectStoreNames.contains('share_target_files')) {
db.deleteObjectStore('share_target_files');
}
if (e.oldVersion <= 2) {
// migrate to v3
db.deleteObjectStore('share_target_files');
db.createObjectStore('share_target_files', {autoIncrement: true});
} catch (error) {
console.log("Object store named 'share_target_files' already exists")
}
if (e.oldVersion <= 3) {
// migrate to v4
let roomSecretsObjectStore4 = txn.objectStore('room_secrets');
roomSecretsObjectStore4.createIndex('display_name', 'display_name');
roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');
}
}
}
@@ -1746,7 +1921,7 @@ class PersistentStorage {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('keyval', 'readwrite');
const transaction = db.transaction('keyval', 'readonly');
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.get(key);
objectStoreRequest.onsuccess = _ => {
@@ -1779,16 +1954,21 @@ class PersistentStorage {
})
}
static addRoomSecret(roomSecret) {
static addRoomSecret(roomSecret, displayName, deviceName) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.add({'secret': roomSecret});
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. RoomSecret added: ${roomSecret}`);
const objectStoreRequest = objectStore.add({
'secret': roomSecret,
'display_name': displayName,
'device_name': deviceName,
'auto_accept': false
});
objectStoreRequest.onsuccess = e => {
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
resolve();
}
}
@@ -1798,21 +1978,31 @@ class PersistentStorage {
})
}
static getAllRoomSecrets() {
static async getAllRoomSecrets() {
try {
const roomSecrets = await this.getAllRoomSecretEntries();
let secrets = [];
for (let i = 0; i < roomSecrets.length; i++) {
secrets.push(roomSecrets[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
return(secrets);
} catch (e) {
this.logBrowserNotCapable();
return false;
}
}
static getAllRoomSecretEntries() {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.getAll();
objectStoreRequest.onsuccess = e => {
let secrets = [];
for (let i=0; i<e.target.result.length; i++) {
secrets.push(e.target.result[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
resolve(secrets);
resolve(e.target.result);
}
}
DBOpenRequest.onerror = (e) => {
@@ -1821,24 +2011,59 @@ class PersistentStorage {
});
}
static deleteRoomSecret(room_secret) {
static getRoomSecretEntry(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
const key = e.target.result;
if (!key) {
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestRetrieval = objectStore.get(key);
objectStoreRequestRetrieval.onsuccess = e => {
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
resolve({
"entry": e.target.result,
"key": key
});
}
objectStoreRequestRetrieval.onerror = (e) => {
reject(e);
}
};
}
DBOpenRequest.onerror = (e) => {
reject(e);
}
});
}
static deleteRoomSecret(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(room_secret);
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
if (!e.target.result) {
console.log(`Nothing to delete. room_secret not existing: ${room_secret}`);
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestDeletion = objectStore.delete(e.target.result);
const key = e.target.result;
const objectStoreRequestDeletion = objectStore.delete(key);
objectStoreRequestDeletion.onsuccess = _ => {
console.log(`Request successful. Deleted room_secret: ${room_secret}`);
resolve();
console.log(`Request successful. Deleted room_secret: ${key}`);
resolve(roomSecret);
}
objectStoreRequestDeletion.onerror = (e) => {
reject(e);
@@ -1869,22 +2094,116 @@ class PersistentStorage {
}
})
}
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
}
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
}
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
this.getRoomSecretEntry(roomSecret)
.then(roomSecretEntry => {
if (!roomSecretEntry) {
resolve(false);
return;
}
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
const updatedRoomSecretEntry = {
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
};
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
objectStoreRequestUpdate.onsuccess = e => {
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
resolve({
"entry": updatedRoomSecretEntry,
"key": roomSecretEntry.key
});
}
objectStoreRequestUpdate.onerror = (e) => {
reject(e);
}
})
.catch(e => reject(e));
};
DBOpenRequest.onerror = e => reject(e);
})
}
}
class Broadcast {
class BrowserTabsConnector {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
}
_broadcastMessage(message) {
_broadcastSend(message) {
this.bc.postMessage(message);
}
_onMessage(e) {
console.log('Broadcast message received:', e.data)
Events.fire(e.data.type, e.data.detail);
console.log('Broadcast:', e.data)
switch (e.data.type) {
case 'self-display-name-changed':
Events.fire('self-display-name-changed', e.data.detail);
break;
}
}
static peerIsSameBrowser(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
return peerIdsBrowser
? peerIdsBrowser.indexOf(peerId) !== -1
: false;
}
static async addPeerIdToLocalStorage() {
const peerId = sessionStorage.getItem("peerId");
if (!peerId) return false;
let peerIdsBrowser = [];
let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peerIdsBrowser"));
if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);
peerIdsBrowser.push(peerId);
peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerIdsBrowser;
}
static async removePeerIdFromLocalStorage(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
const index = peerIdsBrowser.indexOf(peerId);
peerIdsBrowser.splice(index, 1);
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerId;
}
static async removeOtherPeerIdsFromLocalStorage() {
const peerId = sessionStorage.getItem("peerId");
if (!peerId) return false;
let peerIdsBrowser = [peerId];
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerIdsBrowser;
}
}
@@ -1899,7 +2218,7 @@ class PairDrop {
const sendTextDialog = new SendTextDialog();
const receiveTextDialog = new ReceiveTextDialog();
const pairDeviceDialog = new PairDeviceDialog();
const clearDevicesDialog = new ClearDevicesDialog();
const clearDevicesDialog = new EditPairedDevicesDialog();
const base64ZipDialog = new Base64ZipDialog();
const toast = new Toast();
const notifications = new Notifications();
@@ -1907,7 +2226,7 @@ class PairDrop {
const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI();
const broadCast = new Broadcast();
const broadCast = new BrowserTabsConnector();
});
}
}
@@ -1936,14 +2255,7 @@ window.addEventListener('beforeinstallprompt', e => {
// Background Circles
Events.on('load', () => {
let c = document.createElement('canvas');
let style = c.style;
style.width = '100%';
style.position = 'absolute';
style.zIndex = -1;
style.top = 0;
style.left = 0;
style.animation = "fade-in 800ms";
let c = $$('canvas');
let cCtx = c.getContext('2d');
let x0, y0, w, h, dw, offset;
@@ -1954,7 +2266,6 @@ Events.on('load', () => {
w = document.documentElement.clientWidth;
h = document.documentElement.clientHeight;
offset = $$('footer').offsetHeight - 33;
if (h > 800) offset += 16;
if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed
@@ -1964,11 +2275,7 @@ Events.on('load', () => {
y0 = h - offset;
dw = Math.round(Math.max(w, h, 1000) / 13);
if (document.body.contains(c)) {
document.body.removeChild(c);
}
drawCircles(cCtx, dw);
document.body.appendChild(c);
}
Events.on('bg-resize', _ => init());
@@ -1984,6 +2291,7 @@ Events.on('load', () => {
}
function drawCircles(ctx, frame) {
ctx.clearRect(0, 0, w, h);
for (let i = 0; i < 13; i++) {
drawCircle(ctx, dw * i + frame + 33);
}

View File

@@ -5,7 +5,7 @@ if (!navigator.clipboard) {
// A <span> contains the text to copy
const span = document.createElement('span');
span.textContent = text;
span.innerText = text;
span.style.whiteSpace = 'pre'; // Preserve consecutive spaces and newlines
// Paint the span outside the viewport
@@ -399,6 +399,14 @@ const cyrb53 = function(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}
function getUrlWithoutArguments() {
return `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
}
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
@@ -410,10 +418,10 @@ function arrayBufferToBase64(buffer) {
}
function base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64);
var binary_string = window.atob(base64);
var len = binary_string.length;
var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.6.3';
const cacheVersion = 'v1.7.6';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',
@@ -72,8 +72,7 @@ self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") {
// Requests related to Web Share Target.
event.respondWith((async () => {
let share_url = await evaluateRequestData(event.request);
share_url = event.request.url + share_url;
const share_url = await evaluateRequestData(event.request);
return Response.redirect(encodeURI(share_url), 302);
})());
} else {
@@ -101,15 +100,16 @@ self.addEventListener('activate', evt =>
)
);
const evaluateRequestData = async function (request) {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const evaluateRequestData = function (request) {
return new Promise(async (resolve) => {
const formData = await request.formData();
const title = formData.get("title");
const text = formData.get("text");
const url = formData.get("url");
const files = formData.getAll("allfiles");
const pairDropUrl = request.url;
if (files && files.length > 0) {
let fileObjects = [];
for (let i=0; i<files.length; i++) {
@@ -128,21 +128,21 @@ const evaluateRequestData = async function (request) {
const objectStoreRequest = objectStore.add(fileObjects[i]);
objectStoreRequest.onsuccess = _ => {
if (i === fileObjects.length - 1) resolve('?share-target=files');
if (i === fileObjects.length - 1) resolve(pairDropUrl + '?share-target=files');
}
}
}
DBOpenRequest.onerror = _ => {
resolve('');
resolve(pairDropUrl);
}
} else {
let share_url = '?share-target=text';
let urlArgument = '?share-target=text';
if (title) share_url += `&title=${title}`;
if (text) share_url += `&text=${text}`;
if (url) share_url += `&url=${url}`;
if (title) urlArgument += `&title=${title}`;
if (text) urlArgument += `&text=${text}`;
if (url) urlArgument += `&url=${url}`;
resolve(share_url);
resolve(pairDropUrl + urlArgument);
}
});
}

View File

@@ -20,6 +20,10 @@ body {
overflow-x: hidden;
overscroll-behavior: none;
overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
body {
@@ -216,6 +220,14 @@ hr {
color: white;
}
input {
cursor: pointer;
}
input[type="checkbox"] {
min-width: 13px;
}
x-noscript {
background: var(--primary-color);
color: white;
@@ -274,7 +286,7 @@ x-noscript {
}
@media screen and (min-width: 402px) and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center {
header:has(#edit-pair-devices:not([hidden]))~#center {
--footer-height: 164px;
}
}
@@ -452,8 +464,6 @@ x-no-peers[drop-bg] * {
/* Peer */
x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
@@ -557,7 +567,6 @@ x-peer.ws-peer .highlight-wrapper {
.status,
.device-name,
.connection-hash {
height: 18px;
opacity: 0.7;
}
@@ -717,6 +726,12 @@ x-dialog x-paper {
height: 625px;
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
}
x-dialog:not([show]) {
pointer-events: none;
}
@@ -738,7 +753,7 @@ x-dialog .font-subheading {
margin-bottom: 5px;
}
/* PairDevicesDialog */
/* Pair Devices Dialog */
#key-input-container {
width: 100%;
@@ -746,7 +761,7 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input {
#key-input-container > input {
width: 45px;
height: 45px;
font-size: 30px;
@@ -762,15 +777,18 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input + * {
#key-input-container > input + * {
margin-left: 6px;
}
#key-input-container>input:nth-of-type(4) {
#key-input-container > input:nth-of-type(4) {
margin-left: 5%;
}
#room-key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block;
@@ -782,14 +800,106 @@ x-dialog .font-subheading {
margin: 16px;
}
#pair-device-dialog hr {
margin: 40px -24px;
x-dialog hr {
margin: 40px -24px 30px -24px;
border: solid 1.25px var(--border-color);
}
#pair-device-dialog x-background {
padding: 16px!important;
}
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: "No paired devices.";
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
.paired-device > .auto-accept {
cursor: pointer;
}
/* Receive Dialog */
x-dialog .row {
@@ -838,7 +948,7 @@ x-paper > div:last-child > .button:not(:last-child) {
}
/* Send Text Dialog */
/* Todo: add pair underline to send / receive dialogs displayName */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
@@ -856,9 +966,9 @@ x-dialog .dialog-subheader {
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
white-space: pre-wrap;
padding: 15px 0;
}
@@ -1040,11 +1150,14 @@ button::-moz-focus-inner {
text-align: center;
}
#about header {
z-index: 1;
}
#about .fade-in {
transition: opacity 300ms;
will-change: opacity;
transition-delay: 300ms;
z-index: 11;
pointer-events: all;
}
@@ -1058,12 +1171,23 @@ button::-moz-focus-inner {
--icon-size: 96px;
}
#about .title-wrapper {
display: flex;
align-items: baseline;
}
#about .title-wrapper > div {
margin-left: 0.5em;
}
#about x-background {
position: absolute;
top: calc(32px - 250px);
right: calc(32px - 250px);
width: 500px;
height: 500px;
--size: max(max(230vw, 230vh), calc(150vh + 150vw));
--size-half: calc(var(--size)/2);
top: calc(28px - var(--size-half));
right: calc(36px - var(--size-half));
width: var(--size);
height: var(--size);
border-radius: 50%;
background: var(--primary-color);
transform: scale(0);
@@ -1077,7 +1201,7 @@ button::-moz-focus-inner {
}
#about:target x-background {
transform: scale(10);
transform: scale(1);
}
#about .row a {
@@ -1092,6 +1216,14 @@ button::-moz-focus-inner {
align-self: end;
}
canvas.circles {
width: 100vw;
position: absolute;
z-index: -10;
top: 0;
left: 0;
}
/* Loading Indicator */
.progress {
@@ -1200,7 +1332,7 @@ x-peers:empty~x-instructions {
@media (hover: none) and (pointer: coarse) {
x-peer {
transform: scale(0.95);
padding: 4px 0;
padding: 4px;
}
}
@@ -1213,7 +1345,7 @@ x-peers:empty~x-instructions {
transition: opacity 300ms;
}
#websocket-fallback>span {
#websocket-fallback > span {
margin: 2px;
}
@@ -1233,7 +1365,7 @@ x-peers:empty~x-instructions {
@media screen and (min-height: 800px) {
#websocket-fallback {
padding-bottom: 15px;
padding-bottom: 16px;
}
}
@@ -1363,3 +1495,9 @@ x-dialog x-paper {
background: #bfbfbf;
border-radius: 4px;
}
::-moz-selection,
::selection {
color: black;
background: var(--primary-color);
}