mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2026-04-06 18:03:48 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27ac7786d0 | ||
|
|
edf2ab5eb3 | ||
|
|
c3863a9dd3 | ||
|
|
5934e94761 | ||
|
|
1bc23dc4b3 | ||
|
|
cc78b34d2e | ||
|
|
f34f5bd4b2 | ||
|
|
b2f6a75c99 | ||
|
|
82138c06f3 | ||
|
|
ee820ed6e0 | ||
|
|
b7e7fd1b68 | ||
|
|
96ed0e53b1 | ||
|
|
77b76a3b8d | ||
|
|
e37f9bd9fb | ||
|
|
451173caac | ||
|
|
460e8ec79c | ||
|
|
002b31a113 | ||
|
|
1eb53498b1 | ||
|
|
d56ee87437 |
10
README.md
10
README.md
@@ -42,9 +42,9 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* Paired devices outside your local network that are behind a NAT are connected automatically via [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/)
|
||||
|
||||
### [Improved UI for sending/receiving files](https://github.com/RobinLinus/snapdrop/issues/560)
|
||||
* Files are transferred only after a request is accepted first. On transfer completion they are downloaded automatically if possible.
|
||||
* Multiple files are downloaded as ZIP file
|
||||
* On iOS and Android the devices share menu is opened instead of downloading the files
|
||||
* Files are transferred only after a request is accepted first. On transfer completion files are downloaded automatically if possible.
|
||||
* Multiple files are downloaded as a ZIP file
|
||||
* On iOS and Android, in addition to downloading, files can be shared or saved to the gallery via the Share menu.
|
||||
* Multiple files are transferred at once with an overall progress indicator
|
||||
|
||||
### Send Files or Text Directly From Share Menu, Context Menu or CLI
|
||||
@@ -54,7 +54,8 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* [Send directly via command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
|
||||
|
||||
### Other changes
|
||||
* [Paste Mode](https://github.com/RobinLinus/snapdrop/pull/534)
|
||||
* Change your display name permanently to easily differentiate your devices
|
||||
* [Paste files/text and choose the recipient afterwords ](https://github.com/RobinLinus/snapdrop/pull/534)
|
||||
* [Prevent devices from sleeping on file transfer](https://github.com/RobinLinus/snapdrop/pull/413)
|
||||
* Warn user before PairDrop is closed on file transfer
|
||||
* Open PairDrop on multiple tabs simultaneously (Thanks [@willstott101](https://github.com/willstott101))
|
||||
@@ -79,6 +80,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop)
|
||||
* [Progressive Web App](https://wikipedia.org/wiki/Progressive_Web_App)
|
||||
* [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)
|
||||
* [zip.js](https://gildas-lormeau.github.io/zip.js/)
|
||||
* [cyrb53](https://github.com/bryc) super fast hash function
|
||||
|
||||
Have any questions? Read our [FAQ](/docs/faq.md).
|
||||
|
||||
|
||||
88
index.js
88
index.js
@@ -3,6 +3,11 @@ const crypto = require('crypto')
|
||||
const {spawn} = require('child_process')
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const parser = require('ua-parser-js');
|
||||
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
||||
const express = require('express');
|
||||
const RateLimit = require('express-rate-limit');
|
||||
const http = require('http');
|
||||
|
||||
// Handle SIGINT
|
||||
process.on('SIGINT', () => {
|
||||
@@ -52,7 +57,7 @@ if (process.argv.includes('--auto-restart')) {
|
||||
}
|
||||
|
||||
const rtcConfig = process.env.RTC_CONFIG
|
||||
? fs.readFileSync(process.env.RTC_CONFIG, 'utf8')
|
||||
? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
|
||||
: {
|
||||
"sdpSemantics": "unified-plan",
|
||||
"iceServers": [
|
||||
@@ -70,10 +75,6 @@ const rtcConfig = process.env.RTC_CONFIG
|
||||
]
|
||||
};
|
||||
|
||||
const express = require('express');
|
||||
const RateLimit = require('express-rate-limit');
|
||||
const http = require('http');
|
||||
|
||||
const app = express();
|
||||
|
||||
if (process.argv.includes('--rate-limit')) {
|
||||
@@ -114,9 +115,6 @@ if (process.argv.includes('--localhost-only')) {
|
||||
server.listen(port);
|
||||
}
|
||||
|
||||
const parser = require('ua-parser-js');
|
||||
const { uniqueNamesGenerator, animals, colors } = require('unique-names-generator');
|
||||
|
||||
class PairDropServer {
|
||||
|
||||
constructor() {
|
||||
@@ -145,7 +143,8 @@ class PairDropServer {
|
||||
message: {
|
||||
displayName: peer.name.displayName,
|
||||
deviceName: peer.name.deviceName,
|
||||
peerId: peer.id
|
||||
peerId: peer.id,
|
||||
peerIdHash: peer.id.hashCode128BitSalted()
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -369,6 +368,10 @@ 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];
|
||||
@@ -550,9 +553,11 @@ class Peer {
|
||||
}
|
||||
|
||||
_setPeerId(request) {
|
||||
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id");
|
||||
if (peer_id && Peer.isValidUuid(peer_id)) {
|
||||
this.id = peer_id;
|
||||
const searchParams = new URL(request.url, "http://server").searchParams;
|
||||
let peerId = searchParams.get("peer_id");
|
||||
let peerIdHash = searchParams.get("peer_id_hash");
|
||||
if (peerId && Peer.isValidUuid(peerId) && this.isPeerIdHashValid(peerId, peerIdHash)) {
|
||||
this.id = peerId;
|
||||
} else {
|
||||
this.id = crypto.randomUUID();
|
||||
}
|
||||
@@ -611,6 +616,10 @@ class Peer {
|
||||
return /^([0-9]|[a-f]){8}-(([0-9]|[a-f]){4}-){3}([0-9]|[a-f]){12}$/.test(uuid);
|
||||
}
|
||||
|
||||
isPeerIdHashValid(peerId, peerIdHash) {
|
||||
return peerIdHash === peerId.hashCode128BitSalted();
|
||||
}
|
||||
|
||||
addRoomSecret(roomSecret) {
|
||||
if (!(roomSecret in this.roomSecrets)) {
|
||||
this.roomSecrets.push(roomSecret);
|
||||
@@ -626,14 +635,55 @@ class Peer {
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode', {
|
||||
value: function() {
|
||||
var hash = 0, i, chr;
|
||||
for (i = 0; i < this.length; i++) {
|
||||
chr = this.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
return cyrb53(this);
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(String.prototype, 'hashCode128BitSalted', {
|
||||
value: function() {
|
||||
return hasher.hashCode128BitSalted(this);
|
||||
}
|
||||
});
|
||||
|
||||
const hasher = (() => {
|
||||
let seeds;
|
||||
return {
|
||||
hashCode128BitSalted(str) {
|
||||
if (!seeds) {
|
||||
// seeds are created on first call to salt hash.
|
||||
seeds = [4];
|
||||
for (let i=0; i<4; i++) {
|
||||
const randomBuffer = new Uint32Array(1);
|
||||
crypto.webcrypto.getRandomValues(randomBuffer);
|
||||
seeds[i] = randomBuffer[0];
|
||||
}
|
||||
}
|
||||
let hashCode = "";
|
||||
for (let i=0; i<4; i++) {
|
||||
hashCode += cyrb53(str, seeds[i]);
|
||||
}
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
/*
|
||||
cyrb53 (c) 2018 bryc (github.com/bryc)
|
||||
A fast and simple hash function with decent collision resistance.
|
||||
Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
|
||||
Public domain. Attribution appreciated.
|
||||
*/
|
||||
const cyrb53 = function(str, seed = 0) {
|
||||
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
|
||||
for (let i = 0, ch; i < str.length; i++) {
|
||||
ch = str.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
|
||||
return 4294967296 * (2097151 & h2) + (h1>>>0);
|
||||
};
|
||||
|
||||
new PairDropServer();
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pairdrop",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pairdrop",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -106,7 +106,11 @@ sendFiles()
|
||||
zip -q -b /tmp/ -r "$zipPath" "$path"
|
||||
zip -q -b /tmp/ "$zipPathTemp" "$zipPath"
|
||||
|
||||
hash=$(base64 -w 0 "$zipPathTemp")
|
||||
if [[ $OS == "Mac" ]];then
|
||||
hash=$(base64 -i "$zipPathTemp")
|
||||
else
|
||||
hash=$(base64 -w 0 "$zipPathTemp")
|
||||
fi
|
||||
|
||||
# remove temporary temp file
|
||||
rm "$zipPathTemp"
|
||||
@@ -116,7 +120,11 @@ sendFiles()
|
||||
# Create zip file temporarily to send file
|
||||
zip -q -b /tmp/ "$zipPath" "$path"
|
||||
|
||||
hash=$(base64 -w 0 "$zipPath")
|
||||
if [[ $OS == "Mac" ]];then
|
||||
hash=$(base64 -i "$zipPath")
|
||||
else
|
||||
hash=$(base64 -w 0 "$zipPath")
|
||||
fi
|
||||
fi
|
||||
|
||||
# remove temporary temp file
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" >
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
@@ -89,7 +89,13 @@
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="display-name" placeholder=" "></div>
|
||||
<div>
|
||||
<span>You are known as:</span>
|
||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg id="edit-pen" class="icon">
|
||||
<use xlink:href="#edit-pen-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
@@ -332,6 +338,10 @@
|
||||
<!--! 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>
|
||||
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/util.js"></script>
|
||||
|
||||
@@ -21,10 +21,10 @@ class ServerConnection {
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
_connect() {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
const ws = new WebSocket(await this._endpoint());
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = _ => this._onOpen();
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
@@ -53,7 +53,7 @@ class ServerConnection {
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
@@ -118,34 +118,24 @@ class ServerConnection {
|
||||
|
||||
_onDisplayName(msg) {
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
if (!peerId) {
|
||||
// save peerId to indexedDB to retrieve after PWA is installed
|
||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
||||
});
|
||||
}
|
||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
async _endpoint() {
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = await this._peerId();
|
||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
}
|
||||
return ws_url.toString();
|
||||
}
|
||||
|
||||
async _peerId() {
|
||||
// make peerId persistent when pwa is installed
|
||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
||||
? await PersistentStorage.get('peerId')
|
||||
: sessionStorage.getItem("peerId");
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
@@ -161,7 +151,7 @@ class ServerConnection {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'Connecting..');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
@@ -358,6 +348,9 @@ class Peer {
|
||||
case 'text':
|
||||
this._onTextReceived(messageJSON);
|
||||
break;
|
||||
case 'display-name-changed':
|
||||
this._onDisplayNameChanged(messageJSON);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +488,11 @@ class Peer {
|
||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||
this.sendJSON({ type: 'message-transfer-complete' });
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(message) {
|
||||
if (!message.displayName) return;
|
||||
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
@@ -568,14 +566,14 @@ class RTCPeer extends Peer {
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
const channel = event.channel || event.target;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = _ => this._onChannelClosed();
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._closeChannel());
|
||||
this._channel = channel;
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._onPageHide());
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
@@ -618,10 +616,16 @@ class RTCPeer extends Peer {
|
||||
}
|
||||
}
|
||||
|
||||
_closeChannel() {
|
||||
if (this._channel) this._channel.onclose = null;
|
||||
if (this._conn) this._conn.close();
|
||||
this._conn = null;
|
||||
_onPageHide() {
|
||||
this._disconnect();
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
if (this._conn && this._channel) {
|
||||
this._channel.onclose = null;
|
||||
this._channel.close();
|
||||
}
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
@@ -635,9 +639,11 @@ class RTCPeer extends Peer {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection disconnected');
|
||||
break;
|
||||
case 'failed':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection failed');
|
||||
break;
|
||||
}
|
||||
@@ -696,8 +702,11 @@ 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-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('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
@@ -721,10 +730,6 @@ class PeersManager {
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||
}
|
||||
@@ -756,6 +761,10 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
delete this.peers[peerId];
|
||||
@@ -773,6 +782,23 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||
for (const peerId in this.peers) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeerDisplayNameChanged(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
if (!peer) return;
|
||||
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
|
||||
}
|
||||
|
||||
_onDisplayName(displayName) {
|
||||
this._originalDisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
|
||||
@@ -9,9 +9,8 @@ window.pasteMode.activated = false;
|
||||
// set display name
|
||||
Events.on('display-name', e => {
|
||||
const me = e.detail.message;
|
||||
const $displayName = $('display-name')
|
||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
||||
$displayName.title = me.deviceName;
|
||||
const $displayName = $('display-name');
|
||||
$displayName.setAttribute('placeholder', me.displayName);
|
||||
});
|
||||
|
||||
class PeersUI {
|
||||
@@ -43,6 +42,80 @@ class PeersUI {
|
||||
|
||||
Events.on('peer-added', _ => this.evaluateOverflowing());
|
||||
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
||||
|
||||
this.$displayName = $('display-name');
|
||||
|
||||
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
|
||||
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
|
||||
|
||||
// Load saved display name on page load
|
||||
this._getSavedDisplayName().then(displayName => {
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
if (displayName) Events.fire('self-display-name-changed', displayName);
|
||||
});
|
||||
}
|
||||
|
||||
_insertDisplayName(displayName) {
|
||||
this.$displayName.textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDownDisplayName(e) {
|
||||
if (e.key === "Enter" || e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUpDisplayName(e) {
|
||||
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
|
||||
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
|
||||
}
|
||||
|
||||
async _saveDisplayName(newDisplayName) {
|
||||
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
|
||||
const savedDisplayName = await this._getSavedDisplayName();
|
||||
if (newDisplayName === savedDisplayName) return;
|
||||
|
||||
if (newDisplayName) {
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
|
||||
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||
}).catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||
}).finally(_ => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
} else {
|
||||
PersistentStorage.delete('editedDisplayName').catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
Events.fire('notify-user', 'Random Display name is used again.');
|
||||
}).finally(_ => {
|
||||
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getSavedDisplayName() {
|
||||
return new Promise((resolve) => {
|
||||
PersistentStorage.get('editedDisplayName')
|
||||
.then(displayName => resolve(displayName ?? ""))
|
||||
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
|
||||
});
|
||||
}
|
||||
|
||||
_changePeerDisplayName(peerId, displayName) {
|
||||
this.peers[peerId].name.displayName = displayName;
|
||||
const peerIdNode = $(peerId);
|
||||
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
@@ -544,6 +617,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
// Todo: change count in document.title and move '- PairDrop' to back
|
||||
if (!this._filesQueue.length) { // nothing to do
|
||||
this._busy = false;
|
||||
return;
|
||||
@@ -766,15 +840,17 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
class PairDeviceDialog extends Dialog {
|
||||
constructor() {
|
||||
super('pair-device-dialog');
|
||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||
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.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||
let createJoinForm = this.$el.querySelector('form');
|
||||
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$createJoinForm = this.$el.querySelector('form');
|
||||
|
||||
this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
|
||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||
@@ -867,6 +943,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
this.$pairDeviceBtn.removeAttribute('hidden');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
this._evaluateNumberRoomSecrets();
|
||||
@@ -1723,6 +1800,23 @@ class PersistentStorage {
|
||||
}
|
||||
}
|
||||
|
||||
class Broadcast {
|
||||
constructor() {
|
||||
this.bc = new BroadcastChannel('pairdrop');
|
||||
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
|
||||
}
|
||||
|
||||
_broadcastMessage(message) {
|
||||
this.bc.postMessage(message);
|
||||
}
|
||||
|
||||
_onMessage(e) {
|
||||
console.log('Broadcast message received:', e.data)
|
||||
Events.fire(e.data.type, e.data.detail);
|
||||
}
|
||||
}
|
||||
|
||||
class PairDrop {
|
||||
constructor() {
|
||||
Events.on('load', _ => {
|
||||
@@ -1742,6 +1836,7 @@ class PairDrop {
|
||||
const webShareTargetUI = new WebShareTargetUI();
|
||||
const webFileHandlersUI = new WebFileHandlersUI();
|
||||
const noSleepUI = new NoSleepUI();
|
||||
const broadCast = new Broadcast();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.3.0';
|
||||
const cacheVersion = 'v1.4.1';
|
||||
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -450,6 +450,7 @@ x-peer[status] x-icon {
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -533,6 +534,7 @@ footer {
|
||||
padding: 0 0 16px 0;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
@@ -557,6 +559,39 @@ footer .font-body2 {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#display-name {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
max-width: 15em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -6px;
|
||||
padding-right: 0.3rem;
|
||||
padding-left: 0.3em;
|
||||
padding-bottom: 0.1rem;
|
||||
border-radius: 1.3rem/30%;
|
||||
border-right: solid 1rem transparent;
|
||||
border-left: solid 1rem transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(var(--text-color), 43%);
|
||||
color: white;
|
||||
transition: background-color 0.5s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#edit-pen {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -2px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
@@ -995,11 +1030,11 @@ button::-moz-focus-inner {
|
||||
x-toast {
|
||||
position: absolute;
|
||||
min-height: 48px;
|
||||
bottom: 24px;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
max-width: 344px;
|
||||
background-color: #323232;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background-color: rgb(var(--text-color));
|
||||
color: rgb(var(--bg-color));
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
@@ -1013,7 +1048,7 @@ x-toast {
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<use xlink:href="#homescreen" />
|
||||
</svg>
|
||||
</a>
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" >
|
||||
<a id="pair-device" class="icon-button" title="Pair Device" hidden>
|
||||
<svg class="icon">
|
||||
<use xlink:href="#pair-device-icon" />
|
||||
</svg>
|
||||
@@ -89,7 +89,13 @@
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
<div id="display-name" placeholder=" "></div>
|
||||
<div>
|
||||
<span>You are known as:</span>
|
||||
<div id="display-name" placeholder="Loading..." title="Edit your device name permanently" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
|
||||
<svg id="edit-pen" class="icon">
|
||||
<use xlink:href="#edit-pen-icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="font-body2">
|
||||
You can be discovered by everyone <span id="on-this-network">on this network</span>
|
||||
<span id="and-by-paired-devices" hidden> and by <span id="paired-devices">paired devices</span></span>
|
||||
@@ -245,14 +251,14 @@
|
||||
</div>
|
||||
<!-- About Page -->
|
||||
<x-about id="about" class="full center column">
|
||||
<header class="row-reverse fade-in">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<section class="center column fade-in">
|
||||
<header class="row-reverse">
|
||||
<a href="#" class="close icon-button">
|
||||
<svg class="icon">
|
||||
<use xlink:href="#close-icon" />
|
||||
</svg>
|
||||
</a>
|
||||
</header>
|
||||
<svg class="icon logo">
|
||||
<use xlink:href="#wifi-tethering" />
|
||||
</svg>
|
||||
@@ -335,6 +341,10 @@
|
||||
<!--! 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>
|
||||
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
|
||||
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
<!-- Scripts -->
|
||||
<script src="scripts/util.js"></script>
|
||||
|
||||
@@ -19,10 +19,10 @@ class ServerConnection {
|
||||
Events.on('online', _ => this._connect());
|
||||
}
|
||||
|
||||
async _connect() {
|
||||
_connect() {
|
||||
clearTimeout(this._reconnectTimer);
|
||||
if (this._isConnected() || this._isConnecting()) return;
|
||||
const ws = new WebSocket(await this._endpoint());
|
||||
const ws = new WebSocket(this._endpoint());
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = _ => this._onOpen();
|
||||
ws.onmessage = e => this._onMessage(e.data);
|
||||
@@ -51,7 +51,7 @@ class ServerConnection {
|
||||
|
||||
_onPairDeviceJoin(roomKey) {
|
||||
if (!this._isConnected()) {
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 5000);
|
||||
setTimeout(_ => this._onPairDeviceJoin(roomKey), 1000);
|
||||
return;
|
||||
}
|
||||
this.send({ type: 'pair-device-join', roomKey: roomKey })
|
||||
@@ -113,6 +113,7 @@ class ServerConnection {
|
||||
case 'file-transfer-complete':
|
||||
case 'message-transfer-complete':
|
||||
case 'text':
|
||||
case 'display-name-changed':
|
||||
case 'ws-chunk':
|
||||
Events.fire('ws-relay', JSON.stringify(msg));
|
||||
break;
|
||||
@@ -128,34 +129,24 @@ class ServerConnection {
|
||||
|
||||
_onDisplayName(msg) {
|
||||
sessionStorage.setItem("peerId", msg.message.peerId);
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
if (!peerId) {
|
||||
// save peerId to indexedDB to retrieve after PWA is installed
|
||||
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
|
||||
console.log(`peerId saved to indexedDB: ${peerId}`);
|
||||
});
|
||||
}
|
||||
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
|
||||
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
|
||||
Events.fire('display-name', msg);
|
||||
}
|
||||
|
||||
async _endpoint() {
|
||||
_endpoint() {
|
||||
// hack to detect if deployment or development environment
|
||||
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
|
||||
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
|
||||
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
|
||||
const peerId = await this._peerId();
|
||||
if (peerId) ws_url.searchParams.append('peer_id', peerId)
|
||||
const peerId = sessionStorage.getItem("peerId");
|
||||
const peerIdHash = sessionStorage.getItem("peerIdHash");
|
||||
if (peerId && peerIdHash) {
|
||||
ws_url.searchParams.append('peer_id', peerId);
|
||||
ws_url.searchParams.append('peer_id_hash', peerIdHash);
|
||||
}
|
||||
return ws_url.toString();
|
||||
}
|
||||
|
||||
async _peerId() {
|
||||
// make peerId persistent when pwa is installed
|
||||
return window.matchMedia('(display-mode: minimal-ui)').matches
|
||||
? await PersistentStorage.get('peerId')
|
||||
: sessionStorage.getItem("peerId");
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
this.send({ type: 'disconnect' });
|
||||
if (this._socket) {
|
||||
@@ -171,7 +162,7 @@ class ServerConnection {
|
||||
console.log('WS: server disconnected');
|
||||
Events.fire('notify-user', 'Connecting..');
|
||||
clearTimeout(this._reconnectTimer);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
|
||||
this._reconnectTimer = setTimeout(_ => this._connect(), 1000);
|
||||
Events.fire('ws-disconnected');
|
||||
this._isReconnect = true;
|
||||
}
|
||||
@@ -368,6 +359,9 @@ class Peer {
|
||||
case 'text':
|
||||
this._onTextReceived(messageJSON);
|
||||
break;
|
||||
case 'display-name-changed':
|
||||
this._onDisplayNameChanged(messageJSON);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,6 +499,11 @@ class Peer {
|
||||
Events.fire('text-received', { text: escaped, peerId: this._peerId });
|
||||
this.sendJSON({ type: 'message-transfer-complete' });
|
||||
}
|
||||
|
||||
_onDisplayNameChanged(message) {
|
||||
if (!message.displayName) return;
|
||||
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
|
||||
}
|
||||
}
|
||||
|
||||
class RTCPeer extends Peer {
|
||||
@@ -578,14 +577,14 @@ class RTCPeer extends Peer {
|
||||
|
||||
_onChannelOpened(event) {
|
||||
console.log('RTC: channel opened with', this._peerId);
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
const channel = event.channel || event.target;
|
||||
channel.binaryType = 'arraybuffer';
|
||||
channel.onmessage = e => this._onMessage(e.data);
|
||||
channel.onclose = _ => this._onChannelClosed();
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._closeChannel());
|
||||
this._channel = channel;
|
||||
Events.on('beforeunload', e => this._onBeforeUnload(e));
|
||||
Events.on('pagehide', _ => this._onPageHide());
|
||||
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
|
||||
}
|
||||
|
||||
_onMessage(message) {
|
||||
@@ -628,10 +627,16 @@ class RTCPeer extends Peer {
|
||||
}
|
||||
}
|
||||
|
||||
_closeChannel() {
|
||||
if (this._channel) this._channel.onclose = null;
|
||||
if (this._conn) this._conn.close();
|
||||
this._conn = null;
|
||||
_onPageHide() {
|
||||
this._disconnect();
|
||||
}
|
||||
|
||||
_disconnect() {
|
||||
if (this._conn && this._channel) {
|
||||
this._channel.onclose = null;
|
||||
this._channel.close();
|
||||
}
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
}
|
||||
|
||||
_onChannelClosed() {
|
||||
@@ -645,9 +650,11 @@ class RTCPeer extends Peer {
|
||||
console.log('RTC: state changed:', this._conn.connectionState);
|
||||
switch (this._conn.connectionState) {
|
||||
case 'disconnected':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection disconnected');
|
||||
break;
|
||||
case 'failed':
|
||||
Events.fire('peer-disconnected', this._peerId);
|
||||
this._onError('rtc connection failed');
|
||||
break;
|
||||
}
|
||||
@@ -701,6 +708,7 @@ class WSPeer extends Peer {
|
||||
super(serverConnection, peerId, roomType, roomSecret);
|
||||
this.rtcSupported = false;
|
||||
if (!peerId) return; // we will listen for a caller
|
||||
this._isCaller = true;
|
||||
this._sendSignal();
|
||||
}
|
||||
|
||||
@@ -712,6 +720,7 @@ class WSPeer extends Peer {
|
||||
}
|
||||
|
||||
sendJSON(message) {
|
||||
console.debug(message)
|
||||
message.to = this._peerId;
|
||||
message.roomType = this._roomType;
|
||||
message.roomSecret = this._roomSecret;
|
||||
@@ -723,9 +732,9 @@ class WSPeer extends Peer {
|
||||
}
|
||||
|
||||
onServerMessage(message) {
|
||||
this._peerId = message.sender.id;
|
||||
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
|
||||
if (message.connected) return;
|
||||
this._peerId = message.sender.id;
|
||||
this._sendSignal(true);
|
||||
}
|
||||
|
||||
@@ -746,8 +755,11 @@ 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-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('display-name', e => this._onDisplayName(e.detail.message.displayName));
|
||||
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
|
||||
Events.on('ws-disconnected', _ => this._onWsDisconnected());
|
||||
Events.on('ws-relay', e => this._onWsRelay(e.detail));
|
||||
}
|
||||
@@ -787,10 +799,6 @@ class PeersManager {
|
||||
})
|
||||
}
|
||||
|
||||
sendTo(peerId, message) {
|
||||
this.peers[peerId].send(message);
|
||||
}
|
||||
|
||||
_onRespondToFileTransferRequest(detail) {
|
||||
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
|
||||
}
|
||||
@@ -825,6 +833,10 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
|
||||
_onWsDisconnected() {
|
||||
for (const peerId in this.peers) {
|
||||
console.debug(this.peers[peerId].rtcSupported);
|
||||
@@ -851,6 +863,23 @@ class PeersManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeersDisplayNameChanged(newDisplayName) {
|
||||
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
|
||||
for (const peerId in this.peers) {
|
||||
this._notifyPeerDisplayNameChanged(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
_notifyPeerDisplayNameChanged(peerId) {
|
||||
const peer = this.peers[peerId];
|
||||
if (!peer) return;
|
||||
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
|
||||
}
|
||||
|
||||
_onDisplayName(displayName) {
|
||||
this._originalDisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
class FileChunker {
|
||||
|
||||
@@ -9,9 +9,8 @@ window.pasteMode.activated = false;
|
||||
// set display name
|
||||
Events.on('display-name', e => {
|
||||
const me = e.detail.message;
|
||||
const $displayName = $('display-name')
|
||||
$displayName.textContent = 'You are known as ' + me.displayName;
|
||||
$displayName.title = me.deviceName;
|
||||
const $displayName = $('display-name');
|
||||
$displayName.setAttribute('placeholder', me.displayName);
|
||||
});
|
||||
|
||||
class PeersUI {
|
||||
@@ -43,6 +42,80 @@ class PeersUI {
|
||||
|
||||
Events.on('peer-added', _ => this.evaluateOverflowing());
|
||||
Events.on('bg-resize', _ => this.evaluateOverflowing());
|
||||
|
||||
this.$displayName = $('display-name');
|
||||
|
||||
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
|
||||
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
|
||||
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
|
||||
|
||||
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
|
||||
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
|
||||
|
||||
// Load saved display name on page load
|
||||
this._getSavedDisplayName().then(displayName => {
|
||||
console.log("Retrieved edited display name:", displayName)
|
||||
if (displayName) Events.fire('self-display-name-changed', displayName);
|
||||
});
|
||||
}
|
||||
|
||||
_insertDisplayName(displayName) {
|
||||
this.$displayName.textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDownDisplayName(e) {
|
||||
if (e.key === "Enter" || e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUpDisplayName(e) {
|
||||
// fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty
|
||||
if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = '';
|
||||
}
|
||||
|
||||
async _saveDisplayName(newDisplayName) {
|
||||
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
|
||||
const savedDisplayName = await this._getSavedDisplayName();
|
||||
if (newDisplayName === savedDisplayName) return;
|
||||
|
||||
if (newDisplayName) {
|
||||
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
|
||||
Events.fire('notify-user', 'Device name is changed permanently.');
|
||||
}).catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.");
|
||||
localStorage.setItem('editedDisplayName', newDisplayName);
|
||||
Events.fire('notify-user', 'Device name is changed only for this session.');
|
||||
}).finally(_ => {
|
||||
Events.fire('self-display-name-changed', newDisplayName);
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
|
||||
});
|
||||
} else {
|
||||
PersistentStorage.delete('editedDisplayName').catch(_ => {
|
||||
console.log("This browser does not support IndexedDB. Use localStorage instead.")
|
||||
localStorage.removeItem('editedDisplayName');
|
||||
Events.fire('notify-user', 'Random Display name is used again.');
|
||||
}).finally(_ => {
|
||||
Events.fire('notify-user', 'Device name is randomly generated again.');
|
||||
Events.fire('self-display-name-changed', '');
|
||||
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getSavedDisplayName() {
|
||||
return new Promise((resolve) => {
|
||||
PersistentStorage.get('editedDisplayName')
|
||||
.then(displayName => resolve(displayName ?? ""))
|
||||
.catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? ""))
|
||||
});
|
||||
}
|
||||
|
||||
_changePeerDisplayName(peerId, displayName) {
|
||||
this.peers[peerId].name.displayName = displayName;
|
||||
const peerIdNode = $(peerId);
|
||||
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
@@ -545,6 +618,7 @@ class ReceiveFileDialog extends ReceiveDialog {
|
||||
}
|
||||
|
||||
_dequeueFile() {
|
||||
// Todo: change count in document.title and move '- PairDrop' to back
|
||||
if (!this._filesQueue.length) { // nothing to do
|
||||
this._busy = false;
|
||||
return;
|
||||
@@ -767,15 +841,17 @@ class ReceiveRequestDialog extends ReceiveDialog {
|
||||
class PairDeviceDialog extends Dialog {
|
||||
constructor() {
|
||||
super('pair-device-dialog');
|
||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#key-input-container>input');
|
||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||
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.$footerInstructionsPairedDevices = $('and-by-paired-devices');
|
||||
let createJoinForm = this.$el.querySelector('form');
|
||||
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$createJoinForm = this.$el.querySelector('form');
|
||||
|
||||
this.$createJoinForm.addEventListener('submit', e => this._onSubmit(e));
|
||||
this.$pairDeviceBtn.addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
|
||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||
@@ -868,6 +944,7 @@ class PairDeviceDialog extends Dialog {
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
this.$pairDeviceBtn.removeAttribute('hidden');
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
this._evaluateNumberRoomSecrets();
|
||||
@@ -1724,6 +1801,23 @@ class PersistentStorage {
|
||||
}
|
||||
}
|
||||
|
||||
class Broadcast {
|
||||
constructor() {
|
||||
this.bc = new BroadcastChannel('pairdrop');
|
||||
this.bc.addEventListener('message', e => this._onMessage(e));
|
||||
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
|
||||
}
|
||||
|
||||
_broadcastMessage(message) {
|
||||
this.bc.postMessage(message);
|
||||
}
|
||||
|
||||
_onMessage(e) {
|
||||
console.log('Broadcast message received:', e.data)
|
||||
Events.fire(e.data.type, e.data.detail);
|
||||
}
|
||||
}
|
||||
|
||||
class PairDrop {
|
||||
constructor() {
|
||||
Events.on('load', _ => {
|
||||
@@ -1743,6 +1837,7 @@ class PairDrop {
|
||||
const webShareTargetUI = new WebShareTargetUI();
|
||||
const webFileHandlersUI = new WebFileHandlersUI();
|
||||
const noSleepUI = new NoSleepUI();
|
||||
const broadCast = new Broadcast();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const cacheVersion = 'v1.3.0';
|
||||
const cacheVersion = 'v1.4.1';
|
||||
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
|
||||
const urlsToCache = [
|
||||
'index.html',
|
||||
|
||||
@@ -477,6 +477,7 @@ x-peer.ws-peer .highlight-wrapper {
|
||||
}
|
||||
|
||||
.device-descriptor {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -559,6 +560,7 @@ footer {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
transition: color 300ms;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
footer .logo {
|
||||
@@ -583,6 +585,39 @@ footer .font-body2 {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
#display-name {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
max-width: 15em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: text;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -6px;
|
||||
padding-right: 0.3rem;
|
||||
padding-left: 0.3em;
|
||||
padding-bottom: 0.1rem;
|
||||
border-radius: 1.3rem/30%;
|
||||
border-right: solid 1rem transparent;
|
||||
border-left: solid 1rem transparent;
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(var(--text-color), 43%);
|
||||
color: white;
|
||||
transition: background-color 0.5s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#edit-pen {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: -2px;
|
||||
position: relative;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
|
||||
x-dialog x-background {
|
||||
@@ -1021,11 +1056,11 @@ button::-moz-focus-inner {
|
||||
x-toast {
|
||||
position: absolute;
|
||||
min-height: 48px;
|
||||
bottom: 24px;
|
||||
top: 50px;
|
||||
width: 100%;
|
||||
max-width: 344px;
|
||||
background-color: #323232;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
background-color: rgb(var(--text-color));
|
||||
color: rgb(var(--bg-color));
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 24px;
|
||||
@@ -1039,7 +1074,7 @@ x-toast {
|
||||
|
||||
x-toast:not([show]):not(:hover) {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user