From 74b88c2e7dd24f468fec94082e08f190d163a8c5 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 24 Feb 2023 16:53:13 +0100 Subject: [PATCH 001/608] fix dialog heights --- public/styles.css | 3 +++ public_included_ws_fallback/styles.css | 3 +++ 2 files changed, 6 insertions(+) diff --git a/public/styles.css b/public/styles.css index 0b089bf..aa08cbc 100644 --- a/public/styles.css +++ b/public/styles.css @@ -423,6 +423,9 @@ x-dialog x-paper { box-sizing: border-box; transition: transform 300ms; will-change: transform; +} + +#pairDeviceDialog x-paper { position: absolute; top: max(50%, 350px); height: 650px; diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index c415cdd..ab61629 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -432,6 +432,9 @@ x-dialog x-paper { box-sizing: border-box; transition: transform 300ms; will-change: transform; +} + +#pairDeviceDialog x-paper { position: absolute; top: max(50%, 350px); height: 650px; From 66359da2cac78de868a17a90942f117dd8cdaac1 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 24 Feb 2023 18:08:48 +0100 Subject: [PATCH 002/608] get rtcConfig dynamically from the server --- docs/host-your-own.md | 86 ++++++++++++++++--- index.js | 25 ++++++ public/scripts/network.js | 26 ++---- .../scripts/network.js | 26 ++---- rtc_config_example.json | 16 ++++ 5 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 rtc_config_example.json diff --git a/docs/host-your-own.md b/docs/host-your-own.md index e2cf6fe..2448a5a 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -14,7 +14,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ls Set options by using the following flags in the `docker run` command: #### Port -``` +```bash -p 127.0.0.1:8080:3000 ``` > Specify the port used by the docker image @@ -27,7 +27,7 @@ Set options by using the following flags in the `docker run` command: > Limits clients to 100 requests per 5 min #### Websocket Fallback (for VPN) -``` +```bash -e WS_FALLBACK=true ``` > Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. @@ -39,6 +39,34 @@ Set options by using the following flags in the `docker run` command: > Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. > Additionally, beware that all traffic using this fallback debits the servers data plan. +#### Specify STUN/TURN Servers +```bash +-e RTC_CONFIG="rtc_config.json" +``` + +> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration. +> You can use `pairdrop/rtc_config_example.json` as a starting point. +> +> Default configuration: +> ```json +> { +> "sdpSemantics": "unified-plan", +> "iceServers": [ +> { +> "urls": "stun:stun.l.google.com:19302" +> }, +> { +> "urls": "stun:openrelay.metered.ca:80" +> }, +> { +> "urls": "turn:openrelay.metered.ca:443", +> "username": "openrelayproject", +> "credential": "openrelayproject" +> } +> ] +> } +> ``` +
## Deployment with Docker with self-built image @@ -100,6 +128,38 @@ $env:PORT=3010; npm start ``` > Specify the port PairDrop is running on. (Default: 3000) +#### Specify STUN/TURN Server +On Unix based systems +```bash +RTC_CONFIG="rtc_config.json" npm start +``` +On Windows +```bash +$env:RTC_CONFIG="rtc_config.json"; npm start +``` +> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration. +> You can use `pairdrop/rtc_config_example.json` as a starting point. +> +> Default configuration: +> ```json +> { +> "sdpSemantics": "unified-plan", +> "iceServers": [ +> { +> "urls": "stun:stun.l.google.com:19302" +> }, +> { +> "urls": "stun:openrelay.metered.ca:80" +> }, +> { +> "urls": "turn:openrelay.metered.ca:443", +> "username": "openrelayproject", +> "credential": "openrelayproject" +> } +> ] +> } +> ``` + ### Options / Flags #### Local Run ```bash @@ -158,7 +218,7 @@ When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Ot ### Using nginx #### Allow http and https requests -``` +```nginx configuration server { listen 80; @@ -191,7 +251,7 @@ server { ``` #### Automatic http to https redirect: -``` +```nginx configuration server { listen 80; @@ -221,13 +281,13 @@ server { ### Using Apache install modules `proxy`, `proxy_http`, `mod_proxy_wstunnel` -```shell +```bash a2enmod proxy ``` -```shell +```bash a2enmod proxy_http ``` -```shell +```bash a2enmod proxy_wstunnel ``` @@ -237,7 +297,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian) **pairdrop.conf** #### Allow http and https requests -``` +```apacheconf ProxyPass / http://127.0.0.1:3000/ RewriteEngine on @@ -254,7 +314,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian) ``` #### Automatic http to https redirect: -``` +```apacheconf Redirect permanent / https://127.0.0.1:3000/ @@ -267,10 +327,10 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian) ``` Activate the new virtual host and reload apache: -```shell +```bash a2ensite pairdrop ``` -```shell +```bash service apache2 reload ``` @@ -281,7 +341,7 @@ All files needed for developing are available on the branch `dev`. First, [Install docker with docker-compose.](https://docs.docker.com/compose/install/) Then, clone the repository and run docker-compose: -```shell +```bash git clone https://github.com/schlagmichdoch/PairDrop.git cd PairDrop @@ -306,7 +366,7 @@ The nginx container creates a CA certificate and a website certificate for you. If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://:8080/ca.crt`. Install that certificate to the trust store of your operating system. - On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. -- On MacOS, double click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL. +- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL. - Firefox uses its own trust store. To install the CA, point Firefox at `http://:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK. - When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data). diff --git a/index.js b/index.js index 31fbca9..eb380ea 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const process = require('process') const crypto = require('crypto') const {spawn} = require('child_process') +const fs = require('fs'); // Handle SIGINT process.on('SIGINT', () => { @@ -49,6 +50,25 @@ if (process.argv.includes('--auto-restart')) { ); } +const rtcConfig = process.env.RTC_CONFIG + ? fs.readFileSync(process.env.RTC_CONFIG, 'utf8') + : { + "sdpSemantics": "unified-plan", + "iceServers": [ + { + "urls": "stun:stun.l.google.com:19302" + }, + { + "urls": "stun:openrelay.metered.ca:80" + }, + { + "urls": "turn:openrelay.metered.ca:443", + "username": "openrelayproject", + "credential": "openrelayproject" + } + ] + }; + const express = require('express'); const RateLimit = require('express-rate-limit'); const http = require('http'); @@ -110,6 +130,11 @@ class PairDropServer { } _onConnection(peer) { + this._send(peer, { + type: 'rtc-config', + config: rtcConfig + }); + this._joinRoom(peer); peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.onerror = e => console.error(e); diff --git a/public/scripts/network.js b/public/scripts/network.js index be1389f..a65e95c 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -58,10 +58,17 @@ class ServerConnection { this.send({ type: 'pair-device-join', roomKey: roomKey }) } + _setRtcConfig(config) { + window.rtcConfig = config; + } + _onMessage(msg) { msg = JSON.parse(msg); if (msg.type !== 'ping') console.log('WS:', msg); switch (msg.type) { + case 'rtc-config': + this._setRtcConfig(msg.config); + break; case 'peers': Events.fire('peers', msg); break; @@ -509,7 +516,7 @@ class RTCPeer extends Peer { _openConnection(peerId, isCaller) { this._isCaller = isCaller; this._peerId = peerId; - this._conn = new RTCPeerConnection(RTCPeer.config); + this._conn = new RTCPeerConnection(window.rtcConfig); this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); @@ -852,20 +859,3 @@ class Events { return window.removeEventListener(type, callback, false); } } - -RTCPeer.config = { - 'sdpSemantics': 'unified-plan', - 'iceServers': [ - { - urls: 'stun:stun.l.google.com:19302' - }, - { - urls: 'stun:openrelay.metered.ca:80' - }, - { - urls: 'turn:openrelay.metered.ca:443', - username: 'openrelayproject', - credential: 'openrelayproject', - }, - ] -} diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index f739465..8c39017 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -56,10 +56,17 @@ class ServerConnection { this.send({ type: 'pair-device-join', roomKey: roomKey }) } + _setRtcConfig(config) { + window.rtcConfig = config; + } + _onMessage(msg) { msg = JSON.parse(msg); if (msg.type !== 'ping') console.log('WS:', msg); switch (msg.type) { + case 'rtc-config': + this._setRtcConfig(msg.config); + break; case 'peers': Events.fire('peers', msg); break; @@ -519,7 +526,7 @@ class RTCPeer extends Peer { _openConnection(peerId, isCaller) { this._isCaller = isCaller; this._peerId = peerId; - this._conn = new RTCPeerConnection(RTCPeer.config); + this._conn = new RTCPeerConnection(window.rtcConfig); this._conn.onicecandidate = e => this._onIceCandidate(e); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); @@ -919,20 +926,3 @@ class Events { return window.removeEventListener(type, callback, false); } } - -RTCPeer.config = { - 'sdpSemantics': 'unified-plan', - 'iceServers': [ - { - urls: 'stun:stun.l.google.com:19302' - }, - { - urls: 'stun:openrelay.metered.ca:80' - }, - { - urls: 'turn:openrelay.metered.ca:443', - username: 'openrelayproject', - credential: 'openrelayproject', - }, - ] -} diff --git a/rtc_config_example.json b/rtc_config_example.json new file mode 100644 index 0000000..f78905d --- /dev/null +++ b/rtc_config_example.json @@ -0,0 +1,16 @@ +{ + "sdpSemantics": "unified-plan", + "iceServers": [ + { + "urls": "stun:stun.l.google.com:19302" + }, + { + "urls": "stun:openrelay.metered.ca:80" + }, + { + "urls": "turn:openrelay.metered.ca:443", + "username": "openrelayproject", + "credential": "openrelayproject" + } + ] +} From d56ee874376e611d57fd560e6b33525ff85a11c3 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 1 Mar 2023 21:35:00 +0100 Subject: [PATCH 003/608] - Enable renaming of own display name permanently via UI - Make peerId completely ephemeral - Stabilize RTCConnection by closing connections cleanly --- index.js | 11 +- public/index.html | 18 ++- public/scripts/network.js | 103 ++++++++++------ public/scripts/ui.js | 84 ++++++++++++- public/styles.css | 31 ++++- public_included_ws_fallback/index.html | 18 ++- .../scripts/network.js | 112 +++++++++++------- public_included_ws_fallback/scripts/ui.js | 84 ++++++++++++- public_included_ws_fallback/styles.css | 31 ++++- 9 files changed, 377 insertions(+), 115 deletions(-) diff --git a/index.js b/index.js index 31fbca9..766d9bd 100644 --- a/index.js +++ b/index.js @@ -453,7 +453,7 @@ class Peer { this._setIP(request); // set peer id - this._setPeerId(request) + this.id = crypto.randomUUID(); // is WebRTC supported ? this.rtcSupported = request.url.indexOf('webrtc') > -1; @@ -525,15 +525,6 @@ class Peer { return false; } - _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; - } else { - this.id = crypto.randomUUID(); - } - } - toString() { return `` } diff --git a/public/index.html b/public/index.html index 59a257a..6c54479 100644 --- a/public/index.html +++ b/public/index.html @@ -89,7 +89,11 @@ -
+
+ You are known as: +
+ +
You can be discovered by everyone on this network @@ -145,7 +149,7 @@

PairDrop

- + would like to share
@@ -190,7 +194,7 @@

PairDrop

Send a Message to - +
@@ -208,8 +212,8 @@

PairDrop - Message Received

- - sent the following message: + + sent a message:
@@ -326,6 +330,10 @@ + + + + diff --git a/public/scripts/network.js b/public/scripts/network.js index be1389f..76d9e0b 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -8,6 +8,7 @@ class ServerConnection { constructor() { this._connect(); Events.on('pagehide', _ => this._disconnect()); + Events.on('beforeunload', _ => this._onBeforeUnload()); document.addEventListener('visibilitychange', _ => this._onVisibilityChange()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); @@ -21,10 +22,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); @@ -109,45 +110,29 @@ 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()) 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) 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' }); + _onBeforeUnload() { if (this._socket) { this._socket.onclose = null; this._socket.close(); this._socket = null; - Events.fire('ws-disconnected'); } } + _disconnect() { + this.send({ type: 'disconnect' }); + } + _onDisconnect() { console.log('WS: server disconnected'); Events.fire('notify-user', 'No server connection. Retry in 5s...'); @@ -320,7 +305,6 @@ class Peer { return; } message = JSON.parse(message); - console.log('RTC:', message); switch (message.type) { case 'request': this._onFilesTransferRequest(message); @@ -349,6 +333,9 @@ class Peer { case 'text': this._onTextReceived(message); break; + case 'display-name-changed': + this._onDisplayNameChanged(message); + break; } } @@ -486,6 +473,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 { @@ -496,6 +488,13 @@ class RTCPeer extends Peer { this._connect(peerId, true); } + _onMessage(message) { + if (typeof message !== 'string') { + console.log('RTC:', JSON.parse(message)); + } + super._onMessage(message); + } + _connect(peerId, isCaller) { if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller); @@ -558,14 +557,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()); + channel.onclose = e => this._onChannelClosed(e); 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()}); } getConnectionHash() { @@ -598,13 +597,21 @@ class RTCPeer extends Peer { if (this._busy) { e.preventDefault(); return "There are unfinished transfers. Are you sure you want to close?"; + } else { + this._disconnect(); } } - _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() { @@ -618,9 +625,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; } @@ -679,8 +688,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) { @@ -704,10 +716,6 @@ class PeersManager { }) } - sendTo(peerId, message) { - this.peers[peerId].send(message); - } - _onRespondToFileTransferRequest(detail) { this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); } @@ -739,6 +747,10 @@ class PeersManager { } } + _onPeerConnected(peerId) { + this._notifyPeerDisplayNameChanged(peerId); + } + _onPeerDisconnected(peerId) { const peer = this.peers[peerId]; delete this.peers[peerId]; @@ -756,6 +768,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 || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return; + this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName}); + } + + _onDisplayName(displayName) { + this._originalDisplayName = displayName; + } } class FileChunker { diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 25733b8..eca37af 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -10,8 +10,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; + const $displayName = $('display-name'); + $displayName.setAttribute('placeholder', me.displayName); $displayName.title = me.deviceName; }); @@ -44,6 +44,61 @@ 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 + PersistentStorage.get('editedDisplayName').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) { + if (/(\n|\r|\r\n)/.test(e.target.innerText)) e.target.innerText = e.target.innerText.replace(/(\n|\r|\r\n)/, ''); + } + + async _saveDisplayName(newDisplayName) { + const savedDisplayName = await PersistentStorage.get('editedDisplayName') ?? ""; + if (newDisplayName === savedDisplayName) return; + + if (newDisplayName) { + PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => { + Events.fire('notify-user', `Display name is set permanently.`); + Events.fire('self-display-name-changed', newDisplayName); + Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName}); + }); + } else { + PersistentStorage.delete('editedDisplayName').then(_ => { + Events.fire('notify-user', 'Display name is randomly generated again.'); + Events.fire('self-display-name-changed', ''); + Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); + }); + } + } + + _changePeerDisplayName(peerId, displayName) { + this.peers[peerId].name.displayName = displayName; + const peerIdNode = $(peerId); + if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName; } _onKeyDown(e) { @@ -520,6 +575,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; @@ -661,7 +717,7 @@ class ReceiveRequestDialog extends ReceiveDialog { constructor() { super('receive-request-dialog'); - this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name'); + this.$requestingPeerDisplayNameNode = this.$el.querySelector('#receive-request-dialog .display-name'); this.$fileStemNode = this.$el.querySelector('#file-stem'); this.$fileExtensionNode = this.$el.querySelector('#file-extension'); this.$fileOtherNode = this.$el.querySelector('#file-other'); @@ -991,7 +1047,7 @@ class SendTextDialog extends Dialog { super('send-text-dialog'); Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); this.$text = this.$el.querySelector('#text-input'); - this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name'); + this.$peerDisplayName = this.$el.querySelector('#send-text-dialog .display-name'); this.$form = this.$el.querySelector('form'); this.$submit = this.$el.querySelector('button[type="submit"]'); this.$form.addEventListener('submit', _ => this._send()); @@ -1059,7 +1115,7 @@ class ReceiveTextDialog extends Dialog { Events.on("keydown", e => this._onKeyDown(e)); - this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name'); + this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-dialog .display-name'); this._receiveTextQueue = []; } @@ -1683,6 +1739,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', _ => { @@ -1702,6 +1775,7 @@ class PairDrop { const webShareTargetUI = new WebShareTargetUI(); const webFileHandlersUI = new WebFileHandlersUI(); const noSleepUI = new NoSleepUI(); + const broadCast = new Broadcast(); }); } } diff --git a/public/styles.css b/public/styles.css index d3c05ac..2303116 100644 --- a/public/styles.css +++ b/public/styles.css @@ -450,6 +450,7 @@ x-peer[status] x-icon { } .device-descriptor { + width: 100%; text-align: center; } @@ -557,6 +558,28 @@ footer .font-body2 { padding-bottom: 1px; } +#display-name { + display: inline-block; + text-align: left; + padding-right: 1rem; + border: none; + outline: none; + max-width: 18em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: -5px; +} + +#edit-pen { + width: 1rem; + height: 1rem; + margin-left: -1rem; + margin-bottom: -2px; + position: relative; + z-index: -1; +} + /* Dialog */ x-dialog x-background { @@ -1012,11 +1035,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; @@ -1030,7 +1053,7 @@ x-toast { x-toast:not([show]):not(:hover) { opacity: 0; - transform: translateY(100px); + transform: translateY(-100px); } diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 8227434..3610ca9 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -89,7 +89,11 @@ -
+
+ You are known as: +
+ +
You can be discovered by everyone on this network @@ -148,7 +152,7 @@

PairDrop

- + would like to share
@@ -193,7 +197,7 @@

PairDrop

Send a Message to - +
@@ -211,8 +215,8 @@

PairDrop - Message Received

- - sent the following message: + + sent a message:
@@ -329,6 +333,10 @@ + + + + diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index f739465..bf277d3 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -6,6 +6,7 @@ class ServerConnection { constructor() { this._connect(); Events.on('pagehide', _ => this._disconnect()); + Events.on('beforeunload', _ => this._onBeforeUnload()); document.addEventListener('visibilitychange', _ => this._onVisibilityChange()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); @@ -19,10 +20,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); @@ -105,6 +106,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; @@ -119,45 +121,29 @@ 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()) 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) 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' }); + _onBeforeUnload() { if (this._socket) { this._socket.onclose = null; this._socket.close(); this._socket = null; - Events.fire('ws-disconnected'); } } + _disconnect() { + this.send({ type: 'disconnect' }); + } + _onDisconnect() { console.log('WS: server disconnected'); Events.fire('notify-user', 'No server connection. Retry in 5s...'); @@ -324,13 +310,12 @@ class Peer { this.sendJSON({ type: 'progress', progress: progress }); } - _onMessage(message, logMessage = true) { + _onMessage(message) { if (typeof message !== 'string') { this._onChunkReceived(message); return; } message = JSON.parse(message); - if (logMessage) console.log('RTC:', message); switch (message.type) { case 'request': this._onFilesTransferRequest(message); @@ -359,6 +344,9 @@ class Peer { case 'text': this._onTextReceived(message); break; + case 'display-name-changed': + this._onDisplayNameChanged(message); + break; } } @@ -496,6 +484,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 { @@ -506,6 +499,13 @@ class RTCPeer extends Peer { this._connect(peerId, true); } + _onMessage(message) { + if (typeof message !== 'string') { + console.log('RTC:', JSON.parse(message)); + } + super._onMessage(message); + } + _connect(peerId, isCaller) { if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller); @@ -568,14 +568,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()); + channel.onclose = e => this._onChannelClosed(e); 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()}); } getConnectionHash() { @@ -608,13 +608,21 @@ class RTCPeer extends Peer { if (this._busy) { e.preventDefault(); return "There are unfinished transfers. Are you sure you want to close?"; + } else { + this._disconnect(); } } - _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() { @@ -628,9 +636,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; } @@ -683,6 +693,7 @@ class WSPeer extends Peer { constructor(serverConnection, peerId, roomType, roomSecret) { super(serverConnection, peerId, roomType, roomSecret); if (!peerId) return; // we will listen for a caller + this._isCaller = true; this._sendSignal(); } @@ -694,6 +705,7 @@ class WSPeer extends Peer { } sendJSON(message) { + console.debug(message) message.to = this._peerId; message.roomType = this._roomType; message.roomSecret = this._roomSecret; @@ -705,9 +717,9 @@ class WSPeer extends Peer { } onServerMessage(message) { - Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()}) - if (this._peerId) return; this._peerId = message.sender.id; + Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()}) + if (this._isCaller) return; this._sendSignal(); } @@ -728,8 +740,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-relay', e => this._onWsRelay(e.detail)); } @@ -768,10 +783,6 @@ class PeersManager { }) } - sendTo(peerId, message) { - this.peers[peerId].send(message); - } - _onRespondToFileTransferRequest(detail) { this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); } @@ -806,6 +817,10 @@ class PeersManager { } } + _onPeerConnected(peerId) { + this._notifyPeerDisplayNameChanged(peerId); + } + _onPeerDisconnected(peerId) { const peer = this.peers[peerId]; delete this.peers[peerId]; @@ -823,6 +838,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 || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return; + this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName}); + } + + _onDisplayName(displayName) { + this._originalDisplayName = displayName; + } } class FileChunker { diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index dadfb02..6eb3a05 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -10,8 +10,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; + const $displayName = $('display-name'); + $displayName.setAttribute('placeholder', me.displayName); $displayName.title = me.deviceName; }); @@ -44,6 +44,61 @@ 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 + PersistentStorage.get('editedDisplayName').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) { + if (/(\n|\r|\r\n)/.test(e.target.innerText)) e.target.innerText = e.target.innerText.replace(/(\n|\r|\r\n)/, ''); + } + + async _saveDisplayName(newDisplayName) { + const savedDisplayName = await PersistentStorage.get('editedDisplayName') ?? ""; + if (newDisplayName === savedDisplayName) return; + + if (newDisplayName) { + PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => { + Events.fire('notify-user', `Display name is set permanently.`); + Events.fire('self-display-name-changed', newDisplayName); + Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName}); + }); + } else { + PersistentStorage.delete('editedDisplayName').then(_ => { + Events.fire('notify-user', 'Display name is randomly generated again.'); + Events.fire('self-display-name-changed', ''); + Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); + }); + } + } + + _changePeerDisplayName(peerId, displayName) { + this.peers[peerId].name.displayName = displayName; + const peerIdNode = $(peerId); + if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName; } _onKeyDown(e) { @@ -521,6 +576,7 @@ class ReceiveFileDialog extends ReceiveDialog { } _dequeueFile() { + // Todo: change change count in document.title and move '- PairDrop' to back if (!this._filesQueue.length) { // nothing to do this._busy = false; return; @@ -662,7 +718,7 @@ class ReceiveRequestDialog extends ReceiveDialog { constructor() { super('receive-request-dialog'); - this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name'); + this.$requestingPeerDisplayNameNode = this.$el.querySelector('#receive-request-dialog .display-name'); this.$fileStemNode = this.$el.querySelector('#file-stem'); this.$fileExtensionNode = this.$el.querySelector('#file-extension'); this.$fileOtherNode = this.$el.querySelector('#file-other'); @@ -992,7 +1048,7 @@ class SendTextDialog extends Dialog { super('send-text-dialog'); Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); this.$text = this.$el.querySelector('#text-input'); - this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name'); + this.$peerDisplayName = this.$el.querySelector('#send-text-dialog .display-name'); this.$form = this.$el.querySelector('form'); this.$submit = this.$el.querySelector('button[type="submit"]'); this.$form.addEventListener('submit', _ => this._send()); @@ -1060,7 +1116,7 @@ class ReceiveTextDialog extends Dialog { Events.on("keydown", e => this._onKeyDown(e)); - this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name'); + this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-dialog .display-name'); this._receiveTextQueue = []; } @@ -1684,6 +1740,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', _ => { @@ -1703,6 +1776,7 @@ class PairDrop { const webShareTargetUI = new WebShareTargetUI(); const webFileHandlersUI = new WebFileHandlersUI(); const noSleepUI = new NoSleepUI(); + const broadCast = new Broadcast(); }); } } diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index f153398..de2d6f9 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -477,6 +477,7 @@ x-peer.ws-peer .highlight-wrapper { } .device-descriptor { + width: 100%; text-align: center; } @@ -583,6 +584,28 @@ footer .font-body2 { padding-bottom: 1px; } +#display-name { + display: inline-block; + text-align: left; + padding-right: 1rem; + border: none; + outline: none; + max-width: 18em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: -5px; +} + +#edit-pen { + width: 1rem; + height: 1rem; + margin-left: -1rem; + margin-bottom: -2px; + position: relative; + z-index: -1; +} + /* Dialog */ x-dialog x-background { @@ -1038,11 +1061,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; @@ -1056,7 +1079,7 @@ x-toast { x-toast:not([show]):not(:hover) { opacity: 0; - transform: translateY(100px); + transform: translateY(-100px); } From 1eb53498b1c99fc95a9128c99ee7ef4cffb85c16 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 2 Mar 2023 15:06:22 +0100 Subject: [PATCH 004/608] add localStorage fallback to fix renaming on private tabs and fix Firefox inserting linebreaks into edited divs --- public/scripts/ui.js | 32 +++++++++++++++++---- public_included_ws_fallback/scripts/ui.js | 34 ++++++++++++++++++----- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index eca37af..a45ace6 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -54,8 +54,8 @@ class PeersUI { 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 - PersistentStorage.get('editedDisplayName').then(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); }); @@ -73,21 +73,33 @@ class PeersUI { } _onKeyUpDisplayName(e) { - if (/(\n|\r|\r\n)/.test(e.target.innerText)) e.target.innerText = e.target.innerText.replace(/(\n|\r|\r\n)/, ''); + // 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) { - const savedDisplayName = await PersistentStorage.get('editedDisplayName') ?? ""; + newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') + console.debug(newDisplayName) + const savedDisplayName = await this._getSavedDisplayName(); if (newDisplayName === savedDisplayName) return; if (newDisplayName) { PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => { - Events.fire('notify-user', `Display name is set permanently.`); + Events.fire('notify-user', 'Display name is changed permanently.'); + }).catch(_ => { + console.log("This browser does not support IndexedDB. Use localStorage instead."); + localStorage.setItem('editedDisplayName', newDisplayName); + Events.fire('notify-user', 'Display 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').then(_ => { + 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', 'Display name is randomly generated again.'); Events.fire('self-display-name-changed', ''); Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); @@ -95,6 +107,14 @@ class PeersUI { } } + _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); diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 6eb3a05..f8040bf 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -54,8 +54,8 @@ class PeersUI { 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 - PersistentStorage.get('editedDisplayName').then(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); }); @@ -73,21 +73,33 @@ class PeersUI { } _onKeyUpDisplayName(e) { - if (/(\n|\r|\r\n)/.test(e.target.innerText)) e.target.innerText = e.target.innerText.replace(/(\n|\r|\r\n)/, ''); + // 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) { - const savedDisplayName = await PersistentStorage.get('editedDisplayName') ?? ""; + newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') + console.debug(newDisplayName) + const savedDisplayName = await this._getSavedDisplayName(); if (newDisplayName === savedDisplayName) return; if (newDisplayName) { PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => { - Events.fire('notify-user', `Display name is set permanently.`); + Events.fire('notify-user', 'Display name is changed permanently.'); + }).catch(_ => { + console.log("This browser does not support IndexedDB. Use localStorage instead."); + localStorage.setItem('editedDisplayName', newDisplayName); + Events.fire('notify-user', 'Display 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').then(_ => { + 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', 'Display name is randomly generated again.'); Events.fire('self-display-name-changed', ''); Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); @@ -95,6 +107,14 @@ class PeersUI { } } + _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); @@ -576,7 +596,7 @@ class ReceiveFileDialog extends ReceiveDialog { } _dequeueFile() { - // Todo: change change count in document.title and move '- PairDrop' to back + // Todo: change count in document.title and move '- PairDrop' to back if (!this._filesQueue.length) { // nothing to do this._busy = false; return; From 460e8ec79c571da89fc0c3f733160d3562f4f07d Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 3 Mar 2023 17:43:03 +0100 Subject: [PATCH 005/608] change cursor to clarify that the display name is editable --- public/styles.css | 2 ++ public_included_ws_fallback/styles.css | 2 ++ 2 files changed, 4 insertions(+) diff --git a/public/styles.css b/public/styles.css index bc022e6..a3a4a0d 100644 --- a/public/styles.css +++ b/public/styles.css @@ -534,6 +534,7 @@ footer { padding: 0 0 16px 0; text-align: center; transition: color 300ms; + cursor: default; } footer .logo { @@ -569,6 +570,7 @@ footer .font-body2 { text-overflow: ellipsis; white-space: nowrap; margin-bottom: -5px; + cursor: text; } #edit-pen { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 4b3cc83..92e8841 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -560,6 +560,7 @@ footer { align-items: center; text-align: center; transition: color 300ms; + cursor: default; } footer .logo { @@ -595,6 +596,7 @@ footer .font-body2 { text-overflow: ellipsis; white-space: nowrap; margin-bottom: -5px; + cursor: text; } #edit-pen { From 8bcaa3f60f0a3a3791bf89f219129f0b572d3c83 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 3 Mar 2023 18:28:49 +0100 Subject: [PATCH 006/608] Fix header hierarchy for dynamic stun/turn in docs --- docs/host-your-own.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 31d3c5b..6209895 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -42,7 +42,7 @@ Set options by using the following flags in the `docker run` command: > Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. > Additionally, beware that all traffic using this fallback debits the servers data plan. -#### Specify STUN/TURN Servers +##### Specify STUN/TURN Servers ```bash -e RTC_CONFIG="rtc_config.json" ``` From 451173caac7f8e469de1d964b580b861caba0de9 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 3 Mar 2023 19:10:24 +0100 Subject: [PATCH 007/608] Add possibility to change the display name to the README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9d2930f..69f875e 100644 --- a/README.md +++ b/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)) From e7ab5e26cc3a1b5b5166243f9e53c4c4fa7c2f0d Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 3 Mar 2023 19:01:32 +0100 Subject: [PATCH 008/608] Add dynamic stun/turn config as new feature to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9d2930f..111e4f0 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop) * Automatic restart on error (Thanks [@KaKi87](https://github.com/KaKi87)) * Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101)) * To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558) +* When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers) ## Screenshots
From 8b2eb67266dddb99e1a2673915de230f2c755138 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 3 Mar 2023 19:37:07 +0100 Subject: [PATCH 009/608] fix position of close btn on about page --- public/index.html | 14 +++++++------- public/styles.css | 7 +++++++ public_included_ws_fallback/styles.css | 7 +++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/public/index.html b/public/index.html index 4171562..3a4dff2 100644 --- a/public/index.html +++ b/public/index.html @@ -242,14 +242,14 @@
+
+ + + + + +
-
- - - - - -
diff --git a/public/styles.css b/public/styles.css index c88d9d5..f998e83 100644 --- a/public/styles.css +++ b/public/styles.css @@ -939,6 +939,13 @@ button::-moz-focus-inner { margin: 8px 8px -16px; } +#about section { + flex-grow: 1; +} + +#about header { + align-self: end; +} /* Loading Indicator */ diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 0026356..e9eda38 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -965,6 +965,13 @@ button::-moz-focus-inner { margin: 8px 8px -16px; } +#about section { + flex-grow: 1; +} + +#about header { + align-self: end; +} /* Loading Indicator */ From 67a1b04da2b7c677b8e193ccb37829a39d1536d5 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 3 Mar 2023 19:45:04 +0100 Subject: [PATCH 010/608] increase version to v1.3.0 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c5060cc..ff74f69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.2.2", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.2.2", + "version": "1.3.0", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index d2baecf..32b1307 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.2.2", + "version": "1.3.0", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index b76115f..dd35230 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.2.2'; +const cacheVersion = 'v1.3.0'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index c384efd..f45869c 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.2.2'; +const cacheVersion = 'v1.3.0'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From e37f9bd9fb8d6bcce8e1d4c979d767e1f9f05016 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 4 Mar 2023 15:44:42 +0100 Subject: [PATCH 011/608] fix display name offset in styles.css --- public/styles.css | 2 +- public_included_ws_fallback/styles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index a3a4a0d..335f3f1 100644 --- a/public/styles.css +++ b/public/styles.css @@ -569,7 +569,7 @@ footer .font-body2 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-bottom: -5px; + margin-bottom: -4px; cursor: text; } diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 92e8841..552512d 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -595,7 +595,7 @@ footer .font-body2 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-bottom: -5px; + margin-bottom: -4px; cursor: text; } From 77b76a3b8dac42720ba11e0cc0d42bb3d50267b4 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 4 Mar 2023 15:46:26 +0100 Subject: [PATCH 012/608] reduce reconnect timers to 1s --- public/scripts/network.js | 4 ++-- public_included_ws_fallback/scripts/network.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/scripts/network.js b/public/scripts/network.js index 75fb9b0..2bf52c8 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -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 }) @@ -161,7 +161,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; } diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index 392c53f..6f40723 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -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 }) @@ -171,7 +171,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; } From 96ed0e53b1103eaba66db63509b0d824446e4214 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 4 Mar 2023 20:50:52 +0100 Subject: [PATCH 013/608] apply styling to clarify that the display-name is editable --- public/styles.css | 12 +++++++++--- public_included_ws_fallback/styles.css | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/public/styles.css b/public/styles.css index 335f3f1..14fc4ec 100644 --- a/public/styles.css +++ b/public/styles.css @@ -562,15 +562,21 @@ footer .font-body2 { #display-name { display: inline-block; text-align: left; - padding-right: 1rem; border: none; outline: none; max-width: 18em; - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-bottom: -4px; cursor: text; + margin-bottom: -4px; + margin-left: -1rem; + padding-right: 0.3rem; + padding-left: 0.3em; + 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), 28%); } #edit-pen { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 552512d..a5046d9 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -588,15 +588,21 @@ footer .font-body2 { #display-name { display: inline-block; text-align: left; - padding-right: 1rem; border: none; outline: none; max-width: 18em; - overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-bottom: -4px; cursor: text; + margin-bottom: -4px; + margin-left: -1rem; + padding-right: 0.3rem; + padding-left: 0.3em; + 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), 28%); } #edit-pen { From 82138c06f3ff1a9c2e82adc5255945b51afe0b41 Mon Sep 17 00:00:00 2001 From: Echo Date: Sat, 4 Mar 2023 15:53:13 -0500 Subject: [PATCH 014/608] Updates CLI to work with OSX base64 --- pairdrop-cli/pairdrop | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pairdrop-cli/pairdrop b/pairdrop-cli/pairdrop index 5a36d2e..9172412 100644 --- a/pairdrop-cli/pairdrop +++ b/pairdrop-cli/pairdrop @@ -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 From f34f5bd4b22d71e990588b516ea9b4bc3880476e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 4 Mar 2023 22:59:49 +0100 Subject: [PATCH 015/608] tidy up code, add tooltip to device name and change color and bg-color of device-name --- public/index.html | 8 +++++--- public/scripts/network.js | 1 - public/scripts/ui.js | 18 +++++++++--------- public/styles.css | 5 ++++- public_included_ws_fallback/index.html | 22 ++++++++++++---------- public_included_ws_fallback/scripts/ui.js | 17 +++++++++-------- public_included_ws_fallback/styles.css | 5 ++++- 7 files changed, 43 insertions(+), 33 deletions(-) diff --git a/public/index.html b/public/index.html index f0aec21..cbb0337 100644 --- a/public/index.html +++ b/public/index.html @@ -59,7 +59,7 @@ - +
- +
+ + + + +
-
- - - - - -
diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 0699043..896608e 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -11,7 +11,6 @@ Events.on('display-name', e => { const me = e.detail.message; const $displayName = $('display-name'); $displayName.setAttribute('placeholder', me.displayName); - $displayName.title = me.deviceName; }); class PeersUI { @@ -78,17 +77,16 @@ class PeersUI { async _saveDisplayName(newDisplayName) { newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') - console.debug(newDisplayName) const savedDisplayName = await this._getSavedDisplayName(); if (newDisplayName === savedDisplayName) return; if (newDisplayName) { PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => { - Events.fire('notify-user', 'Display name is changed permanently.'); + 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', 'Display name is changed only for this session.'); + 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}); @@ -99,7 +97,7 @@ class PeersUI { localStorage.removeItem('editedDisplayName'); Events.fire('notify-user', 'Random Display name is used again.'); }).finally(_ => { - Events.fire('notify-user', 'Display name is randomly generated again.'); + 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: ''}); }); @@ -843,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))); @@ -944,6 +944,7 @@ class PairDeviceDialog extends Dialog { } _onWsConnected() { + this.$pairDeviceBtn.removeAttribute('hidden'); PersistentStorage.getAllRoomSecrets().then(roomSecrets => { Events.fire('room-secrets', roomSecrets); this._evaluateNumberRoomSecrets(); diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 16300e8..eb17081 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -602,7 +602,10 @@ footer .font-body2 { border-right: solid 1rem transparent; border-left: solid 1rem transparent; background-clip: padding-box; - background-color: rgba(var(--text-color), 28%); + background-color: rgba(var(--text-color), 32%); + color: white; + transition: background-color 0.5s ease; + overflow: hidden; } #edit-pen { From cc78b34d2ee04eb9f980832219a92a059c1d1665 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 00:06:57 +0100 Subject: [PATCH 016/608] Revert making peerId ephemeral to prevent duplication of shown peers on reconnect. Implement peerIdHash to prevent rogue users from overtaking peerIds --- README.md | 1 + index.js | 87 +++++++++++++++---- public/scripts/network.js | 8 ++ .../scripts/network.js | 8 ++ 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 63a807f..7f192b0 100644 --- a/README.md +++ b/README.md @@ -80,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). diff --git a/index.js b/index.js index 9e7fd71..d7298a7 100644 --- a/index.js +++ b/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', () => { @@ -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() } }); } @@ -477,7 +476,7 @@ class Peer { this._setIP(request); // set peer id - this.id = crypto.randomUUID(); + this._setPeerId(request) // is WebRTC supported ? this.rtcSupported = request.url.indexOf('webrtc') > -1; @@ -549,6 +548,17 @@ class Peer { return false; } + _setPeerId(request) { + 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(); + } + } + toString() { return `` } @@ -602,6 +612,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); @@ -617,14 +631,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(); diff --git a/public/scripts/network.js b/public/scripts/network.js index 7f9bc44..55fdc89 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -118,6 +118,8 @@ class ServerConnection { } _onDisplayName(msg) { + sessionStorage.setItem("peerId", msg.message.peerId); + sessionStorage.setItem("peerIdHash", msg.message.peerIdHash); Events.fire('display-name', msg); } @@ -126,6 +128,12 @@ class ServerConnection { 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 = 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(); } diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index 58e0019..4c5b255 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -129,6 +129,8 @@ class ServerConnection { } _onDisplayName(msg) { + sessionStorage.setItem("peerId", msg.message.peerId); + sessionStorage.setItem("peerIdHash", msg.message.peerIdHash); Events.fire('display-name', msg); } @@ -137,6 +139,12 @@ class ServerConnection { 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 = 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(); } From 1bc23dc4b3e393b7f739f7d9656a8e85a6758e6a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 00:16:33 +0100 Subject: [PATCH 017/608] fix read rtcConfig.json must be parsed as JSON.. --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index d7298a7..80ffe22 100644 --- a/index.js +++ b/index.js @@ -57,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": [ From 5934e9476197a8a1961bf896d271096e82f93323 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 00:40:53 +0100 Subject: [PATCH 018/608] edit some styling of the display-name edit field --- public/styles.css | 7 ++++--- public_included_ws_fallback/styles.css | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/public/styles.css b/public/styles.css index d120feb..237764e 100644 --- a/public/styles.css +++ b/public/styles.css @@ -564,19 +564,20 @@ footer .font-body2 { text-align: left; border: none; outline: none; - max-width: 18em; + max-width: 15em; text-overflow: ellipsis; white-space: nowrap; cursor: text; - margin-bottom: -4px; 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), 32%); + background-color: rgba(var(--text-color), 43%); color: white; transition: background-color 0.5s ease; overflow: hidden; diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index eb17081..0f8742d 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -590,19 +590,20 @@ footer .font-body2 { text-align: left; border: none; outline: none; - max-width: 18em; + max-width: 15em; text-overflow: ellipsis; white-space: nowrap; cursor: text; - margin-bottom: -4px; 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), 32%); + background-color: rgba(var(--text-color), 43%); color: white; transition: background-color 0.5s ease; overflow: hidden; From c3863a9dd363d1f77ce80bb1c63b0676ebbb4463 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 02:19:41 +0100 Subject: [PATCH 019/608] increase version to v1.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index ff74f69..6301d00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.3.0", + "version": "1.4.0", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 32b1307..e3b7d5c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.3.0", + "version": "1.4.0", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index dd35230..e29f59e 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.3.0'; +const cacheVersion = 'v1.4.0'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index f45869c..de41d23 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.3.0'; +const cacheVersion = 'v1.4.0'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From edf2ab5eb3541797d9da3a70508077d1a59e6406 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 03:36:46 +0100 Subject: [PATCH 020/608] revert some changes to regain stability --- index.js | 4 ++++ public/scripts/network.js | 14 ++++---------- public_included_ws_fallback/scripts/network.js | 14 ++++---------- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 80ffe22..0fa6dae 100644 --- a/index.js +++ b/index.js @@ -368,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]; diff --git a/public/scripts/network.js b/public/scripts/network.js index 55fdc89..f40093a 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -8,7 +8,6 @@ class ServerConnection { constructor() { this._connect(); Events.on('pagehide', _ => this._disconnect()); - Events.on('beforeunload', _ => this._onBeforeUnload()); document.addEventListener('visibilitychange', _ => this._onVisibilityChange()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); @@ -137,7 +136,8 @@ class ServerConnection { return ws_url.toString(); } - _onBeforeUnload() { + _disconnect() { + this.send({ type: 'disconnect' }); if (this._socket) { this._socket.onclose = null; this._socket.close(); @@ -147,10 +147,6 @@ class ServerConnection { } } - _disconnect() { - this.send({ type: 'disconnect' }); - } - _onDisconnect() { console.log('WS: server disconnected'); Events.fire('notify-user', 'Connecting..'); @@ -573,7 +569,7 @@ class RTCPeer extends Peer { const channel = event.channel || event.target; channel.binaryType = 'arraybuffer'; channel.onmessage = e => this._onMessage(e.data); - channel.onclose = e => this._onChannelClosed(e); + channel.onclose = _ => this._onChannelClosed(); this._channel = channel; Events.on('beforeunload', e => this._onBeforeUnload(e)); Events.on('pagehide', _ => this._onPageHide()); @@ -617,8 +613,6 @@ class RTCPeer extends Peer { if (this._busy) { e.preventDefault(); return "There are unfinished transfers. Are you sure you want to close?"; - } else { - this._disconnect(); } } @@ -798,7 +792,7 @@ class PeersManager { _notifyPeerDisplayNameChanged(peerId) { const peer = this.peers[peerId]; - if (!peer || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return; + if (!peer) return; this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName}); } diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index 4c5b255..ea8f946 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -6,7 +6,6 @@ class ServerConnection { constructor() { this._connect(); Events.on('pagehide', _ => this._disconnect()); - Events.on('beforeunload', _ => this._onBeforeUnload()); document.addEventListener('visibilitychange', _ => this._onVisibilityChange()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); @@ -148,7 +147,8 @@ class ServerConnection { return ws_url.toString(); } - _onBeforeUnload() { + _disconnect() { + this.send({ type: 'disconnect' }); if (this._socket) { this._socket.onclose = null; this._socket.close(); @@ -158,10 +158,6 @@ class ServerConnection { } } - _disconnect() { - this.send({ type: 'disconnect' }); - } - _onDisconnect() { console.log('WS: server disconnected'); Events.fire('notify-user', 'Connecting..'); @@ -584,7 +580,7 @@ class RTCPeer extends Peer { const channel = event.channel || event.target; channel.binaryType = 'arraybuffer'; channel.onmessage = e => this._onMessage(e.data); - channel.onclose = e => this._onChannelClosed(e); + channel.onclose = _ => this._onChannelClosed(); this._channel = channel; Events.on('beforeunload', e => this._onBeforeUnload(e)); Events.on('pagehide', _ => this._onPageHide()); @@ -628,8 +624,6 @@ class RTCPeer extends Peer { if (this._busy) { e.preventDefault(); return "There are unfinished transfers. Are you sure you want to close?"; - } else { - this._disconnect(); } } @@ -879,7 +873,7 @@ class PeersManager { _notifyPeerDisplayNameChanged(peerId) { const peer = this.peers[peerId]; - if (!peer || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return; + if (!peer) return; this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName}); } From 27ac7786d09c40b2ea8acfbb8e4e7c59941d94cd Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 03:48:23 +0100 Subject: [PATCH 021/608] increase version to v1.4.1 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6301d00..6696135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.4.0", + "version": "1.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.4.0", + "version": "1.4.1", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index e3b7d5c..f1b5c3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.4.0", + "version": "1.4.1", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index e29f59e..d692b5e 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.0'; +const cacheVersion = 'v1.4.1'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index de41d23..8d99fa9 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.0'; +const cacheVersion = 'v1.4.1'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From d1273ef9cc073d8367264fcc329317250bd16b29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 05:12:00 +0000 Subject: [PATCH 022/608] Bump ua-parser-js from 1.0.33 to 1.0.34 Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.33 to 1.0.34. - [Release notes](https://github.com/faisalman/ua-parser-js/releases) - [Changelog](https://github.com/faisalman/ua-parser-js/blob/1.0.34/changelog.md) - [Commits](https://github.com/faisalman/ua-parser-js/compare/1.0.33...1.0.34) --- updated-dependencies: - dependency-name: ua-parser-js dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6696135..b4a63cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "express": "^4.18.2", "express-rate-limit": "^6.7.0", - "ua-parser-js": "^1.0.33", + "ua-parser-js": "^1.0.34", "unique-names-generator": "^4.3.0", "ws": "^8.12.1" }, @@ -583,9 +583,9 @@ } }, "node_modules/ua-parser-js": { - "version": "1.0.33", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", - "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz", + "integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==", "funding": [ { "type": "opencollective", @@ -1070,9 +1070,9 @@ } }, "ua-parser-js": { - "version": "1.0.33", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", - "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==" + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz", + "integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==" }, "unique-names-generator": { "version": "4.7.1", diff --git a/package.json b/package.json index f1b5c3a..1bf3b4b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "dependencies": { "express": "^4.18.2", "express-rate-limit": "^6.7.0", - "ua-parser-js": "^1.0.33", + "ua-parser-js": "^1.0.34", "unique-names-generator": "^4.3.0", "ws": "^8.12.1" }, From 9f2e4c5f8fb27dc599e8e94b9b3300c104be18ae Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 11:24:19 +0100 Subject: [PATCH 023/608] fix displayName sometimes not exchanged on reload --- public/scripts/network.js | 10 ++++++++-- public_included_ws_fallback/scripts/network.js | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/public/scripts/network.js b/public/scripts/network.js index f40093a..571749b 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -194,6 +194,10 @@ class Peer { this._send(JSON.stringify(message)); } + sendDisplayName(displayName) { + this.sendJSON({type: 'display-name-changed', displayName: displayName}); + } + async createHeader(file) { return { name: file.name, @@ -490,7 +494,8 @@ class Peer { } _onDisplayNameChanged(message) { - if (!message.displayName) return; + if (!message.displayName || this._displayName === message.displayName) return; + this._displayName = message.displayName; Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); } } @@ -707,6 +712,7 @@ class PeersManager { Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); + Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId)); } _onMessage(message) { @@ -793,7 +799,7 @@ class PeersManager { _notifyPeerDisplayNameChanged(peerId) { const peer = this.peers[peerId]; if (!peer) return; - this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName}); + this.peers[peerId].sendDisplayName(this._displayName); } _onDisplayName(displayName) { diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index ea8f946..4fb57bb 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -205,6 +205,10 @@ class Peer { this._send(JSON.stringify(message)); } + sendDisplayName(displayName) { + this.sendJSON({type: 'display-name-changed', displayName: displayName}); + } + async createHeader(file) { return { name: file.name, @@ -501,7 +505,8 @@ class Peer { } _onDisplayNameChanged(message) { - if (!message.displayName) return; + if (!message.displayName || this._displayName === message.displayName) return; + this._displayName = message.displayName; Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); } } @@ -760,6 +765,7 @@ class PeersManager { Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); + Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId)); Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-relay', e => this._onWsRelay(e.detail)); } @@ -874,7 +880,7 @@ class PeersManager { _notifyPeerDisplayNameChanged(peerId) { const peer = this.peers[peerId]; if (!peer) return; - this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName}); + this.peers[peerId].sendDisplayName(this._displayName); } _onDisplayName(displayName) { From fdf024f3787804a179af0fcf55dda3a62f939cfa Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 02:18:07 +0100 Subject: [PATCH 024/608] pairdrop-cli: add fallback if navigator.clipboard.readText() is not available --- public/index.html | 1 + public/scripts/ui.js | 85 +++++++++++++---------- public/styles.css | 21 +++++- public_included_ws_fallback/index.html | 1 + public_included_ws_fallback/scripts/ui.js | 85 +++++++++++++---------- public_included_ws_fallback/styles.css | 21 +++++- 6 files changed, 140 insertions(+), 74 deletions(-) diff --git a/public/index.html b/public/index.html index cbb0337..ad8658b 100644 --- a/public/index.html +++ b/public/index.html @@ -238,6 +238,7 @@ + diff --git a/public/scripts/ui.js b/public/scripts/ui.js index b7e75f3..62c1f83 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1245,21 +1245,21 @@ class Base64ZipDialog extends Dialog { const base64Hash = window.location.hash.substring(1); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); + this.$fallbackTextarea = this.$el.querySelector('.textarea'); if (base64Text) { this.show(); if (base64Text === "paste") { // ?base64text=paste // base64 encoded string is ready to be pasted from clipboard - this.$pasteBtn.innerText = 'Tap here to paste text'; - this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text')); + this.preparePasting("text"); } else if (base64Text === "hash") { // ?base64text=hash#BASE64ENCODED // base64 encoded string is url hash which is never sent to server and faster (recommended) this.processBase64Text(base64Hash) .catch(_ => { Events.fire('notify-user', 'Text content is incorrect.'); - console.log("Text content incorrect.") + console.log("Text content incorrect."); }).finally(_ => { this.hide(); }); @@ -1269,7 +1269,7 @@ class Base64ZipDialog extends Dialog { this.processBase64Text(base64Text) .catch(_ => { Events.fire('notify-user', 'Text content is incorrect.'); - console.log("Text content incorrect.") + console.log("Text content incorrect."); }).finally(_ => { this.hide(); }); @@ -1282,14 +1282,13 @@ class Base64ZipDialog extends Dialog { this.processBase64Zip(base64Hash) .catch(_ => { Events.fire('notify-user', 'File content is incorrect.'); - console.log("File content incorrect.") + console.log("File content incorrect."); }).finally(_ => { this.hide(); }); } else { // ?base64zip=paste || ?base64zip=true - this.$pasteBtn.innerText = 'Tap here to paste files'; - this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file')); + this.preparePasting('files'); } } } @@ -1299,39 +1298,53 @@ class Base64ZipDialog extends Dialog { this.$pasteBtn.innerText = "Processing..."; } - async processClipboard(type) { - if (!navigator.clipboard.readText) { - Events.fire('notify-user', 'This feature is not available on your browser.'); - console.log("navigator.clipboard.readText() is not available on your browser.") - this.hide(); - return; - } - - this._setPasteBtnToProcessing(); - - const base64 = await navigator.clipboard.readText(); - - if (!base64) return; - - if (type === "text") { - this.processBase64Text(base64) - .catch(_ => { - Events.fire('notify-user', 'Clipboard content is incorrect.'); - console.log("Clipboard content is incorrect.") - }).finally(_ => { - this.hide(); - }); + preparePasting(type) { + if (navigator.clipboard.readText) { + this.$pasteBtn.innerText = `Tap here to paste ${type}`; + this.$pasteBtn.addEventListener('click', _ => this.processClipboard(type), { once: true }); } else { - this.processBase64Zip(base64) - .catch(_ => { - Events.fire('notify-user', 'Clipboard content is incorrect.'); - console.log("Clipboard content is incorrect.") - }).finally(_ => { - this.hide(); - }); + console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") + this.$pasteBtn.setAttribute('hidden', ''); + this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); + this.$fallbackTextarea.removeAttribute('hidden'); + this.$fallbackTextarea.addEventListener('input', _ => this.processInput(type), { once: true }); + this.$fallbackTextarea.focus(); } } + async processInput(type) { + const base64 = this.$fallbackTextarea.textContent; + this.$fallbackTextarea.textContent = ''; + try { + // check if input is base64 encoded + window.atob(base64); + await this.processBase64(type, base64); + } catch (e) { + // input is not base64 string. Do nothing. + } + } + + async processClipboard(type) { + this._setPasteBtnToProcessing(); + const base64 = await navigator.clipboard.readText(); + await this.processBase64(type, base64); + } + + async processBase64(type, base64) { + if (!base64) return; + try { + if (type === "text") { + await this.processBase64Text(base64); + } else { + await this.processBase64Zip(base64); + } + } catch(_) { + Events.fire('notify-user', 'Clipboard content is incorrect.'); + console.log("Clipboard content is incorrect.") + } + this.hide(); + } + processBase64Text(base64Text){ return new Promise((resolve) => { this._setPasteBtnToProcessing(); diff --git a/public/styles.css b/public/styles.css index 237764e..db87050 100644 --- a/public/styles.css +++ b/public/styles.css @@ -791,10 +791,29 @@ x-dialog .dialog-subheader { margin: auto -24px; } -#base64-paste-btn { +#base64-paste-btn, +#base64-paste-dialog .textarea { width: 100%; height: 40vh; border: solid 12px #438cff; + text-align: center; +} + +#base64-paste-dialog .textarea { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +#base64-paste-dialog .textarea::before { + font-size: 15px; + letter-spacing: 0.12em; + color: var(--primary-color); + font-weight: 700; + text-transform: uppercase; + content: attr(placeholder); } #base64-paste-dialog button { diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index a2a4b4d..549e16e 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -241,6 +241,7 @@ + diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 896608e..b1f666b 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1246,21 +1246,21 @@ class Base64ZipDialog extends Dialog { const base64Hash = window.location.hash.substring(1); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); + this.$fallbackTextarea = this.$el.querySelector('.textarea'); if (base64Text) { this.show(); if (base64Text === "paste") { // ?base64text=paste // base64 encoded string is ready to be pasted from clipboard - this.$pasteBtn.innerText = 'Tap here to paste text'; - this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text')); + this.preparePasting("text"); } else if (base64Text === "hash") { // ?base64text=hash#BASE64ENCODED // base64 encoded string is url hash which is never sent to server and faster (recommended) this.processBase64Text(base64Hash) .catch(_ => { Events.fire('notify-user', 'Text content is incorrect.'); - console.log("Text content incorrect.") + console.log("Text content incorrect."); }).finally(_ => { this.hide(); }); @@ -1270,7 +1270,7 @@ class Base64ZipDialog extends Dialog { this.processBase64Text(base64Text) .catch(_ => { Events.fire('notify-user', 'Text content is incorrect.'); - console.log("Text content incorrect.") + console.log("Text content incorrect."); }).finally(_ => { this.hide(); }); @@ -1283,14 +1283,13 @@ class Base64ZipDialog extends Dialog { this.processBase64Zip(base64Hash) .catch(_ => { Events.fire('notify-user', 'File content is incorrect.'); - console.log("File content incorrect.") + console.log("File content incorrect."); }).finally(_ => { this.hide(); }); } else { // ?base64zip=paste || ?base64zip=true - this.$pasteBtn.innerText = 'Tap here to paste files'; - this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file')); + this.preparePasting('files'); } } } @@ -1300,39 +1299,53 @@ class Base64ZipDialog extends Dialog { this.$pasteBtn.innerText = "Processing..."; } - async processClipboard(type) { - if (!navigator.clipboard.readText) { - Events.fire('notify-user', 'This feature is not available on your browser.'); - console.log("navigator.clipboard.readText() is not available on your browser.") - this.hide(); - return; - } - - this._setPasteBtnToProcessing(); - - const base64 = await navigator.clipboard.readText(); - - if (!base64) return; - - if (type === "text") { - this.processBase64Text(base64) - .catch(_ => { - Events.fire('notify-user', 'Clipboard content is incorrect.'); - console.log("Clipboard content is incorrect.") - }).finally(_ => { - this.hide(); - }); + preparePasting(type) { + if (navigator.clipboard.readText) { + this.$pasteBtn.innerText = `Tap here to paste ${type}`; + this.$pasteBtn.addEventListener('click', _ => this.processClipboard(type), { once: true }); } else { - this.processBase64Zip(base64) - .catch(_ => { - Events.fire('notify-user', 'Clipboard content is incorrect.'); - console.log("Clipboard content is incorrect.") - }).finally(_ => { - this.hide(); - }); + console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") + this.$pasteBtn.setAttribute('hidden', ''); + this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); + this.$fallbackTextarea.removeAttribute('hidden'); + this.$fallbackTextarea.addEventListener('input', _ => this.processInput(type), { once: true }); + this.$fallbackTextarea.focus(); } } + async processInput(type) { + const base64 = this.$fallbackTextarea.textContent; + this.$fallbackTextarea.textContent = ''; + try { + // check if input is base64 encoded + window.atob(base64); + await this.processBase64(type, base64); + } catch (e) { + // input is not base64 string. Do nothing. + } + } + + async processClipboard(type) { + this._setPasteBtnToProcessing(); + const base64 = await navigator.clipboard.readText(); + await this.processBase64(type, base64); + } + + async processBase64(type, base64) { + if (!base64) return; + try { + if (type === "text") { + await this.processBase64Text(base64); + } else { + await this.processBase64Zip(base64); + } + } catch(_) { + Events.fire('notify-user', 'Clipboard content is incorrect.'); + console.log("Clipboard content is incorrect.") + } + this.hide(); + } + processBase64Text(base64Text){ return new Promise((resolve) => { this._setPasteBtnToProcessing(); diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 0f8742d..2d29898 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -817,10 +817,29 @@ x-dialog .dialog-subheader { margin: auto -24px; } -#base64-paste-btn { +#base64-paste-btn, +#base64-paste-dialog .textarea { width: 100%; height: 40vh; border: solid 12px #438cff; + text-align: center; +} + +#base64-paste-dialog .textarea { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} + +#base64-paste-dialog .textarea::before { + font-size: 15px; + letter-spacing: 0.12em; + color: var(--primary-color); + font-weight: 700; + text-transform: uppercase; + content: attr(placeholder); } #base64-paste-dialog button { From 36e152dc7c1d4a112c0f11c89336291963055b2a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 11:59:56 +0100 Subject: [PATCH 025/608] add { once: true } to deactivate-paste-mode event listener --- public/scripts/network.js | 8 ++++---- public/scripts/ui.js | 2 +- public_included_ws_fallback/scripts/network.js | 8 ++++---- public_included_ws_fallback/scripts/ui.js | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/public/scripts/network.js b/public/scripts/network.js index 55fdc89..01eb27d 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -893,11 +893,11 @@ class Events { window.dispatchEvent(new CustomEvent(type, { detail: detail })); } - static on(type, callback) { - return window.addEventListener(type, callback, false); + static on(type, callback, options = false) { + return window.addEventListener(type, callback, options); } - static off(type, callback) { - return window.removeEventListener(type, callback, false); + static off(type, callback, options = false) { + return window.removeEventListener(type, callback, options); } } diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 62c1f83..340b22b 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -241,7 +241,7 @@ class PeersUI { const _callback = (e) => this._sendClipboardData(e, files, text); Events.on('paste-pointerdown', _callback); - Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback)); + Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true }); this.$cancelPasteModeBtn.removeAttribute('hidden'); diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index 4c5b255..b805654 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -974,11 +974,11 @@ class Events { window.dispatchEvent(new CustomEvent(type, { detail: detail })); } - static on(type, callback) { - return window.addEventListener(type, callback, false); + static on(type, callback, options = false) { + return window.addEventListener(type, callback, options); } - static off(type, callback) { - return window.removeEventListener(type, callback, false); + static off(type, callback, options = false) { + return window.removeEventListener(type, callback, options); } } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index b1f666b..0227167 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -241,7 +241,7 @@ class PeersUI { const _callback = (e) => this._sendClipboardData(e, files, text); Events.on('paste-pointerdown', _callback); - Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback)); + Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true }); this.$cancelPasteModeBtn.removeAttribute('hidden'); From c0d504f6a800b6ce76c080f57682621f2f46eeba Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 12:20:30 +0100 Subject: [PATCH 026/608] remove base64 event listeners manually on hide instead of once: true --- public/scripts/ui.js | 31 +++++++++++++++-------- public_included_ws_fallback/scripts/ui.js | 29 +++++++++++++-------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 340b22b..580b73c 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1301,13 +1301,15 @@ class Base64ZipDialog extends Dialog { preparePasting(type) { if (navigator.clipboard.readText) { this.$pasteBtn.innerText = `Tap here to paste ${type}`; - this.$pasteBtn.addEventListener('click', _ => this.processClipboard(type), { once: true }); + this._clickCallback = _ => this.processClipboard(type); + this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); } else { console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") this.$pasteBtn.setAttribute('hidden', ''); this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); this.$fallbackTextarea.removeAttribute('hidden'); - this.$fallbackTextarea.addEventListener('input', _ => this.processInput(type), { once: true }); + this._inputCallback = _ => this.processInput(type); + this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); this.$fallbackTextarea.focus(); } } @@ -1315,23 +1317,28 @@ class Base64ZipDialog extends Dialog { async processInput(type) { const base64 = this.$fallbackTextarea.textContent; this.$fallbackTextarea.textContent = ''; - try { - // check if input is base64 encoded - window.atob(base64); - await this.processBase64(type, base64); - } catch (e) { - // input is not base64 string. Do nothing. - } + await this.processBase64(type, base64); } async processClipboard(type) { - this._setPasteBtnToProcessing(); const base64 = await navigator.clipboard.readText(); await this.processBase64(type, base64); } + isValidBase64(base64) { + try { + // check if input is base64 encoded + window.atob(base64); + return true; + } catch (e) { + // input is not base64 string. + return false; + } + } + async processBase64(type, base64) { - if (!base64) return; + if (!base64 || !this.isValidBase64(base64)) return; + this._setPasteBtnToProcessing(); try { if (type === "text") { await this.processBase64Text(base64); @@ -1378,6 +1385,8 @@ class Base64ZipDialog extends Dialog { hide() { this.clearBrowserHistory(); + this.$pasteBtn.removeEventListener('click', _ => this._clickCallback()); + this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback()); super.hide(); } } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 0227167..e45143e 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1302,13 +1302,15 @@ class Base64ZipDialog extends Dialog { preparePasting(type) { if (navigator.clipboard.readText) { this.$pasteBtn.innerText = `Tap here to paste ${type}`; - this.$pasteBtn.addEventListener('click', _ => this.processClipboard(type), { once: true }); + this._clickCallback = _ => this.processClipboard(type); + this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); } else { console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") this.$pasteBtn.setAttribute('hidden', ''); this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); this.$fallbackTextarea.removeAttribute('hidden'); - this.$fallbackTextarea.addEventListener('input', _ => this.processInput(type), { once: true }); + this._inputCallback = _ => this.processInput(type); + this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); this.$fallbackTextarea.focus(); } } @@ -1316,23 +1318,28 @@ class Base64ZipDialog extends Dialog { async processInput(type) { const base64 = this.$fallbackTextarea.textContent; this.$fallbackTextarea.textContent = ''; - try { - // check if input is base64 encoded - window.atob(base64); - await this.processBase64(type, base64); - } catch (e) { - // input is not base64 string. Do nothing. - } + await this.processBase64(type, base64); } async processClipboard(type) { - this._setPasteBtnToProcessing(); const base64 = await navigator.clipboard.readText(); await this.processBase64(type, base64); } + isValidBase64(base64) { + try { + // check if input is base64 encoded + window.atob(base64); + return true; + } catch (e) { + // input is not base64 string. + return false; + } + } + async processBase64(type, base64) { - if (!base64) return; + if (!base64 || !this.isValidBase64(base64)) return; + this._setPasteBtnToProcessing(); try { if (type === "text") { await this.processBase64Text(base64); From 79af04d95a7bd49d50bb6a739cabb4b236c29305 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 12:29:17 +0100 Subject: [PATCH 027/608] increase version to v1.4.2 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b4a63cf..4cb0ed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.4.1", + "version": "1.4.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.4.1", + "version": "1.4.2", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 1bf3b4b..9a28eb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.4.1", + "version": "1.4.2", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index d692b5e..c46966a 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.1'; +const cacheVersion = 'v1.4.2'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 8d99fa9..cf98402 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.1'; +const cacheVersion = 'v1.4.2'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From c9dca7e0831df97ecf15f68fa170b7c8c620a551 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 15:05:13 +0100 Subject: [PATCH 028/608] fix download notification and add request notification --- public/scripts/ui.js | 25 +++++++++++++++++++++-- public_included_ws_fallback/scripts/ui.js | 25 +++++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 580b73c..02dceab 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1417,9 +1417,9 @@ class Notifications { this.$button.removeAttribute('hidden'); this.$button.addEventListener('click', _ => this._requestPermission()); } - // Todo: fix Notifications Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('files-received', e => this._downloadNotification(e.detail.files)); + Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId)); } _requestPermission() { @@ -1492,8 +1492,29 @@ class Notifications { } } + _requestNotification(request, peerId) { + if (document.visibilityState !== 'visible') { + let imagesOnly = true; + for(let i=0; i 1) { + descriptor = imagesOnly ? ' images' : ' files'; + } else { + descriptor = imagesOnly ? ' image' : ' file'; + } + let displayName = $(peerId).querySelector('.name').textContent + let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`; + const notification = this._notify(title, 'Click to show'); + } + } + _download(notification) { - $('share-or-download').click(); + $('download-btn').click(); notification.close(); } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index e45143e..8968521 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1416,9 +1416,9 @@ class Notifications { this.$button.removeAttribute('hidden'); this.$button.addEventListener('click', _ => this._requestPermission()); } - // Todo: fix Notifications Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('files-received', e => this._downloadNotification(e.detail.files)); + Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId)); } _requestPermission() { @@ -1491,8 +1491,29 @@ class Notifications { } } + _requestNotification(request, peerId) { + if (document.visibilityState !== 'visible') { + let imagesOnly = true; + for(let i=0; i 1) { + descriptor = imagesOnly ? ' images' : ' files'; + } else { + descriptor = imagesOnly ? ' image' : ' file'; + } + let displayName = $(peerId).querySelector('.name').textContent + let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`; + const notification = this._notify(title, 'Click to show'); + } + } + _download(notification) { - $('share-or-download').click(); + $('download-btn').click(); notification.close(); } From cdfbc7a2dfa4c24f4eb2b89a5e0b093b6b9a2801 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 15:32:58 +0100 Subject: [PATCH 029/608] add missing removal of event listener to ws fallback ui.js --- public_included_ws_fallback/scripts/ui.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 8968521..5762e3b 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1386,6 +1386,8 @@ class Base64ZipDialog extends Dialog { hide() { this.clearBrowserHistory(); + this.$pasteBtn.removeEventListener('click', _ => this._clickCallback()); + this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback()); super.hide(); } } From 660e523263034e9d4b57b5a2bf1e8769d0c3612c Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 15:33:22 +0100 Subject: [PATCH 030/608] prevent sending of displayName if RTCPeer is not connected --- public/scripts/network.js | 5 +++++ public_included_ws_fallback/scripts/network.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/public/scripts/network.js b/public/scripts/network.js index d624812..0083814 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -694,6 +694,11 @@ class RTCPeer extends Peer { _isConnecting() { return this._channel && this._channel.readyState === 'connecting'; } + + sendDisplayName(displayName) { + if (!this._isConnected()) return; + super.sendDisplayName(displayName); + } } class PeersManager { diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index b745b93..c45f37a 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -705,6 +705,11 @@ class RTCPeer extends Peer { _isConnecting() { return this._channel && this._channel.readyState === 'connecting'; } + + sendDisplayName(displayName) { + if (!this._isConnected()) return; + super.sendDisplayName(displayName); + } } class WSPeer extends Peer { From 5eeaae01fe17fefbd185baf07cea927150c77302 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 15:39:24 +0100 Subject: [PATCH 031/608] add connection hash to title of display-name of receive dialogs --- public/scripts/ui.js | 26 ++++++++++++----------- public_included_ws_fallback/scripts/ui.js | 26 ++++++++++++----------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 02dceab..04eef4f 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -300,7 +300,8 @@ class PeerUI { constructor(peer, connectionHash) { this._peer = peer; - this._connectionHash = connectionHash; + this._connectionHash = + `${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`; this._initDom(); this._bindListeners(); @@ -345,8 +346,7 @@ class PeerUI { this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.device-name').textContent = this._deviceName(); - this.$el.querySelector('.connection-hash').textContent = - this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16); + this.$el.querySelector('.connection-hash').textContent = this._connectionHash; } _initDom() { @@ -569,7 +569,7 @@ class ReceiveDialog extends Dialog { } } - _parseFileData(displayName, files, imagesOnly, totalSize) { + _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { if (files.length > 1) { let fileOtherText = ` and ${files.length - 1} other `; if (files.length === 2) { @@ -586,6 +586,7 @@ class ReceiveDialog extends Dialog { this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); this.$fileExtension.innerText = fileExtension; this.$displayName.innerText = displayName; + this.$displayName.title = connectionHash; this.$fileSize.innerText = this._formatFileSize(totalSize); } } @@ -603,8 +604,9 @@ class ReceiveFileDialog extends ReceiveDialog { } _onFilesReceived(sender, files, imagesOnly, totalSize) { - const displayName = $(sender).ui._displayName() - this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); + const displayName = $(sender).ui._displayName(); + const connectionHash = $(sender).ui._connectionHash; + this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); this._nextFiles(); window.blop.play(); } @@ -612,12 +614,11 @@ class ReceiveFileDialog extends ReceiveDialog { _nextFiles() { if (this._busy) return; this._busy = true; - const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift(); - this._displayFiles(peer, displayName, files, imagesOnly, totalSize); + const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift(); + this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize); } _dequeueFile() { - // Todo: change count in document.title and move '- PairDrop' to back if (!this._filesQueue.length) { // nothing to do this._busy = false; return; @@ -655,8 +656,8 @@ class ReceiveFileDialog extends ReceiveDialog { }); } - async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) { - this._parseFileData(displayName, files, imagesOnly, totalSize); + async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) { + this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize); let descriptor, url, filenameDownload; if (files.length === 1) { @@ -803,7 +804,8 @@ class ReceiveRequestDialog extends ReceiveDialog { this.correspondingPeerId = peerId; const displayName = $(peerId).ui._displayName(); - this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize); + const connectionHash = $(peerId).ui._connectionHash; + this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize); if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { let element = document.createElement('img'); diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 5762e3b..b0cb60a 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -300,7 +300,8 @@ class PeerUI { constructor(peer, connectionHash) { this._peer = peer; - this._connectionHash = connectionHash; + this._connectionHash = + `${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`; this._initDom(); this._bindListeners(); @@ -345,8 +346,7 @@ class PeerUI { this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.device-name').textContent = this._deviceName(); - this.$el.querySelector('.connection-hash').textContent = - this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16); + this.$el.querySelector('.connection-hash').textContent = this._connectionHash; } _initDom() { @@ -570,7 +570,7 @@ class ReceiveDialog extends Dialog { } } - _parseFileData(displayName, files, imagesOnly, totalSize) { + _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { if (files.length > 1) { let fileOtherText = ` and ${files.length - 1} other `; if (files.length === 2) { @@ -587,6 +587,7 @@ class ReceiveDialog extends Dialog { this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); this.$fileExtension.innerText = fileExtension; this.$displayName.innerText = displayName; + this.$displayName.title = connectionHash; this.$fileSize.innerText = this._formatFileSize(totalSize); } } @@ -604,8 +605,9 @@ class ReceiveFileDialog extends ReceiveDialog { } _onFilesReceived(sender, files, imagesOnly, totalSize) { - const displayName = $(sender).ui._displayName() - this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); + const displayName = $(sender).ui._displayName(); + const connectionHash = $(sender).ui._connectionHash; + this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); this._nextFiles(); window.blop.play(); } @@ -613,12 +615,11 @@ class ReceiveFileDialog extends ReceiveDialog { _nextFiles() { if (this._busy) return; this._busy = true; - const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift(); - this._displayFiles(peer, displayName, files, imagesOnly, totalSize); + const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift(); + this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize); } _dequeueFile() { - // Todo: change count in document.title and move '- PairDrop' to back if (!this._filesQueue.length) { // nothing to do this._busy = false; return; @@ -656,8 +657,8 @@ class ReceiveFileDialog extends ReceiveDialog { }); } - async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) { - this._parseFileData(displayName, files, imagesOnly, totalSize); + async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) { + this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize); let descriptor, url, filenameDownload; if (files.length === 1) { @@ -804,7 +805,8 @@ class ReceiveRequestDialog extends ReceiveDialog { this.correspondingPeerId = peerId; const displayName = $(peerId).ui._displayName(); - this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize); + const connectionHash = $(peerId).ui._connectionHash; + this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize); if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { let element = document.createElement('img'); From 5fc8e85f75b46c10df57c8c7a3c426f004e349a3 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 15:40:09 +0100 Subject: [PATCH 032/608] increase version to 1.4.3 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4cb0ed8..4d0c1dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.4.2", + "version": "1.4.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.4.2", + "version": "1.4.3", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 9a28eb0..f2335ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.4.2", + "version": "1.4.3", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index c46966a..81b9d4a 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.2'; +const cacheVersion = 'v1.4.3'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index cf98402..17aceaa 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.2'; +const cacheVersion = 'v1.4.3'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From ff8f28660ad3f0d603d44fab2563dd093fad6ce9 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 16:03:34 +0100 Subject: [PATCH 033/608] prevent buttons from submitting form by adding `type="button"` --- public/index.html | 6 +++--- public_included_ws_fallback/index.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/index.html b/public/index.html index ad8658b..5f2aa1e 100644 --- a/public/index.html +++ b/public/index.html @@ -122,7 +122,7 @@
Enter key from another device to continue.
- +
@@ -137,7 +137,7 @@
Are you sure to unpair all devices?
- +
@@ -209,7 +209,7 @@
- +
diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 549e16e..6c70f9f 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -125,7 +125,7 @@
Enter key from another device to continue.
- +
@@ -140,7 +140,7 @@
Are you sure to unpair all devices?
- +
@@ -212,7 +212,7 @@
- +
From 11a988e5509912c0c868786c09efe00f8121c04f Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 6 Mar 2023 16:05:58 +0100 Subject: [PATCH 034/608] increase version to v1.4.4 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4d0c1dc..d045501 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.4.3", + "version": "1.4.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.4.3", + "version": "1.4.4", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index f2335ef..dc6e289 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.4.3", + "version": "1.4.4", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 81b9d4a..72844cc 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.3'; +const cacheVersion = 'v1.4.4'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 17aceaa..99855b8 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.3'; +const cacheVersion = 'v1.4.4'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 490e4db3806f683f7f227bbd8cd6298e33273f22 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 7 Mar 2023 18:25:25 +0100 Subject: [PATCH 035/608] Add information about specifying TURN servers --- docs/faq.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index e19770e..ac493b7 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -58,8 +58,11 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection. ### Transferring many files with paired devices takes too long -Naturally, if traffic needs to be routed through the turn server transfer speed decreases. -As a workaround you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster. +Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases. + +As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance. + +Alternatively, you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster as no TURN server is needed. - [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11) - [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) From 715356aafb276f708fb2772f93797adc79aa7ee9 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 8 Mar 2023 11:35:37 +0100 Subject: [PATCH 036/608] Fix AirDrop typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f192b0..4c545ba 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

PairDrop

- Local file sharing in your browser. Inspired by Apple's Airdrop. + Local file sharing in your browser. Inspired by Apple's AirDrop.
Explore »
From 7ddd600b0c574768ac1a86ff87c739832aa5248f Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 10 Mar 2023 18:47:04 +0100 Subject: [PATCH 037/608] fix display name hidden on Firefox for Android --- public/styles.css | 11 ++++++++--- public_included_ws_fallback/styles.css | 11 ++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/public/styles.css b/public/styles.css index db87050..73e4cb2 100644 --- a/public/styles.css +++ b/public/styles.css @@ -22,13 +22,18 @@ body { } body { - min-height: 100vh; + height: 100%; /* mobile viewport bug fix */ - min-height: -webkit-fill-available; + min-height: -moz-available; /* WebKit-based browsers will ignore this. */ + min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + min-height: fill-available; } html { - height: -webkit-fill-available; + height: 100%; + min-height: -moz-available; /* WebKit-based browsers will ignore this. */ + min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + min-height: fill-available; } .row-reverse { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 2d29898..0a68922 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -23,13 +23,18 @@ body { } body { - min-height: 100vh; + height: 100%; /* mobile viewport bug fix */ - min-height: -webkit-fill-available; + min-height: -moz-available; /* WebKit-based browsers will ignore this. */ + min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + min-height: fill-available; } html { - height: -webkit-fill-available; + height: 100%; + min-height: -moz-available; /* WebKit-based browsers will ignore this. */ + min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ + min-height: fill-available; } .row-reverse { From 1093f4d24661e838ffa29a0e3876940958484da5 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 10 Mar 2023 22:21:19 +0100 Subject: [PATCH 038/608] log error onicecandidateerror --- public/scripts/network.js | 1 + public_included_ws_fallback/scripts/network.js | 1 + 2 files changed, 2 insertions(+) diff --git a/public/scripts/network.js b/public/scripts/network.js index 0083814..615dae2 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -524,6 +524,7 @@ class RTCPeer extends Peer { this._peerId = peerId; this._conn = new RTCPeerConnection(window.rtcConfig); this._conn.onicecandidate = e => this._onIceCandidate(e); + this._conn.onicecandidateerror = e => this._onError(e); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); } diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index c45f37a..0ddf0e7 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -535,6 +535,7 @@ class RTCPeer extends Peer { this._peerId = peerId; this._conn = new RTCPeerConnection(window.rtcConfig); this._conn.onicecandidate = e => this._onIceCandidate(e); + this._conn.onicecandidateerror = e => this._onError(e); this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); } From 028752a8096deade614d85d99f07f09f5c3d5306 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 13 Mar 2023 00:04:48 +0100 Subject: [PATCH 039/608] fixes #76. 'File received' dialog not showing on iOS when big videos are sent. --- public/scripts/ui.js | 72 ++++++++++++++--------- public_included_ws_fallback/scripts/ui.js | 72 ++++++++++++++--------- 2 files changed, 90 insertions(+), 54 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 04eef4f..7610834 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -632,26 +632,34 @@ class ReceiveFileDialog extends ReceiveDialog { createPreviewElement(file) { return new Promise((resolve, reject) => { - let mime = file.type.split('/')[0] - let previewElement = { - image: 'img', - audio: 'audio', - video: 'video' - } + try { + let mime = file.type.split('/')[0] + let previewElement = { + image: 'img', + audio: 'audio', + video: 'video' + } - if (Object.keys(previewElement).indexOf(mime) === -1) { - resolve(false); - } else { - console.log('the file is able to preview'); - let element = document.createElement(previewElement[mime]); - element.src = URL.createObjectURL(file); - element.controls = true; - element.onload = _ => { - this.$previewBox.appendChild(element); - resolve(true) - }; - element.addEventListener('loadeddata', _ => resolve(true)); - element.onerror = _ => reject(`${mime} preview could not be loaded from type ${file.type}`); + if (Object.keys(previewElement).indexOf(mime) === -1) { + resolve(false); + } else { + let element = document.createElement(previewElement[mime]); + element.controls = true; + element.onload = _ => { + this.$previewBox.appendChild(element); + resolve(true); + }; + element.onloadeddata = _ => { + this.$previewBox.appendChild(element); + resolve(true); + }; + element.onerror = _ => { + reject(`${mime} preview could not be loaded from type ${file.type}`); + }; + element.src = URL.createObjectURL(file); + } + } catch (e) { + reject(`preview could not be loaded from type ${file.type}`); } }); } @@ -734,20 +742,30 @@ class ReceiveFileDialog extends ReceiveDialog { setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); }; - this.createPreviewElement(files[0]).finally(_ => { - document.title = files.length === 1 - ? 'File received - PairDrop' - : `${files.length} Files received - PairDrop`; - document.changeFavicon("images/favicon-96x96-notification.png"); - Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) - this.show(); + document.title = files.length === 1 + ? 'File received - PairDrop' + : `${files.length} Files received - PairDrop`; + document.changeFavicon("images/favicon-96x96-notification.png"); + Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) + this.show(); + setTimeout(_ => { if (canShare) { this.$shareBtn.click(); } else { this.$downloadBtn.click(); } - }).catch(r => console.error(r)); + }, 500); + + this.createPreviewElement(files[0]) + .then(canPreview => { + if (canPreview) { + console.log('the file is able to preview'); + } else { + console.log('the file is not able to preview'); + } + }) + .catch(r => console.error(r)); } _downloadFilesIndividually(files) { diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index b0cb60a..91d2f32 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -633,26 +633,34 @@ class ReceiveFileDialog extends ReceiveDialog { createPreviewElement(file) { return new Promise((resolve, reject) => { - let mime = file.type.split('/')[0] - let previewElement = { - image: 'img', - audio: 'audio', - video: 'video' - } + try { + let mime = file.type.split('/')[0] + let previewElement = { + image: 'img', + audio: 'audio', + video: 'video' + } - if (Object.keys(previewElement).indexOf(mime) === -1) { - resolve(false); - } else { - console.log('the file is able to preview'); - let element = document.createElement(previewElement[mime]); - element.src = URL.createObjectURL(file); - element.controls = true; - element.onload = _ => { - this.$previewBox.appendChild(element); - resolve(true) - }; - element.addEventListener('loadeddata', _ => resolve(true)); - element.onerror = _ => reject(`${mime} preview could not be loaded from type ${file.type}`); + if (Object.keys(previewElement).indexOf(mime) === -1) { + resolve(false); + } else { + let element = document.createElement(previewElement[mime]); + element.controls = true; + element.onload = _ => { + this.$previewBox.appendChild(element); + resolve(true); + }; + element.onloadeddata = _ => { + this.$previewBox.appendChild(element); + resolve(true); + }; + element.onerror = _ => { + reject(`${mime} preview could not be loaded from type ${file.type}`); + }; + element.src = URL.createObjectURL(file); + } + } catch (e) { + reject(`preview could not be loaded from type ${file.type}`); } }); } @@ -735,20 +743,30 @@ class ReceiveFileDialog extends ReceiveDialog { setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); }; - this.createPreviewElement(files[0]).finally(_ => { - document.title = files.length === 1 - ? 'File received - PairDrop' - : `${files.length} Files received - PairDrop`; - document.changeFavicon("images/favicon-96x96-notification.png"); - Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) - this.show(); + document.title = files.length === 1 + ? 'File received - PairDrop' + : `${files.length} Files received - PairDrop`; + document.changeFavicon("images/favicon-96x96-notification.png"); + Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) + this.show(); + setTimeout(_ => { if (canShare) { this.$shareBtn.click(); } else { this.$downloadBtn.click(); } - }).catch(r => console.error(r)); + }, 500); + + this.createPreviewElement(files[0]) + .then(canPreview => { + if (canPreview) { + console.log('the file is able to preview'); + } else { + console.log('the file is not able to preview'); + } + }) + .catch(r => console.error(r)); } _downloadFilesIndividually(files) { From fea15d3ee156f597cb78d8b1df3d6abefb60ee6d Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 13 Mar 2023 00:05:57 +0100 Subject: [PATCH 040/608] increase version to v1.4.5 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d045501..deb6464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.4.4", + "version": "1.4.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.4.4", + "version": "1.4.5", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index dc6e289..787d815 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.4.4", + "version": "1.4.5", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 72844cc..c5d4168 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.4'; +const cacheVersion = 'v1.4.5'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 99855b8..7737ecd 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.4'; +const cacheVersion = 'v1.4.5'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 6c6f288c3dac8c6c24f8c82b2ad9701240e0cfd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Mar 2023 05:17:56 +0000 Subject: [PATCH 041/608] Bump ws from 8.12.1 to 8.13.0 Bumps [ws](https://github.com/websockets/ws) from 8.12.1 to 8.13.0. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.12.1...8.13.0) --- updated-dependencies: - dependency-name: ws dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index deb6464..f77b5c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "express-rate-limit": "^6.7.0", "ua-parser-js": "^1.0.34", "unique-names-generator": "^4.3.0", - "ws": "^8.12.1" + "ws": "^8.13.0" }, "engines": { "node": ">=15" @@ -633,9 +633,9 @@ } }, "node_modules/ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "engines": { "node": ">=10.0.0" }, @@ -1095,9 +1095,9 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "ws": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", - "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "requires": {} } } diff --git a/package.json b/package.json index 787d815..8a0def4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "express-rate-limit": "^6.7.0", "ua-parser-js": "^1.0.34", "unique-names-generator": "^4.3.0", - "ws": "^8.12.1" + "ws": "^8.13.0" }, "engines": { "node": ">=15" From 4e0fb89720a420fa59d08587486767d053c5a74d Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 13 Mar 2023 14:21:26 +0100 Subject: [PATCH 042/608] replace javascript operators `??` and `?.` to support older browsers (see #79) --- public/scripts/network.js | 2 +- public/scripts/ui.js | 13 ++++++++++--- public_included_ws_fallback/scripts/network.js | 2 +- public_included_ws_fallback/scripts/ui.js | 13 ++++++++++--- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/public/scripts/network.js b/public/scripts/network.js index 615dae2..e9692d5 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -390,7 +390,7 @@ class Peer { } _onFilesHeader(header) { - if (this._requestAccepted?.header.length) { + if (this._requestAccepted && this._requestAccepted.header.length) { this._lastProgress = 0; this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, this._requestAccepted.totalSize, diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 7610834..f471f4e 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -107,8 +107,15 @@ class PeersUI { _getSavedDisplayName() { return new Promise((resolve) => { PersistentStorage.get('editedDisplayName') - .then(displayName => resolve(displayName ?? "")) - .catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? "")) + .then(displayName => { + if (!displayName) displayName = ""; + resolve(displayName); + }) + .catch(_ => { + let displayName = localStorage.getItem('editedDisplayName'); + if (!displayName) displayName = ""; + resolve(displayName); + }) }); } @@ -825,7 +832,7 @@ class ReceiveRequestDialog extends ReceiveDialog { const connectionHash = $(peerId).ui._connectionHash; this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize); - if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { + if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") { let element = document.createElement('img'); element.src = request.thumbnailDataUrl; this.$previewBox.appendChild(element) diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index 0ddf0e7..f01b6bd 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -401,7 +401,7 @@ class Peer { } _onFilesHeader(header) { - if (this._requestAccepted?.header.length) { + if (this._requestAccepted && this._requestAccepted.header.length) { this._lastProgress = 0; this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, this._requestAccepted.totalSize, diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 91d2f32..d705b9b 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -107,8 +107,15 @@ class PeersUI { _getSavedDisplayName() { return new Promise((resolve) => { PersistentStorage.get('editedDisplayName') - .then(displayName => resolve(displayName ?? "")) - .catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? "")) + .then(displayName => { + if (!displayName) displayName = ""; + resolve(displayName); + }) + .catch(_ => { + let displayName = localStorage.getItem('editedDisplayName'); + if (!displayName) displayName = ""; + resolve(displayName); + }) }); } @@ -826,7 +833,7 @@ class ReceiveRequestDialog extends ReceiveDialog { const connectionHash = $(peerId).ui._connectionHash; this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize); - if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { + if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") { let element = document.createElement('img'); element.src = request.thumbnailDataUrl; this.$previewBox.appendChild(element) From b7781e2bab9c171b7874b03044cc03fabaf9d12d Mon Sep 17 00:00:00 2001 From: Kaindl Network <82705244+kgncloud@users.noreply.github.com> Date: Mon, 13 Mar 2023 20:38:36 +0100 Subject: [PATCH 043/608] Add Healthcheck to Dockerfile --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 3057f35..a307a45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,3 +9,6 @@ RUN npm ci COPY . . EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1 From 4a0cd1f49a7a51cb878d62b6148484abf0b858b9 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 14 Mar 2023 11:48:57 +0100 Subject: [PATCH 044/608] Update issue templates: Point out the FAQ --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index cbb6d42..39a6624 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,6 +1,6 @@ --- name: Bug Report -about: Create a report to help us improve +about: Create a report to help us improve. Please check the FAQ first. title: 'Bug:/Enhancement:/Feature Request: ' labels: '' assignees: '' From 17abc91c86a0f17a2174bd729313854843cd87c9 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 13 Mar 2023 12:15:55 +0100 Subject: [PATCH 045/608] rename function and add event to achieve compatibility with snapdrop-android app --- public/scripts/network.js | 7 +++++-- public_included_ws_fallback/scripts/network.js | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/public/scripts/network.js b/public/scripts/network.js index e9692d5..bf5ac2d 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -329,7 +329,7 @@ class Peer { this._onFilesTransferRequest(messageJSON); break; case 'header': - this._onFilesHeader(messageJSON); + this._onFileHeader(messageJSON); break; case 'partition': this._onReceivedPartitionEnd(messageJSON); @@ -389,7 +389,7 @@ class Peer { this._requestPending = null; } - _onFilesHeader(header) { + _onFileHeader(header) { if (this._requestAccepted && this._requestAccepted.header.length) { this._lastProgress = 0; this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, @@ -443,6 +443,9 @@ class Peer { this._abortTransfer(); } + // include for compatibility with Snapdrop for Android app + Events.fire('file-received', fileBlob); + this._filesReceived.push(fileBlob); if (!this._requestAccepted.header.length) { this._busy = false; diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index f01b6bd..78f6f5a 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -340,7 +340,7 @@ class Peer { this._onFilesTransferRequest(messageJSON); break; case 'header': - this._onFilesHeader(messageJSON); + this._onFileHeader(messageJSON); break; case 'partition': this._onReceivedPartitionEnd(messageJSON); @@ -400,7 +400,7 @@ class Peer { this._requestPending = null; } - _onFilesHeader(header) { + _onFileHeader(header) { if (this._requestAccepted && this._requestAccepted.header.length) { this._lastProgress = 0; this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, @@ -454,6 +454,9 @@ class Peer { this._abortTransfer(); } + // include for compatibility with Snapdrop for Android app + Events.fire('file-received', fileBlob); + this._filesReceived.push(fileBlob); if (!this._requestAccepted.header.length) { this._busy = false; From 1f97c12562ce71a5a632360eb5c0520095700913 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 13 Mar 2023 13:03:07 +0100 Subject: [PATCH 046/608] fix overflow of long device names for snapdrop-android --- public/styles.css | 2 +- public_included_ws_fallback/styles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index 73e4cb2..7b6229a 100644 --- a/public/styles.css +++ b/public/styles.css @@ -459,7 +459,7 @@ x-peer[status] x-icon { text-align: center; } -.name { +.device-descriptor > div { width: 100%; white-space: nowrap; overflow: hidden; diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 0a68922..5ba37a1 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -486,7 +486,7 @@ x-peer.ws-peer .highlight-wrapper { text-align: center; } -.name { +.device-descriptor > div { width: 100%; white-space: nowrap; overflow: hidden; From 3f0909637bc19b08e9e2b3a9cc0f664a5f770cf2 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 13 Mar 2023 23:48:42 +0100 Subject: [PATCH 047/608] fix full button width if only one button is shown --- public/styles.css | 2 +- public_included_ws_fallback/styles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index 7b6229a..644bd04 100644 --- a/public/styles.css +++ b/public/styles.css @@ -720,7 +720,7 @@ x-paper > div:last-child { x-paper > div:last-child > .button { height: 100%; - width: 50%; + width: 100%; } x-paper > div:last-child > .button:not(:last-child) { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 5ba37a1..6193cf2 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -746,7 +746,7 @@ x-paper > div:last-child { x-paper > div:last-child > .button { height: 100%; - width: 50%; + width: 100%; } x-paper > div:last-child > .button:not(:last-child) { From 01cd670afc73365f41a815941bce5361a90fea7a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 14 Mar 2023 15:43:40 +0100 Subject: [PATCH 048/608] Convert FAQ to collapsible sections and add back third-party apps --- docs/faq.md | 119 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 95 insertions(+), 24 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index ac493b7..6e594c1 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,31 +1,35 @@ # Frequently Asked Questions -### Instructions / Discussions -* [Video Instructions](https://www.youtube.com/watch?v=4XN02GkcHUM) (Big thanks to [TheiTeckHq](https://www.youtube.com/channel/UC_DUzWMb8gZZnAbISQjmAfQ)) -* [idownloadblog](http://www.idownloadblog.com/2015/12/29/snapdrop/) -* [thenextweb](http://thenextweb.com/insider/2015/12/27/snapdrop-is-a-handy-web-based-replacement-for-apples-fiddly-airdrop-file-transfer-tool/) -* [winboard](http://www.winboard.org/artikel-ratgeber/6253-dateien-vom-desktop-pc-mit-anderen-plattformen-teilen-mit-snapdrop.html) -* [免費資源網路社群](https://free.com.tw/snapdrop/) -* [Hackernews](https://news.ycombinator.com/front?day=2020-12-24) -* [Reddit](https://www.reddit.com/r/Android/comments/et4qny/snapdrop_is_a_free_open_source_cross_platform/) -* [Producthunt](https://www.producthunt.com/posts/snapdrop) +

+ + Help! I can't install the PWA! + -### Help! I can't install the PWA! if you are using a Chromium-based browser (Chrome, Edge, Brave, etc.), you can easily install PairDrop PWA on your desktop by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net). -Example on how to install a pwa with Edge +Example on how to install a pwa with Edge On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/) +
-### Are there any shortcuts? -Sure! +
+ + Shortcuts? + + +Shortcuts! - Send a message with `CTRL + ENTER` - Close all send and pair dialogs by pressing `Escape`. - Copy a received message to clipboard with `CTRL/⌘ + C`. - Accept file transfer request with `Enter` and decline with `Escape`. +
+ +
+ + How to save images directly to the gallery on iOS? + -### When I receive images on iOS I cannot add them directly to the gallery? Apparently, iOS does not allow images shared from a website to be saved to the gallery directly. It simply does not offer the option for images shared from a website. @@ -33,31 +37,80 @@ iOS Shortcuts to the win: I created a simple iOS shortcut that takes your photos and saves them to your gallery: https://routinehub.co/shortcut/13988/ -### Is it possible to send files or text directly from the context or share menu? +
+ +
+ + Is it possible to send files or text directly from the context or share menu? + + Yes, it finally is! * [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows) * [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) * [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android) -### Is it possible to send files or text directly via CLI? +
+ +
+ + Is it possible to send files or text directly via CLI? + + Yes, it is! * [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) -### What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server? + +
+ +
+ + Are there any Third-Party Apps? + + +Here's a list of some third-party apps compatible with PairDrop: + +1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android) +2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon) +3. Feel free to make one :) +
+ +
+ + What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server? + + It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer. If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages. -### What about privacy? Will files be saved on third-party-servers? +
+ +
+ + What about privacy? Will files be saved on third-party-servers? + + None of your files are ever sent to any server. Files are sent only between peers. PairDrop doesn't even use a database. If you are curious have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js). WebRTC encrypts the files on transit. If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages. -### What about security? Are my files encrypted while being sent between the computers? +
+ +
+ + What about security? Are my files encrypted while being sent between the computers? + + Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection. -### Transferring many files with paired devices takes too long +
+ +
+ + Transferring many files with paired devices takes too long + + Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases. As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance. @@ -71,22 +124,40 @@ Alternatively, you can open a hotspot on one of your devices to bridge the conne You can also use mobile hotspots on phones to do that. Then, all data should be sent directly between devices and your data plan should not be charged. -### Why don't you implement feature xyz? +
+ +
+ + Why don't you implement feature xyz? + + Snapdrop and PairDrop are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer. We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity. If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555). +
-### Snapdrop and PairDrop are awesome! How can I support them? -* [Buy me a cover to support open source software](https://www.buymeacoffee.com/pairdrop) +
+ + Snapdrop and PairDrop are awesome! How can I support them? + + +* [Buy me a coffee to support open source software](https://www.buymeacoffee.com/pairdrop) * [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues) * Share PairDrop on social media. * Fix bugs and make a pull request. * Do security analysis and suggestions +* To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop) +
+ +
+ + How does it work? + -### How does it work? [See here for Information about the Technical Implementation](/docs/technical-documentation.md) +
[< Back](/README.md) From f12067739342ed10bf025aa89f64bba4afbddfea Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 14 Mar 2023 15:52:15 +0100 Subject: [PATCH 049/608] increase version to v1.5.0 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f77b5c1..06d0665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.4.5", + "version": "1.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.4.5", + "version": "1.5.0", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 8a0def4..536b9c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.4.5", + "version": "1.5.0", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index c5d4168..502b9aa 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.5'; +const cacheVersion = 'v1.5.0'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 7737ecd..3809ff5 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.4.5'; +const cacheVersion = 'v1.5.0'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 680ed81bd7eb5f0fe529dd1ffb52063703652ff5 Mon Sep 17 00:00:00 2001 From: Kaindl Network <82705244+kgncloud@users.noreply.github.com> Date: Sat, 18 Mar 2023 23:52:33 +0000 Subject: [PATCH 050/608] Create --- docs/docker-swarm-usage.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/docker-swarm-usage.md diff --git a/docs/docker-swarm-usage.md b/docs/docker-swarm-usage.md new file mode 100644 index 0000000..7a28350 --- /dev/null +++ b/docs/docker-swarm-usage.md @@ -0,0 +1,18 @@ +# Docker Swarm Usage + +## Healthcheck + +The Docker Image includes a Healthcheck with the following options (Look at Dockerfile): + +--interval=30s: This option specifies the time interval at which the health check should be performed. In this case, the health check will be performed every 30 seconds. + +--timeout=10s: This option specifies the amount of time to wait for a response from the health check command. If the response does not arrive within 10 seconds, the health check will be considered a failure. + +--start-period=5s: This option specifies the amount of time to wait before starting the health check process. In this case, the health check process will begin 5 seconds after the container is started. + +--retries=3: This option specifies the number of times Docker should retry the health check before considering the container to be unhealthy. + +The CMD instruction is used to define the command that will be run as part of the health check. In this case, the command is "wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1". This command will attempt to connect to http://localhost:3000/ and if it fails, it will exit with a status code of 1. If this command returns a status code other than 0, the health check will be considered a failure. + +Overall, this HEALTHCHECK instruction is defining a health check process that will run every 30 seconds, wait up to 10 seconds for a response, begin 5 seconds after the container is started, and retry up to 3 times. The health check will consist of attempting to connect to http://localhost:3000/ and will consider the container to be unhealthy if it is unable to connect. + From 195dfd0bb3ac10d4bea487bf2fc44f1fd6317c33 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 25 Mar 2023 03:37:15 +0100 Subject: [PATCH 051/608] Add https requirement for PWAs to faq.md --- docs/faq.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 6e594c1..2f5e260 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -11,6 +11,16 @@ by clicking the install-button in the top-right corner while on [pairdrop.net](h Example on how to install a pwa with Edge On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/) + +
+ +Self-Hosted Instance? + +To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs). +See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more information. + +
+
@@ -23,6 +33,9 @@ Shortcuts! - Close all send and pair dialogs by pressing `Escape`. - Copy a received message to clipboard with `CTRL/⌘ + C`. - Accept file transfer request with `Enter` and decline with `Escape`. + +
+
@@ -37,6 +50,9 @@ iOS Shortcuts to the win: I created a simple iOS shortcut that takes your photos and saves them to your gallery: https://routinehub.co/shortcut/13988/ + +
+
@@ -49,6 +65,9 @@ Yes, it finally is! * [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) * [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android) + +
+
@@ -60,6 +79,9 @@ Yes, it is! * [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) + +
+
@@ -72,6 +94,9 @@ Here's a list of some third-party apps compatible with PairDrop: 1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android) 2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon) 3. Feel free to make one :) + +
+
@@ -83,6 +108,9 @@ It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a S If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages. + +
+
@@ -95,6 +123,9 @@ WebRTC encrypts the files on transit. If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages. + +
+
@@ -104,6 +135,9 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection. + +
+
@@ -124,6 +158,9 @@ Alternatively, you can open a hotspot on one of your devices to bridge the conne You can also use mobile hotspots on phones to do that. Then, all data should be sent directly between devices and your data plan should not be charged. + +
+
@@ -136,6 +173,9 @@ We are not trying to optimize for some edge-cases. We are optimizing the user fl If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555). + +
+
@@ -150,6 +190,9 @@ If you want to learn more about simplicity you can read [Insanely Simple: The Ob * Do security analysis and suggestions * To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop) + +
+
@@ -158,6 +201,9 @@ If you want to learn more about simplicity you can read [Insanely Simple: The Ob [See here for Information about the Technical Implementation](/docs/technical-documentation.md) + +
+
[< Back](/README.md) From bdb39a1d2cca81078ecb109dd860149afec1543c Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 25 Mar 2023 04:08:11 +0100 Subject: [PATCH 052/608] add docker-swarm-usage.md reference to host-your-own.md and tidy up docker-swarm-usage.md --- docs/docker-swarm-usage.md | 39 +++++++++++++++++++++++++++++++------- docs/host-your-own.md | 4 ++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/docs/docker-swarm-usage.md b/docs/docker-swarm-usage.md index 7a28350..ae2c97e 100644 --- a/docs/docker-swarm-usage.md +++ b/docs/docker-swarm-usage.md @@ -2,17 +2,42 @@ ## Healthcheck -The Docker Image includes a Healthcheck with the following options (Look at Dockerfile): +The [Docker Image](../Dockerfile) includes a Healthcheck with the following options: ---interval=30s: This option specifies the time interval at which the health check should be performed. In this case, the health check will be performed every 30 seconds. +``` +--interval=30s +``` +> Specifies the time interval at which the health check should be performed. In this case, the health check will be performed every 30 seconds. ---timeout=10s: This option specifies the amount of time to wait for a response from the health check command. If the response does not arrive within 10 seconds, the health check will be considered a failure. +
---start-period=5s: This option specifies the amount of time to wait before starting the health check process. In this case, the health check process will begin 5 seconds after the container is started. +``` +--timeout=10s +``` +> Specifies the amount of time to wait for a response from the health check command. If the response does not arrive within 10 seconds, the health check will be considered a failure. ---retries=3: This option specifies the number of times Docker should retry the health check before considering the container to be unhealthy. +
-The CMD instruction is used to define the command that will be run as part of the health check. In this case, the command is "wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1". This command will attempt to connect to http://localhost:3000/ and if it fails, it will exit with a status code of 1. If this command returns a status code other than 0, the health check will be considered a failure. +``` +--start-period=5s +``` +> Specifies the amount of time to wait before starting the health check process. In this case, the health check process will begin 5 seconds after the container is started. -Overall, this HEALTHCHECK instruction is defining a health check process that will run every 30 seconds, wait up to 10 seconds for a response, begin 5 seconds after the container is started, and retry up to 3 times. The health check will consist of attempting to connect to http://localhost:3000/ and will consider the container to be unhealthy if it is unable to connect. +
+ +``` +--retries=3 +``` +> Specifies the number of times Docker should retry the health check before considering the container to be unhealthy. + +
+ + +The CMD instruction is used to define the command that will be run as part of the health check. +In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. This command will attempt to connect to `http://localhost:3000/` +and if it fails it will exit with a status code of `1`. If this command returns a status code other than `0`, the health check will be considered a failure. + +Overall, this HEALTHCHECK instruction is defining a health check process that will run every 30 seconds, wait up to 10 seconds for a response, +begin 5 seconds after the container is started, and retry up to 3 times. +The health check will consist of attempting to connect to http://localhost:3000/ and will consider the container to be unhealthy if it is unable to connect. diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 6209895..f93f27a 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -82,6 +82,8 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 gh > > To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) +> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](./docker-swarm-usage.md#docker-swarm-usage) + ### Docker Image self-built #### Build the image ```bash @@ -101,6 +103,8 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -i > > To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) +> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](./docker-swarm-usage.md#docker-swarm-usage) +
## Deployment with Docker Compose From dcc4e8b7479308db88ca7e6a536eb44e9298434f Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 25 Mar 2023 01:54:24 +0100 Subject: [PATCH 053/608] Optimize background animation drastically by using offscreen canvases to reuse frames. Rewrite animate function to prevent it from being called multiple times --- public/scripts/ui.js | 88 +++++++++++++---------- public_included_ws_fallback/scripts/ui.js | 87 +++++++++++++--------- 2 files changed, 104 insertions(+), 71 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index f471f4e..fb44352 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1943,68 +1943,84 @@ Events.on('load', () => { style.zIndex = -1; style.top = 0; style.left = 0; - let ctx = c.getContext('2d'); + let cCtx = c.getContext('2d'); let x0, y0, w, h, dw, offset; + let offscreenCanvases = []; + function init() { + let oldW = w; + let oldH = h; + let oldOffset = offset w = document.documentElement.clientWidth; h = document.documentElement.clientHeight; - c.width = w; - c.height = h; offset = $$('footer').offsetHeight - 32; if (h > 800) offset += 16; + + if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed + + c.width = w; + c.height = h; x0 = w / 2; y0 = h - offset; - dw = Math.max(w, h, 1000) / 13; - drawCircles(); + dw = Math.round(Math.max(w, h, 1000) / 13); + drawCircles(cCtx, 0); + + // enforce redrawing of frames + offscreenCanvases = []; } Events.on('bg-resize', _ => init()); window.onresize = _ => Events.fire('bg-resize'); - function drawCircle(radius) { + function drawCircle(ctx, radius) { ctx.beginPath(); - let color = Math.round(255 * (1 - radius / Math.max(w, h))); - ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)'; + ctx.lineWidth = 2; + let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h)); + ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`; ctx.arc(x0, y0, radius, 0, 2 * Math.PI); ctx.stroke(); - ctx.lineWidth = 2; } - let step = 0; - - function drawCircles() { - ctx.clearRect(0, 0, w, h); - for (let i = 0; i < 8; i++) { - drawCircle(dw * i + step % dw); - } - step += 1; - } - - let loading = true; - - function animate() { - if (loading || !finished()) { - requestAnimationFrame(function() { - drawCircles(); - animate(); - }); + function drawCircles(ctx, frame) { + for (let i = 0; i < 13; i++) { + drawCircle(ctx, dw * i + frame); } } - function finished() { - return step % dw >= dw - 5; + function createOffscreenCanvas(frame) { + let canvas = document.createElement("canvas"); + canvas.width = c.width; + canvas.height = c.height; + offscreenCanvases[frame] = canvas; + let ctx = canvas.getContext('2d'); + drawCircles(ctx, frame); + } + + function drawFrame(frame) { + cCtx.clearRect(0, 0, w, h); + if (!offscreenCanvases[frame]) { + createOffscreenCanvas(frame); + } + cCtx.drawImage(offscreenCanvases[frame], 0, 0); + } + + let animate = true; + let currentFrame = 0; + + function animateBg() { + if (currentFrame + 1 < dw || animate) { + currentFrame = (currentFrame + 1) % dw; + drawFrame(currentFrame); + } + setTimeout(_ => animateBg(), 3000 / dw); } window.animateBackground = function(l) { - if (!l) { - loading = false; - } else if (!loading) { - loading = true; - if (finished()) animate(); - } + animate = l; }; + init(); - animate(); + animateBg(); }); document.changeFavicon = function (src) { diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index d705b9b..8a28a19 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1944,67 +1944,84 @@ Events.on('load', () => { style.zIndex = -1; style.top = 0; style.left = 0; - let ctx = c.getContext('2d'); + let cCtx = c.getContext('2d'); let x0, y0, w, h, dw, offset; + let offscreenCanvases = []; + function init() { + let oldW = w; + let oldH = h; + let oldOffset = offset w = document.documentElement.clientWidth; h = document.documentElement.clientHeight; + offset = $$('footer').offsetHeight - 32; + if (h > 800) offset += 16; + + if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed + c.width = w; c.height = h; - offset = $$('footer').offsetHeight - 32; x0 = w / 2; y0 = h - offset; - dw = Math.max(w, h, 1000) / 13; - drawCircles(); + dw = Math.round(Math.max(w, h, 1000) / 13); + drawCircles(cCtx, 0); + + // enforce redrawing of frames + offscreenCanvases = []; } Events.on('bg-resize', _ => init()); window.onresize = _ => Events.fire('bg-resize'); - function drawCircle(radius) { + function drawCircle(ctx, radius) { ctx.beginPath(); - let color = Math.round(255 * (1 - radius / Math.max(w, h))); - ctx.strokeStyle = 'rgba(' + color + ',' + color + ',' + color + ',0.1)'; + ctx.lineWidth = 2; + let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h)); + ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`; ctx.arc(x0, y0, radius, 0, 2 * Math.PI); ctx.stroke(); - ctx.lineWidth = 2; } - let step = 0; - - function drawCircles() { - ctx.clearRect(0, 0, w, h); - for (let i = 0; i < 8; i++) { - drawCircle(dw * i + step % dw); - } - step += 1; - } - - let loading = true; - - function animate() { - if (loading || !finished()) { - requestAnimationFrame(function() { - drawCircles(); - animate(); - }); + function drawCircles(ctx, frame) { + for (let i = 0; i < 13; i++) { + drawCircle(ctx, dw * i + frame); } } - function finished() { - return step % dw >= dw - 5; + function createOffscreenCanvas(frame) { + let canvas = document.createElement("canvas"); + canvas.width = c.width; + canvas.height = c.height; + offscreenCanvases[frame] = canvas; + let ctx = canvas.getContext('2d'); + drawCircles(ctx, frame); + } + + function drawFrame(frame) { + cCtx.clearRect(0, 0, w, h); + if (!offscreenCanvases[frame]) { + createOffscreenCanvas(frame); + } + cCtx.drawImage(offscreenCanvases[frame], 0, 0); + } + + let animate = true; + let currentFrame = 0; + + function animateBg() { + if (currentFrame + 1 < dw || animate) { + currentFrame = (currentFrame + 1) % dw; + drawFrame(currentFrame); + } + setTimeout(_ => animateBg(), 3000 / dw); } window.animateBackground = function(l) { - if (!l) { - loading = false; - } else if (!loading) { - loading = true; - if (finished()) animate(); - } + animate = l; }; + init(); - animate(); + animateBg(); }); document.changeFavicon = function (src) { From 1bb8a63eed1bb9e5aab344d7f3d7dcef01de0fd0 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 27 Mar 2023 02:31:56 +0200 Subject: [PATCH 054/608] increase version to v1.5.1 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06d0665..5856e1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.5.0", + "version": "1.5.1", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 536b9c2..b5b8d6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.5.0", + "version": "1.5.1", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 502b9aa..d286eb9 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.5.0'; +const cacheVersion = 'v1.5.1'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 3809ff5..25bc379 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.5.0'; +const cacheVersion = 'v1.5.1'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 251df2fbff22b5acc767875ad960353c69c67dfd Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 9 Mar 2023 17:03:44 +0100 Subject: [PATCH 055/608] try to fix share target api --- public/scripts/ui.js | 22 ++++-- public/service-worker.js | 72 ++++++++++++------- public_included_ws_fallback/scripts/ui.js | 22 ++++-- public_included_ws_fallback/service-worker.js | 72 ++++++++++++------- 4 files changed, 128 insertions(+), 60 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index fb44352..cc2a91b 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1617,17 +1617,24 @@ class WebShareTargetUI { console.log('Shared Target Text:', '"' + shareTargetText + '"'); Events.fire('activate-paste-mode', {files: [], text: shareTargetText}) } else if (share_target_type === "files") { - const openRequest = window.indexedDB.open('pairdrop_store') - openRequest.onsuccess( db => { + let openRequest = window.indexedDB.open('pairdrop_store') + openRequest.onsuccess = e => { + const db = e.target.result; const tx = db.transaction('share_target_files', 'readwrite'); const store = tx.objectStore('share_target_files'); const request = store.getAll(); request.onsuccess = _ => { - Events.fire('activate-paste-mode', {files: request.result, text: ""}) + const fileObjects = request.result; + let filesReceived = []; + for (let i=0; i db.close(); } - }) + } } window.history.replaceState({}, "Rewrite URL", '/'); } @@ -1684,7 +1691,7 @@ class PersistentStorage { PersistentStorage.logBrowserNotCapable(); return; } - const DBOpenRequest = window.indexedDB.open('pairdrop_store', 2); + const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3); DBOpenRequest.onerror = (e) => { PersistentStorage.logBrowserNotCapable(); console.log('Error initializing database: '); @@ -1710,7 +1717,10 @@ class PersistentStorage { } try { - db.createObjectStore('share_target_files'); + if (db.objectStoreNames.contains('share_target_files')) { + db.deleteObjectStore('share_target_files'); + } + db.createObjectStore('share_target_files', {autoIncrement: true}); } catch (error) { console.log("Object store named 'share_target_files' already exists") } diff --git a/public/service-worker.js b/public/service-worker.js index d286eb9..e6d1548 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -71,30 +71,12 @@ const update = request => self.addEventListener('fetch', function(event) { if (event.request.method === "POST") { // Requests related to Web Share Target. - event.respondWith( - (async () => { - const formData = await event.request.formData(); - const title = formData.get("title"); - const text = formData.get("text"); - const url = formData.get("url"); - const files = formData.get("files"); - let share_url = "/"; - if (files.length > 0) { - share_url = "/?share-target=files"; - const db = await window.indexedDB.open('pairdrop_store'); - const tx = db.transaction('share_target_files', 'readwrite'); - const store = tx.objectStore('share_target_files'); - for (let i=0; i 0 || text.length > 0 || url.length) { - share_url = `/?share-target=text&title=${title}&text=${text}&url=${url}`; - } - return Response.redirect(encodeURI(share_url), 303); - })() - ); + evaluateRequestData(event.request).then(share_url => { + console.debug(share_url); + event.respondWith( + Response.redirect(encodeURI(share_url), 302) + ); + }) } else { // Regular requests not related to Web Share Target. event.respondWith( @@ -119,3 +101,45 @@ 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("files"); + console.debug(files) + let fileObjects = []; + for (let i=0; i { + if (fileObjects?.length > 0) { + const DBOpenRequest = indexedDB.open('pairdrop_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + for (let i = 0; i < fileObjects.length; i++) { + const transaction = db.transaction('share_target_files', 'readwrite'); + const objectStore = transaction.objectStore('share_target_files'); + + const objectStoreRequest = objectStore.add(fileObjects[i]); + objectStoreRequest.onsuccess = _ => { + if (i === fileObjects.length - 1) resolve('/?share-target=files'); + } + } + } + DBOpenRequest.onerror = _ => { + resolve('/'); + } + } else if (title?.length > 0 || text?.length > 0 || url?.length > 0) { + console.debug(title || text || url); + resolve(`/?share-target=text&title=${title}&text=${text}&url=${url}`); + } else { + resolve('/'); + } + }); +} diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 8a28a19..53b418b 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1618,17 +1618,24 @@ class WebShareTargetUI { console.log('Shared Target Text:', '"' + shareTargetText + '"'); Events.fire('activate-paste-mode', {files: [], text: shareTargetText}) } else if (share_target_type === "files") { - const openRequest = window.indexedDB.open('pairdrop_store') - openRequest.onsuccess( db => { + let openRequest = window.indexedDB.open('pairdrop_store') + openRequest.onsuccess = e => { + const db = e.target.result; const tx = db.transaction('share_target_files', 'readwrite'); const store = tx.objectStore('share_target_files'); const request = store.getAll(); request.onsuccess = _ => { - Events.fire('activate-paste-mode', {files: request.result, text: ""}) + const fileObjects = request.result; + let filesReceived = []; + for (let i=0; i db.close(); } - }) + } } window.history.replaceState({}, "Rewrite URL", '/'); } @@ -1685,7 +1692,7 @@ class PersistentStorage { PersistentStorage.logBrowserNotCapable(); return; } - const DBOpenRequest = window.indexedDB.open('pairdrop_store', 2); + const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3); DBOpenRequest.onerror = (e) => { PersistentStorage.logBrowserNotCapable(); console.log('Error initializing database: '); @@ -1711,7 +1718,10 @@ class PersistentStorage { } try { - db.createObjectStore('share_target_files'); + if (db.objectStoreNames.contains('share_target_files')) { + db.deleteObjectStore('share_target_files'); + } + db.createObjectStore('share_target_files', {autoIncrement: true}); } catch (error) { console.log("Object store named 'share_target_files' already exists") } diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 25bc379..d8f7ece 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -71,30 +71,12 @@ const update = request => self.addEventListener('fetch', function(event) { if (event.request.method === "POST") { // Requests related to Web Share Target. - event.respondWith( - (async () => { - const formData = await event.request.formData(); - const title = formData.get("title"); - const text = formData.get("text"); - const url = formData.get("url"); - const files = formData.get("files"); - let share_url = "/"; - if (files.length > 0) { - share_url = "/?share-target=files"; - const db = await window.indexedDB.open('pairdrop_store'); - const tx = db.transaction('share_target_files', 'readwrite'); - const store = tx.objectStore('share_target_files'); - for (let i=0; i 0 || text.length > 0 || url.length) { - share_url = `/?share-target=text&title=${title}&text=${text}&url=${url}`; - } - return Response.redirect(encodeURI(share_url), 303); - })() - ); + evaluateRequestData(event.request).then(share_url => { + console.debug(share_url); + event.respondWith( + Response.redirect(encodeURI(share_url), 302) + ); + }) } else { // Regular requests not related to Web Share Target. event.respondWith( @@ -119,3 +101,45 @@ 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("files"); + console.debug(files) + let fileObjects = []; + for (let i=0; i { + if (fileObjects?.length > 0) { + const DBOpenRequest = indexedDB.open('pairdrop_store'); + DBOpenRequest.onsuccess = (e) => { + const db = e.target.result; + for (let i = 0; i < fileObjects.length; i++) { + const transaction = db.transaction('share_target_files', 'readwrite'); + const objectStore = transaction.objectStore('share_target_files'); + + const objectStoreRequest = objectStore.add(fileObjects[i]); + objectStoreRequest.onsuccess = _ => { + if (i === fileObjects.length - 1) resolve('/?share-target=files'); + } + } + } + DBOpenRequest.onerror = _ => { + resolve('/'); + } + } else if (title?.length > 0 || text?.length > 0 || url?.length > 0) { + console.debug(title || text || url); + resolve(`/?share-target=text&title=${title}&text=${text}&url=${url}`); + } else { + resolve('/'); + } + }); +} From 34ebd603048602b3209ed085270ab66881d72801 Mon Sep 17 00:00:00 2001 From: Daniel Pham Date: Mon, 27 Mar 2023 21:49:33 +0200 Subject: [PATCH 056/608] Update service worker - files array now matches manifest files name - fixed handling fetch redirect --- public/service-worker.js | 12 ++++++------ public_included_ws_fallback/service-worker.js | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index e6d1548..f49cc5f 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -71,12 +71,12 @@ const update = request => self.addEventListener('fetch', function(event) { if (event.request.method === "POST") { // Requests related to Web Share Target. - evaluateRequestData(event.request).then(share_url => { + event.respondWith((async () => { + let share_url = await evaluateRequestData(event.request); + share_url = event.request.url + share_url.substring(1); console.debug(share_url); - event.respondWith( - Response.redirect(encodeURI(share_url), 302) - ); - }) + return Response.redirect(encodeURI(share_url), 302); + })()); } else { // Regular requests not related to Web Share Target. event.respondWith( @@ -107,7 +107,7 @@ const evaluateRequestData = async function (request) { const title = formData.get("title"); const text = formData.get("text"); const url = formData.get("url"); - const files = formData.getAll("files"); + const files = formData.getAll("allfiles"); console.debug(files) let fileObjects = []; for (let i=0; i self.addEventListener('fetch', function(event) { if (event.request.method === "POST") { // Requests related to Web Share Target. - evaluateRequestData(event.request).then(share_url => { + event.respondWith((async () => { + let share_url = await evaluateRequestData(event.request); + share_url = event.request.url + share_url.substring(1); console.debug(share_url); - event.respondWith( - Response.redirect(encodeURI(share_url), 302) - ); - }) + return Response.redirect(encodeURI(share_url), 302); + })()); } else { // Regular requests not related to Web Share Target. event.respondWith( @@ -107,7 +107,7 @@ const evaluateRequestData = async function (request) { const title = formData.get("title"); const text = formData.get("text"); const url = formData.get("url"); - const files = formData.getAll("files"); + const files = formData.getAll("allfiles"); console.debug(files) let fileObjects = []; for (let i=0; i Date: Tue, 28 Mar 2023 00:42:30 +0200 Subject: [PATCH 057/608] Fix passed arguments for sharing text --- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/service-worker.js b/public/service-worker.js index f49cc5f..7e43687 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -137,7 +137,7 @@ const evaluateRequestData = async function (request) { } } else if (title?.length > 0 || text?.length > 0 || url?.length > 0) { console.debug(title || text || url); - resolve(`/?share-target=text&title=${title}&text=${text}&url=${url}`); + resolve(`/?share-target=text${title ? `&title=${title}` : ''}${text ? `&text=${text}` : ''}${url ? `&url=${url}` : ''}`); } else { resolve('/'); } diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index acb7225..6e815eb 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -137,7 +137,7 @@ const evaluateRequestData = async function (request) { } } else if (title?.length > 0 || text?.length > 0 || url?.length > 0) { console.debug(title || text || url); - resolve(`/?share-target=text&title=${title}&text=${text}&url=${url}`); + resolve(`/?share-target=text${title ? `&title=${title}` : ''}${text ? `&text=${text}` : ''}${url ? `&url=${url}` : ''}`); } else { resolve('/'); } From d0b2c8158286ad50015d59673ebd3ac9baec713d Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 28 Mar 2023 19:07:33 +0200 Subject: [PATCH 058/608] Tidy up code --- public/scripts/ui.js | 7 ++-- public/service-worker.js | 41 ++++++++++--------- public_included_ws_fallback/scripts/ui.js | 7 ++-- public_included_ws_fallback/service-worker.js | 41 ++++++++++--------- 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index cc2a91b..94542bb 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1607,14 +1607,13 @@ class WebShareTargetUI { let shareTargetText; if (url) { - shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable. + shareTargetText = url; // we share only the link - no text. } else if (title && text) { shareTargetText = title + '\r\n' + text; } else { shareTargetText = title + text; } - console.log('Shared Target Text:', '"' + shareTargetText + '"'); Events.fire('activate-paste-mode', {files: [], text: shareTargetText}) } else if (share_target_type === "files") { let openRequest = window.indexedDB.open('pairdrop_store') @@ -1629,10 +1628,10 @@ class WebShareTargetUI { for (let i=0; i db.close(); + + Events.fire('activate-paste-mode', {files: filesReceived, text: ""}) } } } diff --git a/public/service-worker.js b/public/service-worker.js index 7e43687..24c8d08 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -73,8 +73,7 @@ self.addEventListener('fetch', function(event) { // Requests related to Web Share Target. event.respondWith((async () => { let share_url = await evaluateRequestData(event.request); - share_url = event.request.url + share_url.substring(1); - console.debug(share_url); + share_url = event.request.url + share_url; return Response.redirect(encodeURI(share_url), 302); })()); } else { @@ -108,19 +107,20 @@ const evaluateRequestData = async function (request) { const text = formData.get("text"); const url = formData.get("url"); const files = formData.getAll("allfiles"); - console.debug(files) - let fileObjects = []; - for (let i=0; i { - if (fileObjects?.length > 0) { + + return new Promise(async (resolve) => { + if (files && files.length > 0) { + let fileObjects = []; + for (let i=0; i { + DBOpenRequest.onsuccess = e => { const db = e.target.result; for (let i = 0; i < fileObjects.length; i++) { const transaction = db.transaction('share_target_files', 'readwrite'); @@ -128,18 +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('?share-target=files'); } } } DBOpenRequest.onerror = _ => { - resolve('/'); + resolve(''); } - } else if (title?.length > 0 || text?.length > 0 || url?.length > 0) { - console.debug(title || text || url); - resolve(`/?share-target=text${title ? `&title=${title}` : ''}${text ? `&text=${text}` : ''}${url ? `&url=${url}` : ''}`); } else { - resolve('/'); + let share_url = '?share-target=text'; + + if (title) share_url += `&title=${title}`; + if (text) share_url += `&text=${text}`; + if (url) share_url += `&url=${url}`; + + resolve(share_url); } }); } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 53b418b..9c72d12 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1608,14 +1608,13 @@ class WebShareTargetUI { let shareTargetText; if (url) { - shareTargetText = url; // We share only the Link - no text. Because link-only text becomes clickable. + shareTargetText = url; // we share only the link - no text. } else if (title && text) { shareTargetText = title + '\r\n' + text; } else { shareTargetText = title + text; } - console.log('Shared Target Text:', '"' + shareTargetText + '"'); Events.fire('activate-paste-mode', {files: [], text: shareTargetText}) } else if (share_target_type === "files") { let openRequest = window.indexedDB.open('pairdrop_store') @@ -1630,10 +1629,10 @@ class WebShareTargetUI { for (let i=0; i db.close(); + + Events.fire('activate-paste-mode', {files: filesReceived, text: ""}) } } } diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 6e815eb..ea0886d 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -73,8 +73,7 @@ self.addEventListener('fetch', function(event) { // Requests related to Web Share Target. event.respondWith((async () => { let share_url = await evaluateRequestData(event.request); - share_url = event.request.url + share_url.substring(1); - console.debug(share_url); + share_url = event.request.url + share_url; return Response.redirect(encodeURI(share_url), 302); })()); } else { @@ -108,19 +107,20 @@ const evaluateRequestData = async function (request) { const text = formData.get("text"); const url = formData.get("url"); const files = formData.getAll("allfiles"); - console.debug(files) - let fileObjects = []; - for (let i=0; i { - if (fileObjects?.length > 0) { + + return new Promise(async (resolve) => { + if (files && files.length > 0) { + let fileObjects = []; + for (let i=0; i { + DBOpenRequest.onsuccess = e => { const db = e.target.result; for (let i = 0; i < fileObjects.length; i++) { const transaction = db.transaction('share_target_files', 'readwrite'); @@ -128,18 +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('?share-target=files'); } } } DBOpenRequest.onerror = _ => { - resolve('/'); + resolve(''); } - } else if (title?.length > 0 || text?.length > 0 || url?.length > 0) { - console.debug(title || text || url); - resolve(`/?share-target=text${title ? `&title=${title}` : ''}${text ? `&text=${text}` : ''}${url ? `&url=${url}` : ''}`); } else { - resolve('/'); + let share_url = '?share-target=text'; + + if (title) share_url += `&title=${title}`; + if (text) share_url += `&text=${text}`; + if (url) share_url += `&url=${url}`; + + resolve(share_url); } }); } From ab08091f5dd7790c83c09ac222b7daf0196e2b57 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 28 Mar 2023 20:00:05 +0200 Subject: [PATCH 059/608] increase version to v1.5.2 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5856e1f..67eab5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.5.1", + "version": "1.5.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.5.1", + "version": "1.5.2", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index b5b8d6f..fb1e7b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.5.1", + "version": "1.5.2", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 24c8d08..881e496 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.5.1'; +const cacheVersion = 'v1.5.2'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index ea0886d..255742e 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.5.1'; +const cacheVersion = 'v1.5.2'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 0fe36e132c0148105c510a09cd929962d2256897 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 28 Mar 2023 20:24:46 +0200 Subject: [PATCH 060/608] Remove the "under development" message from the share-menu section --- docs/how-to.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/how-to.md b/docs/how-to.md index 621ce5a..a764816 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -33,10 +33,10 @@ https://routinehub.co/shortcut/13990/ ## Send directly from share menu on Android -The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented but not yet tested. -When the PWA is installed, it should register itself to the share-menu of the device automatically. +The [Web Share Target API](https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target) is implemented. + +When the PWA is installed, it will register itself to the share-menu of the device automatically. -This feature is still under development. Please test this feature and create an issue if it does not work. ## Send directly via command-line interface Send files or text with PairDrop via command-line interface. From ac1e88b6a01358c1ef61e0ef746b1f3307f6350d Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 28 Mar 2023 17:26:22 +0200 Subject: [PATCH 061/608] Add possibility to reset theme to auto --- public/index.html | 32 +++++-- public/scripts/theme.js | 97 ++++++++++++++------ public/styles.css | 82 ++++++++++++++--- public_included_ws_fallback/index.html | 32 +++++-- public_included_ws_fallback/scripts/theme.js | 97 ++++++++++++++------ public_included_ws_fallback/styles.css | 78 ++++++++++++++-- 6 files changed, 326 insertions(+), 92 deletions(-) diff --git a/public/index.html b/public/index.html index 5f2aa1e..759c9a4 100644 --- a/public/index.html +++ b/public/index.html @@ -44,11 +44,25 @@ - - - - - + - - - - - + - +
@@ -126,12 +126,12 @@
Input this key on another device
or scan the QR-Code.

- - - - - - + + + + + +
Enter key from another device to continue.
@@ -220,7 +220,7 @@
-
+
@@ -365,11 +365,11 @@ - + - - + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/scripts/qrcode.js b/public/scripts/QRCode.min.js similarity index 100% rename from public/scripts/qrcode.js rename to public/scripts/QRCode.min.js diff --git a/public/styles.css b/public/styles.css index fd29f24..337a82c 100644 --- a/public/styles.css +++ b/public/styles.css @@ -96,7 +96,8 @@ header > div { align-self: flex-start; touch-action: manipulation; } -header > div a { + +header > div .icon-button { height: 40px; transition: all 300ms; } @@ -106,7 +107,7 @@ header > div > div { flex-direction: column; } -header > div:not(:hover) a:not(.selected) { +header > div:not(:hover) .icon-button:not(.selected) { height: 0; opacity: 0; } @@ -125,16 +126,16 @@ header > div:hover::before { margin-bottom: 8px; } -header > div:hover a.selected::before { +header > div:hover .icon-button.selected::before { opacity: 0.1; } @media (pointer: coarse) { - header > div:hover a.selected:hover::before { + header > div:hover .icon-button.selected:hover::before { opacity: 0.2; } - header > div a:not(.selected) { + header > div .icon-button:not(.selected) { height: 0; opacity: 0; pointer-events: none; diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 6cc47b0..29e3ee7 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -45,45 +45,45 @@ - +
@@ -129,12 +129,12 @@
Input this key on another device
or scan the QR-Code.

- - - - - - + + + + + +
Enter key from another device to continue.
@@ -223,7 +223,7 @@
-
+
@@ -368,11 +368,11 @@ - + - - + + diff --git a/public_included_ws_fallback/scripts/qrcode.js b/public_included_ws_fallback/scripts/QRCode.min.js similarity index 100% rename from public_included_ws_fallback/scripts/qrcode.js rename to public_included_ws_fallback/scripts/QRCode.min.js diff --git a/public_included_ws_fallback/scripts/robots.txt b/public_included_ws_fallback/scripts/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public_included_ws_fallback/scripts/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index e68e226..0b7c5d9 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -97,7 +97,8 @@ header > div { align-self: flex-start; touch-action: manipulation; } -header > div a { + +header > div .icon-button { height: 40px; transition: all 300ms; } @@ -107,7 +108,7 @@ header > div > div { flex-direction: column; } -header > div:not(:hover) a:not(.selected) { +header > div:not(:hover) .icon-button:not(.selected) { height: 0; opacity: 0; } @@ -126,16 +127,16 @@ header > div:hover::before { margin-bottom: 8px; } -header > div:hover a.selected::before { +header > div:hover .icon-button.selected::before { opacity: 0.1; } @media (pointer: coarse) { - header > div:hover a.selected:hover::before { + header > div:hover .icon-button.selected:hover::before { opacity: 0.2; } - header > div a:not(.selected) { + header > div .icon-button:not(.selected) { height: 0; opacity: 0; pointer-events: none; From d0046e83cbf653da5189e94d710a60884a64f307 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 17 Apr 2023 15:24:31 +0200 Subject: [PATCH 067/608] remove openrelayproject from rtc_config --- index.js | 8 -------- rtc_config_example.json | 8 -------- 2 files changed, 16 deletions(-) diff --git a/index.js b/index.js index 0fa6dae..6792a36 100644 --- a/index.js +++ b/index.js @@ -63,14 +63,6 @@ const rtcConfig = process.env.RTC_CONFIG "iceServers": [ { "urls": "stun:stun.l.google.com:19302" - }, - { - "urls": "stun:openrelay.metered.ca:80" - }, - { - "urls": "turn:openrelay.metered.ca:443", - "username": "openrelayproject", - "credential": "openrelayproject" } ] }; diff --git a/rtc_config_example.json b/rtc_config_example.json index f78905d..bb327e3 100644 --- a/rtc_config_example.json +++ b/rtc_config_example.json @@ -3,14 +3,6 @@ "iceServers": [ { "urls": "stun:stun.l.google.com:19302" - }, - { - "urls": "stun:openrelay.metered.ca:80" - }, - { - "urls": "turn:openrelay.metered.ca:443", - "username": "openrelayproject", - "credential": "openrelayproject" } ] } From 59dca141b6386936d65f942b6da49e2a5cb69ba2 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 17 Apr 2023 15:25:52 +0200 Subject: [PATCH 068/608] increase version to v1.6.0 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d3f16d..d7ff48b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.5.3", + "version": "1.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.5.3", + "version": "1.6.0", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 7deca5c..5a8da1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.5.3", + "version": "1.6.0", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 9f66380..04ab588 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.5.3'; +const cacheVersion = 'v1.6.0'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index f1d1667..829b13f 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.5.3'; +const cacheVersion = 'v1.6.0'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From cae3bb7c7bcbc43051a9453967c1f0230e2f4002 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 18 Apr 2023 13:06:21 +0200 Subject: [PATCH 069/608] Add server costs to README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 74b5e7d..fdc316d 100644 --- a/README.md +++ b/README.md @@ -89,9 +89,9 @@ You can [host your own instance with Docker](/docs/host-your-own.md). ## Support the Community -PairDrop is free and always will be. Still, we have to pay for the domain. +PairDrop is free and always will be. Still, we have to pay for the domain and the server. -To contribute and support me:
+To contribute and support:
Buy Me A Coffee From 2d8bbd5a79c001c769e5498588d9f668a4a87f3e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 19 Apr 2023 16:50:22 +0200 Subject: [PATCH 070/608] Change docs to include the usage of our own TURN server instead of the TURN server of the Open Relay Project --- README.md | 4 ++-- docs/faq.md | 20 +++++++++++--------- docs/host-your-own.md | 26 ++++++++++---------------- rtc_config_example.json | 5 +++++ 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index fdc316d..0602ff0 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,13 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop) ## Differences to Snapdrop -### Device Pairing +### Device Pairing / Internet Transfer * Pair devices via 6-digit code or QR-Code * Pair devices outside your local network or in complex network environment (public Wi-Fi, company network, Apple Private Relay, VPN etc.). * Connect to devices on your mobile hotspot. * Paired devices will always find each other via shared secrets even after reopening the browser or the Progressive Web App * You will always discover devices on your local network. Paired devices are shown additionally. -* Paired devices outside your local network that are behind a NAT are connected automatically via [Open Relay: Free WebRTC TURN Server](https://www.metered.ca/tools/openrelay/) +* Paired devices outside your local network that are behind a NAT are connected automatically via the PairDrop TURN server. ### [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 files are downloaded automatically if possible. diff --git a/docs/faq.md b/docs/faq.md index 2f5e260..cc95f17 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -104,10 +104,13 @@ Here's a list of some third-party apps compatible with PairDrop: What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server? -It uses a P2P connection if WebRTC is supported by the browser. WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer. +It uses a WebRTC peer to peer connection. WebRTC needs a Signaling Server that is only used to establish a connection. The server is not involved in the file transfer. -If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages. +If devices are on the same network, none of your files are ever sent to any server. +If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC. + +If you host your own instance and want to support devices that do not support WebRTC, you can [start the PairDrop instance with an activated Websocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).
@@ -118,11 +121,12 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R What about privacy? Will files be saved on third-party-servers? -None of your files are ever sent to any server. Files are sent only between peers. PairDrop doesn't even use a database. If you are curious have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js). +Files are sent directly between peers. PairDrop doesn't even use a database. If you are curious, have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js). WebRTC encrypts the files on transit. -If your devices are paired and behind a NAT, the public TURN Server from [Open Relay](https://www.metered.ca/tools/openrelay/) is used to route your files and messages. +If devices are on the same network, none of your files are ever sent to any server. +If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC.
@@ -147,9 +151,7 @@ Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases. -As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance. - -Alternatively, you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster as no TURN server is needed. +You can open a hotspot on one of your devices to bridge the connection which omits the need of the TURN server. - [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11) - [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) @@ -171,7 +173,7 @@ Then, all data should be sent directly between devices and your data plan should Snapdrop and PairDrop are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer. We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity. -If you want to learn more about simplicity you can read [Insanely Simple: The Obsession that Drives Apple's Success](https://www.amazon.com/Insanely-Simple-Ken-Segall-audiobook/dp/B007Z9686O) or [Thinking, Fast and Slow](https://www.amazon.com/Thinking-Fast-Slow-Daniel-Kahneman/dp/0374533555). +If you want to learn more about simplicity you can read *Insanely Simple: The Obsession that Drives Apple's Success* or *Thinking, Fast and Slow*.
@@ -183,7 +185,7 @@ If you want to learn more about simplicity you can read [Insanely Simple: The Ob Snapdrop and PairDrop are awesome! How can I support them? -* [Buy me a coffee to support open source software](https://www.buymeacoffee.com/pairdrop) +* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support open source software * [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues) * Share PairDrop on social media. * Fix bugs and make a pull request. diff --git a/docs/host-your-own.md b/docs/host-your-own.md index f93f27a..f9e5b4d 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -1,6 +1,12 @@ # Deployment Notes The easiest way to get PairDrop up and running is by using Docker. +> TURN server for Internet Transfer +> +> Beware that you have to host your own TURN server in order to enable transfers between different networks. +> +> You can follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) or deploy it via docker-compose (Step 5). + ## Deployment with Docker ### Docker Image from Docker Hub @@ -50,6 +56,8 @@ Set options by using the following flags in the `docker run` command: > Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration. > You can use `pairdrop/rtc_config_example.json` as a starting point. > +> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ +> > Default configuration: > ```json > { @@ -57,14 +65,6 @@ Set options by using the following flags in the `docker run` command: > "iceServers": [ > { > "urls": "stun:stun.l.google.com:19302" -> }, -> { -> "urls": "stun:openrelay.metered.ca:80" -> }, -> { -> "urls": "turn:openrelay.metered.ca:443", -> "username": "openrelayproject", -> "credential": "openrelayproject" > } > ] > } @@ -186,6 +186,8 @@ $env:RTC_CONFIG="rtc_config.json"; npm start ``` > Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration. > You can use `pairdrop/rtc_config_example.json` as a starting point. +> +> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ > > Default configuration: > ```json @@ -194,14 +196,6 @@ $env:RTC_CONFIG="rtc_config.json"; npm start > "iceServers": [ > { > "urls": "stun:stun.l.google.com:19302" -> }, -> { -> "urls": "stun:openrelay.metered.ca:80" -> }, -> { -> "urls": "turn:openrelay.metered.ca:443", -> "username": "openrelayproject", -> "credential": "openrelayproject" > } > ] > } diff --git a/rtc_config_example.json b/rtc_config_example.json index bb327e3..d7e48e8 100644 --- a/rtc_config_example.json +++ b/rtc_config_example.json @@ -3,6 +3,11 @@ "iceServers": [ { "urls": "stun:stun.l.google.com:19302" + }, + { + "urls": "turn:example.com:3478", + "username": "username", + "credential": "password" } ] } From b2fc6415daf32edaf03f684076b331e13913a4f9 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 19 Apr 2023 17:38:14 +0200 Subject: [PATCH 071/608] include example files to run an own TURN server via coturn or via docker-compose --- docker-compose-coturn.yml | 19 +++++++++++++++++++ turnserver_example.conf | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 docker-compose-coturn.yml create mode 100644 turnserver_example.conf diff --git a/docker-compose-coturn.yml b/docker-compose-coturn.yml new file mode 100644 index 0000000..e9a05b4 --- /dev/null +++ b/docker-compose-coturn.yml @@ -0,0 +1,19 @@ +version: "3" +services: + node: + image: "node:lts-alpine" + user: "node" + working_dir: /home/node/app + volumes: + - ./:/home/node/app + command: ash -c "npm i && npm run start:prod" + restart: unless-stopped + ports: + - "3000:3000" + coturn_server: + image: "coturn/coturn" + restart: always + network_mode: "host" + volumes: + - ./turnserver.conf:/etc/coturn/turnserver.conf + #you need to copy turnserver_example.conf to turnserver.conf and specify domain, IP address, user and password diff --git a/turnserver_example.conf b/turnserver_example.conf new file mode 100644 index 0000000..09e7986 --- /dev/null +++ b/turnserver_example.conf @@ -0,0 +1,38 @@ +# TURN server name and realm +realm= +server-name=pairdrop + +# IPs the TURN server listens to +listening-ip=0.0.0.0 + +# External IP-Address of the TURN server +external-ip= + +# Main listening port +listening-port=3478 + +# Further ports that are open for communication +min-port=10000 +max-port=20000 + +# Use fingerprint in TURN message +fingerprint + +# Log file path +log-file=/var/log/turnserver.log + +# Enable verbose logging +verbose + +# Specify the user for the TURN authentification +user=user:password + +# Enable long-term credential mechanism +lt-cred-mech + +# SSL certificates +cert=/etc/letsencrypt/live//cert.pem +pkey=/etc/letsencrypt/live//privkey.pem + +# 443 for TURN over TLS, which can bypass firewalls +tls-listening-port=443 From 87097e9cd420632f014964b1bc378f78e2680459 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 19 Apr 2023 21:15:03 +0200 Subject: [PATCH 072/608] fix header btn shadow styling --- public/styles.css | 5 +++-- public_included_ws_fallback/styles.css | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/public/styles.css b/public/styles.css index 337a82c..463e6bf 100644 --- a/public/styles.css +++ b/public/styles.css @@ -112,7 +112,7 @@ header > div:not(:hover) .icon-button:not(.selected) { opacity: 0; } -header > div:hover::before { +#theme-wrapper:hover::before { border-radius: 20px; background: currentColor; opacity: 0.1; @@ -204,7 +204,8 @@ body { line-height: 18px; } -a { +a, +.icon-button { text-decoration: none; color: currentColor; cursor: pointer; diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 0b7c5d9..b5385c3 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -113,7 +113,7 @@ header > div:not(:hover) .icon-button:not(.selected) { opacity: 0; } -header > div:hover::before { +#theme-wrapper:hover::before { border-radius: 20px; background: currentColor; opacity: 0.1; @@ -205,7 +205,8 @@ body { line-height: 18px; } -a { +a, +.icon-button { text-decoration: none; color: currentColor; cursor: pointer; From 8de899f1241c1424d710ad48b5bbe1e383c864e2 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 19 Apr 2023 21:16:43 +0200 Subject: [PATCH 073/608] increase version to v1.6.1 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d7ff48b..3041f6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.6.0", + "version": "1.6.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.6.0", + "version": "1.6.1", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 5a8da1e..785fe78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.6.0", + "version": "1.6.1", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 04ab588..387485a 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.6.0'; +const cacheVersion = 'v1.6.1'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 829b13f..154997b 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.6.0'; +const cacheVersion = 'v1.6.1'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 5c3f5ece7d640fcd7fb50954cf1c3a50ab50da82 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 20 Apr 2023 21:40:26 +0200 Subject: [PATCH 074/608] increase seo by adding an aria-label and removing 'user-scalable=no' --- public/index.html | 12 ++++++------ public_included_ws_fallback/index.html | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/public/index.html b/public/index.html index 2cad0c6..849f29b 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ PairDrop - + @@ -39,24 +39,24 @@
- +
-
+
-
+
-
+
@@ -264,7 +264,7 @@
- + diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 29e3ee7..05c7fd5 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -6,7 +6,7 @@ PairDrop - + @@ -39,24 +39,24 @@
- +
-
+
-
+
-
+
@@ -267,7 +267,7 @@
- + From 3f72fa116055e9624eb78e7c5eac03827d7a8a31 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 20 Apr 2023 21:11:08 +0200 Subject: [PATCH 075/608] remove fade-in from description (LCP) on page load --- public/scripts/ui.js | 6 ++++++ public/styles.css | 4 ++-- public_included_ws_fallback/scripts/ui.js | 6 ++++++ public_included_ws_fallback/styles.css | 4 ++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 94542bb..eb944b5 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -57,6 +57,12 @@ class PeersUI { console.log("Retrieved edited display name:", displayName) if (displayName) Events.fire('self-display-name-changed', displayName); }); + + + /* prevent animation on load */ + setTimeout(_ => { + this.$xNoPeers.style.animationIterationCount = "1"; + }, 300); } _insertDisplayName(displayName) { diff --git a/public/styles.css b/public/styles.css index 463e6bf..860424d 100644 --- a/public/styles.css +++ b/public/styles.css @@ -405,10 +405,10 @@ x-no-peers { flex-direction: column; padding: 8px; text-align: center; - /* prevent flickering on load */ animation: fade-in 300ms; - animation-delay: 500ms; animation-fill-mode: backwards; + /* prevent flickering on load */ + animation-iteration-count: 0; } x-no-peers h2, diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 9c72d12..71dc0a8 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -57,6 +57,12 @@ class PeersUI { console.log("Retrieved edited display name:", displayName) if (displayName) Events.fire('self-display-name-changed', displayName); }); + + + /* prevent animation on load */ + setTimeout(_ => { + this.$xNoPeers.style.animationIterationCount = "1"; + }, 300); } _insertDisplayName(displayName) { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index b5385c3..33db610 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -415,10 +415,10 @@ x-no-peers { flex-direction: column; padding: 8px; text-align: center; - /* prevent flickering on load */ animation: fade-in 300ms; - animation-delay: 500ms; animation-fill-mode: backwards; + /* prevent flickering on load */ + animation-iteration-count: 0; } x-no-peers h2, From 4c7bdd3a0ff39a1d6961326a9514c5f2eba9c4f7 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 20 Apr 2023 21:44:14 +0200 Subject: [PATCH 076/608] move robots.txt into correct folder --- public_included_ws_fallback/{scripts => }/robots.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename public_included_ws_fallback/{scripts => }/robots.txt (100%) diff --git a/public_included_ws_fallback/scripts/robots.txt b/public_included_ws_fallback/robots.txt similarity index 100% rename from public_included_ws_fallback/scripts/robots.txt rename to public_included_ws_fallback/robots.txt From b42c8a0b1a1e0522af2b21a7d980183627735cd3 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 20 Apr 2023 21:12:06 +0200 Subject: [PATCH 077/608] remove background animation in favor of speed and efficiency --- public/scripts/ui.js | 70 ++++------------------ public_included_ws_fallback/scripts/ui.js | 71 ++++------------------- 2 files changed, 24 insertions(+), 117 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index eb944b5..694a8af 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -181,7 +181,6 @@ class PeersUI { if (!$peer) return; $peer.remove(); this.evaluateOverflowing(); - if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again } _onSecretRoomDeleted(roomSecret) { @@ -321,7 +320,6 @@ class PeerUI { $$('x-peers').appendChild(this.$el) Events.fire('peer-added'); this.$xInstructions = $$('x-instructions'); - setTimeout(_ => window.animateBackground(false), 1750); // Stop animation } html() { @@ -1577,27 +1575,15 @@ class NetworkStatusUI { constructor() { Events.on('offline', _ => this._showOfflineMessage()); Events.on('online', _ => this._showOnlineMessage()); - Events.on('ws-connected', _ => this._onWsConnected()); - Events.on('ws-disconnected', _ => this._onWsDisconnected()); if (!navigator.onLine) this._showOfflineMessage(); } _showOfflineMessage() { Events.fire('notify-user', 'You are offline'); - window.animateBackground(false); } _showOnlineMessage() { Events.fire('notify-user', 'You are back online'); - window.animateBackground(true); - } - - _onWsConnected() { - window.animateBackground(true); - } - - _onWsDisconnected() { - window.animateBackground(false); } } @@ -1948,28 +1934,26 @@ window.addEventListener('beforeinstallprompt', e => { return e.preventDefault(); }); -// Background Animation +// Background Circles Events.on('load', () => { let c = document.createElement('canvas'); - document.body.appendChild(c); 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 cCtx = c.getContext('2d'); let x0, y0, w, h, dw, offset; - let offscreenCanvases = []; - function init() { let oldW = w; let oldH = h; let oldOffset = offset w = document.documentElement.clientWidth; h = document.documentElement.clientHeight; - offset = $$('footer').offsetHeight - 32; + offset = $$('footer').offsetHeight - 33; if (h > 800) offset += 16; if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed @@ -1979,63 +1963,33 @@ Events.on('load', () => { x0 = w / 2; y0 = h - offset; dw = Math.round(Math.max(w, h, 1000) / 13); - drawCircles(cCtx, 0); - // enforce redrawing of frames - offscreenCanvases = []; + if (document.body.contains(c)) { + document.body.removeChild(c); + } + drawCircles(cCtx, dw); + document.body.appendChild(c); } + Events.on('bg-resize', _ => init()); window.onresize = _ => Events.fire('bg-resize'); function drawCircle(ctx, radius) { ctx.beginPath(); ctx.lineWidth = 2; - let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h)); - ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`; + let opacity = 0.3 * (1 - 1.2 * radius / Math.max(w, h)); + ctx.strokeStyle = `rgba(128, 128, 128, ${opacity})`; ctx.arc(x0, y0, radius, 0, 2 * Math.PI); ctx.stroke(); } function drawCircles(ctx, frame) { for (let i = 0; i < 13; i++) { - drawCircle(ctx, dw * i + frame); + drawCircle(ctx, dw * i + frame + 33); } } - function createOffscreenCanvas(frame) { - let canvas = document.createElement("canvas"); - canvas.width = c.width; - canvas.height = c.height; - offscreenCanvases[frame] = canvas; - let ctx = canvas.getContext('2d'); - drawCircles(ctx, frame); - } - - function drawFrame(frame) { - cCtx.clearRect(0, 0, w, h); - if (!offscreenCanvases[frame]) { - createOffscreenCanvas(frame); - } - cCtx.drawImage(offscreenCanvases[frame], 0, 0); - } - - let animate = true; - let currentFrame = 0; - - function animateBg() { - if (currentFrame + 1 < dw || animate) { - currentFrame = (currentFrame + 1) % dw; - drawFrame(currentFrame); - } - setTimeout(_ => animateBg(), 3000 / dw); - } - - window.animateBackground = function(l) { - animate = l; - }; - init(); - animateBg(); }); document.changeFavicon = function (src) { diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 71dc0a8..694a8af 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -181,7 +181,6 @@ class PeersUI { if (!$peer) return; $peer.remove(); this.evaluateOverflowing(); - if ($$('x-peers:empty')) setTimeout(_ => window.animateBackground(true), 1750); // Start animation again } _onSecretRoomDeleted(roomSecret) { @@ -321,7 +320,6 @@ class PeerUI { $$('x-peers').appendChild(this.$el) Events.fire('peer-added'); this.$xInstructions = $$('x-instructions'); - setTimeout(_ => window.animateBackground(false), 1750); // Stop animation } html() { @@ -368,7 +366,6 @@ class PeerUI { this.$el.ui = this; this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`)); this.$el.classList.add('center'); - if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer') this.html(); this._callbackInput = e => this._onFilesSelected(e) @@ -1578,27 +1575,15 @@ class NetworkStatusUI { constructor() { Events.on('offline', _ => this._showOfflineMessage()); Events.on('online', _ => this._showOnlineMessage()); - Events.on('ws-connected', _ => this._onWsConnected()); - Events.on('ws-disconnected', _ => this._onWsDisconnected()); if (!navigator.onLine) this._showOfflineMessage(); } _showOfflineMessage() { Events.fire('notify-user', 'You are offline'); - window.animateBackground(false); } _showOnlineMessage() { Events.fire('notify-user', 'You are back online'); - window.animateBackground(true); - } - - _onWsConnected() { - window.animateBackground(true); - } - - _onWsDisconnected() { - window.animateBackground(false); } } @@ -1949,28 +1934,26 @@ window.addEventListener('beforeinstallprompt', e => { return e.preventDefault(); }); -// Background Animation +// Background Circles Events.on('load', () => { let c = document.createElement('canvas'); - document.body.appendChild(c); 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 cCtx = c.getContext('2d'); let x0, y0, w, h, dw, offset; - let offscreenCanvases = []; - function init() { let oldW = w; let oldH = h; let oldOffset = offset w = document.documentElement.clientWidth; h = document.documentElement.clientHeight; - offset = $$('footer').offsetHeight - 32; + offset = $$('footer').offsetHeight - 33; if (h > 800) offset += 16; if (oldW === w && oldH === h && oldOffset === offset) return; // nothing has changed @@ -1980,63 +1963,33 @@ Events.on('load', () => { x0 = w / 2; y0 = h - offset; dw = Math.round(Math.max(w, h, 1000) / 13); - drawCircles(cCtx, 0); - // enforce redrawing of frames - offscreenCanvases = []; + if (document.body.contains(c)) { + document.body.removeChild(c); + } + drawCircles(cCtx, dw); + document.body.appendChild(c); } + Events.on('bg-resize', _ => init()); window.onresize = _ => Events.fire('bg-resize'); function drawCircle(ctx, radius) { ctx.beginPath(); ctx.lineWidth = 2; - let opacity = 0.2 * (1 - 1.2 * radius / Math.max(w, h)); - ctx.strokeStyle = `rgb(128, 128, 128, ${opacity})`; + let opacity = 0.3 * (1 - 1.2 * radius / Math.max(w, h)); + ctx.strokeStyle = `rgba(128, 128, 128, ${opacity})`; ctx.arc(x0, y0, radius, 0, 2 * Math.PI); ctx.stroke(); } function drawCircles(ctx, frame) { for (let i = 0; i < 13; i++) { - drawCircle(ctx, dw * i + frame); + drawCircle(ctx, dw * i + frame + 33); } } - function createOffscreenCanvas(frame) { - let canvas = document.createElement("canvas"); - canvas.width = c.width; - canvas.height = c.height; - offscreenCanvases[frame] = canvas; - let ctx = canvas.getContext('2d'); - drawCircles(ctx, frame); - } - - function drawFrame(frame) { - cCtx.clearRect(0, 0, w, h); - if (!offscreenCanvases[frame]) { - createOffscreenCanvas(frame); - } - cCtx.drawImage(offscreenCanvases[frame], 0, 0); - } - - let animate = true; - let currentFrame = 0; - - function animateBg() { - if (currentFrame + 1 < dw || animate) { - currentFrame = (currentFrame + 1) % dw; - drawFrame(currentFrame); - } - setTimeout(_ => animateBg(), 3000 / dw); - } - - window.animateBackground = function(l) { - animate = l; - }; - init(); - animateBg(); }); document.changeFavicon = function (src) { From 8f4ce63a0cba1b51909ecd5bceb07941f9a1de77 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 20 Apr 2023 22:04:57 +0200 Subject: [PATCH 078/608] increase version to v1.6.2 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3041f6e..2871a4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.6.1", + "version": "1.6.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.6.1", + "version": "1.6.2", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 785fe78..2b9490f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.6.1", + "version": "1.6.2", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index 387485a..b17e463 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.6.1'; +const cacheVersion = 'v1.6.2'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 154997b..ab7f4d5 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.6.1'; +const cacheVersion = 'v1.6.2'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 5a363e90dd0b3bd6183378a3ce5d8edd962a1dde Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 9 Mar 2023 13:40:53 +0100 Subject: [PATCH 079/608] add debug mode to enable debugging auto discovery --- index.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/index.js b/index.js index 6792a36..5460308 100644 --- a/index.js +++ b/index.js @@ -90,6 +90,12 @@ if (process.argv.includes('--include-ws-fallback')) { app.use(express.static('public')); } +const debugMode = process.env.DEBUG_MODE === "true"; + +if (debugMode) { + console.log("DEBUG_MODE is active. To protect privacy, do not use in production.") +} + app.use(function(req, res) { res.redirect('/'); }); @@ -502,6 +508,17 @@ class Peer { if (this.ip.substring(0,7) === "::ffff:") this.ip = this.ip.substring(7); + 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']); + 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"); + console.debug("----DEBUGGING-PEER-IP-END----"); + } + // IPv4 and IPv6 use different values to refer to localhost // put all peers on the same network as the server into the same room as well if (this.ip === '::1' || this.ipIsPrivate(this.ip)) { From fb08bdaf3642bbf2b1a8ea166fd8cc42bb2c2231 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 27 Apr 2023 18:13:44 +0200 Subject: [PATCH 080/608] add environment variable DEBUG_MODE to docs --- docs/host-your-own.md | 63 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index f9e5b4d..4953b16 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -30,7 +30,7 @@ Set options by using the following flags in the `docker run` command: > - 3000 -> `-p 127.0.0.1:3000:3000` > - 8080 -> `-p 127.0.0.1:8080:3000` ##### Rate limiting requests -``` +```bash -e RATE_LIMIT=true ``` > Limits clients to 1000 requests per 5 min @@ -70,6 +70,31 @@ Set options by using the following flags in the `docker run` command: > } > ``` +##### Debug Mode +```bash +-e DEBUG_MODE="true" +``` + +> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server) +> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status. +> +> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this: +> ``` +> ----DEBUGGING-PEER-IP-START---- +> remoteAddress: ::ffff:172.17.0.1 +> x-forwarded-for: 19.117.63.126 +> cf-connecting-ip: undefined +> PairDrop uses: 19.117.63.126 +> IP is private: false +> if IP is private, '127.0.0.1' is used instead +> ----DEBUGGING-PEER-IP-END---- +> ``` +> If the IP PairDrop uses is the public IP of your device everything is correctly setup. +>To find out your devices public IP visit https://www.whatismyip.com/. +> +> To preserve your clients' privacy, **never use this flag in production!** + +
### Docker Image from GHCR @@ -82,7 +107,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 gh > > To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) -> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](./docker-swarm-usage.md#docker-swarm-usage) +> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage) ### Docker Image self-built #### Build the image @@ -103,7 +128,7 @@ docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -i > > To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) -> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](./docker-swarm-usage.md#docker-swarm-usage) +> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage)
@@ -201,6 +226,36 @@ $env:RTC_CONFIG="rtc_config.json"; npm start > } > ``` +#### Debug Mode +On Unix based systems +```bash +DEBUG_MODE="true" npm start +``` +On Windows +```bash +$env:DEBUG_MODE="true"; npm start +``` + +> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server) +> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status. +> +> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this: +> ``` +> ----DEBUGGING-PEER-IP-START---- +> remoteAddress: ::ffff:172.17.0.1 +> x-forwarded-for: 19.117.63.126 +> cf-connecting-ip: undefined +> PairDrop uses: 19.117.63.126 +> IP is private: false +> if IP is private, '127.0.0.1' is used instead +> ----DEBUGGING-PEER-IP-END---- +> ``` +> If the IP PairDrop uses is the public IP of your device everything is correctly setup. +>To find out your devices public IP visit https://www.whatismyip.com/. +> +> To preserve your clients' privacy, **never use this flag in production!** + + ### Options / Flags #### Local Run ```bash @@ -257,6 +312,8 @@ npm run start:prod -- --localhost-only --include-ws-fallback ## HTTP-Server When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible. +To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode). + ### Using nginx #### Allow http and https requests ``` From fafdbcc829ddd5f0b736da7e02fd86568f395c4c Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 27 Apr 2023 19:16:44 +0200 Subject: [PATCH 081/608] increase version to v1.6.3 --- package-lock.json | 4 ++-- package.json | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2871a4b..8ed975b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.6.2", + "version": "1.6.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.6.2", + "version": "1.6.3", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 2b9490f..d7ff1b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.6.2", + "version": "1.6.3", "description": "", "main": "index.js", "scripts": { diff --git a/public/service-worker.js b/public/service-worker.js index b17e463..d890675 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.6.2'; +const cacheVersion = 'v1.6.3'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index ab7f4d5..af3bb0e 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.6.2'; +const cacheVersion = 'v1.6.3'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From f39bfedf9841ce6e4e767e9b8587c6c733766088 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 4 May 2023 17:34:33 +0200 Subject: [PATCH 082/608] use sha3-512 hash instead of cyrb53 to authenticate peerIds on reconnect --- index.js | 84 +++++++++++++++++++++++--------------------------------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/index.js b/index.js index 6792a36..53f213a 100644 --- a/index.js +++ b/index.js @@ -136,7 +136,7 @@ class PairDropServer { displayName: peer.name.displayName, deviceName: peer.name.deviceName, peerId: peer.id, - peerIdHash: peer.id.hashCode128BitSalted() + peerIdHash: hasher.hashCodeSalted(peer.id) } }); } @@ -238,26 +238,8 @@ class PairDropServer { this._notifyPeers(sender); } - 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; - }); - string += String.fromCharCode.apply(String, arr); - } - return string.substring(0, length) - } - _onPairDeviceInitiate(sender) { - let roomSecret = this.getRandomString(64); + let roomSecret = randomizer.getRandomString(64); let roomKey = this._createRoomKey(sender, roomSecret); if (sender.roomKey) this._removeRoomKey(sender.roomKey); sender.roomKey = roomKey; @@ -583,7 +565,7 @@ class Peer { separator: ' ', dictionaries: [colors, animals], style: 'capital', - seed: this.id.hashCode() + seed: cyrb53(this.id) }) this.name = { @@ -609,7 +591,7 @@ class Peer { } isPeerIdHashValid(peerId, peerIdHash) { - return peerIdHash === peerId.hashCode128BitSalted(); + return peerIdHash === hasher.hashCodeSalted(peerId); } addRoomSecret(roomSecret) { @@ -625,39 +607,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) + } + } })() /* From 0ac3c5a11f920e9c8fff982321c8dc2dfb697f57 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 4 May 2023 17:39:12 +0200 Subject: [PATCH 083/608] remove debugging logs --- public_included_ws_fallback/scripts/network.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index 78f6f5a..6575b18 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -734,7 +734,6 @@ class WSPeer extends Peer { } sendJSON(message) { - console.debug(message) message.to = this._peerId; message.roomType = this._roomType; message.roomSecret = this._roomSecret; @@ -854,7 +853,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); } From 241ea4f98865c5a79c1a046dca3e98b0b774672b Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 4 May 2023 17:38:51 +0200 Subject: [PATCH 084/608] implement auto_accept (#91) and manual unpairing via new Edit Paired Devices Dialog and a BrowserTabsConnector --- index.js | 38 +- public/index.html | 26 +- public/scripts/network.js | 233 +++++++-- public/scripts/ui.js | 489 ++++++++++++----- public/scripts/util.js | 4 + public/styles.css | 163 ++++-- public_included_ws_fallback/index.html | 26 +- .../scripts/network.js | 255 ++++++--- public_included_ws_fallback/scripts/ui.js | 491 +++++++++++++----- public_included_ws_fallback/scripts/util.js | 8 +- public_included_ws_fallback/styles.css | 150 +++++- 11 files changed, 1442 insertions(+), 441 deletions(-) diff --git a/index.js b/index.js index 53f213a..bde7f98 100644 --- a/index.js +++ b/index.js @@ -159,11 +159,8 @@ class PairDropServer { 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); @@ -213,29 +210,26 @@ class PairDropServer { this._joinSecretRooms(sender, roomSecrets); } - _onRoomSecretDeleted(sender, message) { - this._deleteSecretRoom(sender, message.roomSecret) - } - - _onRoomSecretsCleared(sender, message) { + _onRoomSecretsDeleted(sender, message) { for (let i = 0; i
- -
+ diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 8801675..673726d 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -2249,14 +2249,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; @@ -2277,11 +2270,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()); @@ -2297,6 +2286,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); } diff --git a/public/styles.css b/public/styles.css index 1a16425..efb70c5 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1178,6 +1178,14 @@ button::-moz-focus-inner { align-self: end; } +canvas .circles { + width: 100vw; + position: absolute; + z-index: -10; + top: 0; + left: 0; +} + /* Loading Indicator */ .progress { diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 4ced906..ff896c3 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -306,6 +306,7 @@
+ diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 8443d11..4f2229b 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -2250,14 +2250,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; @@ -2277,11 +2270,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()); @@ -2297,6 +2286,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); } diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 9c34a99..a96e5e3 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1204,6 +1204,14 @@ button::-moz-focus-inner { align-self: end; } +canvas .circles { + width: 100vw; + position: absolute; + z-index: -10; + top: 0; + left: 0; +} + /* Loading Indicator */ .progress { From a444be226f883a19dc7c8b7120039a8665adaaa2 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 16 May 2023 19:15:47 +0200 Subject: [PATCH 100/608] Fix canvas selector --- public/styles.css | 2 +- public_included_ws_fallback/styles.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index efb70c5..9273504 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1178,7 +1178,7 @@ button::-moz-focus-inner { align-self: end; } -canvas .circles { +canvas.circles { width: 100vw; position: absolute; z-index: -10; diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index a96e5e3..bd2062c 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1204,7 +1204,7 @@ button::-moz-focus-inner { align-self: end; } -canvas .circles { +canvas.circles { width: 100vw; position: absolute; z-index: -10; From 32e909b8c2112461556c8896165c382dbb547113 Mon Sep 17 00:00:00 2001 From: luckman212 <1992842+luckman212@users.noreply.github.com> Date: Mon, 15 May 2023 13:29:49 -0400 Subject: [PATCH 101/608] fixes for https://github.com/schlagmichdoch/PairDrop/issues/69 (squashed, docs updated, IPV6_LOCALIZE input validation) --- docs/host-your-own.md | 14 ++++++++++++++ index.js | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 4953b16..41ebe9b 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -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 diff --git a/index.js b/index.js index cca34c8..f9a9a34 100644 --- a/index.js +++ b/index.js @@ -96,6 +96,16 @@ if (debugMode) { console.log("DEBUG_MODE is active. To protect privacy, do not use in production.") } +if (process.env.IPV6_LOCALIZE) { + let 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; + } else { + console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment"); + } +} + app.use(function(req, res) { res.redirect('/'); }); @@ -516,11 +526,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"); From b106d90b64a6e6f0cc480112ddd7556b3fac8bf6 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 23 May 2023 02:43:17 +0200 Subject: [PATCH 102/608] Fix ReferenceError: ipv6_lcl is not defined --- index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index f9a9a34..e7e47f1 100644 --- a/index.js +++ b/index.js @@ -96,14 +96,15 @@ if (debugMode) { console.log("DEBUG_MODE is active. To protect privacy, do not use in production.") } +let ipv6_lcl; if (process.env.IPV6_LOCALIZE) { - let ipv6_lcl = parseInt(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; - } else { - console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl > 1 ? "segments" : "segment"); } + + console.log("IPv6 client IPs will be localized to", ipv6_lcl, ipv6_lcl === 1 ? "segment" : "segments"); } app.use(function(req, res) { From 4433e1c58f65061474af40568a7792a62b238005 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 23 May 2023 02:44:25 +0200 Subject: [PATCH 103/608] add version number to about page --- public/index.html | 5 ++++- public/styles.css | 9 +++++++++ public_included_ws_fallback/index.html | 5 ++++- public_included_ws_fallback/styles.css | 9 +++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index 8fcf05e..93a5086 100644 --- a/public/index.html +++ b/public/index.html @@ -276,7 +276,10 @@ -

PairDrop

+
+

PairDrop

+
v1.7.2
+
The easiest way to transfer files across devices
diff --git a/public/styles.css b/public/styles.css index 9273504..7f2913b 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1142,6 +1142,15 @@ 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; --size: max(max(230vw, 230vh), calc(150vh + 150vw)); diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index ff896c3..f8eb380 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -279,7 +279,10 @@ -

PairDrop

+
+

PairDrop

+
v1.7.2
+
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index bd2062c..fb2a5cb 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1168,6 +1168,15 @@ 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; --size: max(max(230vw, 230vh), calc(150vh + 150vw)); From b5987cf01752c61387921d7b03f6478a35beb9ff Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 23 May 2023 02:45:29 +0200 Subject: [PATCH 104/608] increase version to v1.7.3 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f1442c..abef59e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.2", + "version": "1.7.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.2", + "version": "1.7.3", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 62a8c48..5eb4e1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.2", + "version": "1.7.3", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 93a5086..42c209e 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.2
+
v1.7.3
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index 20d2cc8..70d5f25 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.2'; +const cacheVersion = 'v1.7.3'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index f8eb380..78b59eb 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.2
+
v1.7.3
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 85b9e1d..1cd1932 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.2'; +const cacheVersion = 'v1.7.3'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 58b7f6bb7cab637c38bc0d873a322cef02ce65f7 Mon Sep 17 00:00:00 2001 From: fm-sys <64581222+fm-sys@users.noreply.github.com> Date: Fri, 26 May 2023 09:52:17 +0200 Subject: [PATCH 105/608] Add 'files-sent' event --- public/scripts/network.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/scripts/network.js b/public/scripts/network.js index ec448ac..517aea3 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -529,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); @@ -547,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(); } From 27bf0fa74fbce8b7a850ce8254b4cfa2571b9cfa Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 26 May 2023 20:05:50 +0200 Subject: [PATCH 106/608] fix #113 --- public/styles.css | 5 ++++- public_included_ws_fallback/styles.css | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/public/styles.css b/public/styles.css index 7f2913b..db86b60 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1124,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; } diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index fb2a5cb..e384b51 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1150,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; } From 520b772bc8fe8b4521f4ac8491fd2092f1d9fda0 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 26 May 2023 20:24:29 +0200 Subject: [PATCH 107/608] fix #112 and differentiate between textContent and innerText --- public/scripts/ui.js | 4 ++-- public/scripts/util.js | 2 +- public_included_ws_fallback/scripts/ui.js | 4 ++-- public_included_ws_fallback/scripts/util.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 673726d..bfbca32 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1419,7 +1419,7 @@ class ReceiveTextDialog extends Dialog { } async _onCopy() { - await navigator.clipboard.writeText(this.$text.textContent); + await navigator.clipboard.writeText(this.$text.innerText); Events.fire('notify-user', 'Copied to clipboard'); this.hide(); } @@ -1594,7 +1594,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); } diff --git a/public/scripts/util.js b/public/scripts/util.js index bae1e39..f6156f8 100644 --- a/public/scripts/util.js +++ b/public/scripts/util.js @@ -5,7 +5,7 @@ if (!navigator.clipboard) { // A 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 diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 4f2229b..971fe21 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1420,7 +1420,7 @@ class ReceiveTextDialog extends Dialog { } async _onCopy() { - await navigator.clipboard.writeText(this.$text.textContent); + await navigator.clipboard.writeText(this.$text.innerText); Events.fire('notify-user', 'Copied to clipboard'); this.hide(); } @@ -1595,7 +1595,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); } diff --git a/public_included_ws_fallback/scripts/util.js b/public_included_ws_fallback/scripts/util.js index e0cbf24..9b7548c 100644 --- a/public_included_ws_fallback/scripts/util.js +++ b/public_included_ws_fallback/scripts/util.js @@ -5,7 +5,7 @@ if (!navigator.clipboard) { // A 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 From a3a8228327e191d46cc0ade61c0ec625502ce50b Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 26 May 2023 20:37:38 +0200 Subject: [PATCH 108/608] increase version to v1.7.4 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index abef59e..c30c6e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.3", + "version": "1.7.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.3", + "version": "1.7.4", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 5eb4e1c..0958a8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.3", + "version": "1.7.4", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 42c209e..dfb79b4 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.3
+
v1.7.4
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index 70d5f25..1927638 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.3'; +const cacheVersion = 'v1.7.4'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 78b59eb..e298b5a 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.3
+
v1.7.4
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 1cd1932..347ec70 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.3'; +const cacheVersion = 'v1.7.4'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From d36cd3524cace84b2b032ca0fe1bdccb94632625 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 30 May 2023 01:21:17 +0200 Subject: [PATCH 109/608] Fix clearBrowserHistory: url should not always be replaced by "/" as PairDrop might not always be hosted at domain root --- public/scripts/ui.js | 12 ++++--- public/scripts/util.js | 4 +++ public/service-worker.js | 34 +++++++++---------- public_included_ws_fallback/scripts/ui.js | 12 ++++--- public_included_ws_fallback/scripts/util.js | 4 +++ public_included_ws_fallback/service-worker.js | 34 +++++++++---------- 6 files changed, 58 insertions(+), 42 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index bfbca32..cb28677 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1024,7 +1024,8 @@ 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 } } @@ -1575,7 +1576,8 @@ class Base64ZipDialog extends Dialog { } clearBrowserHistory() { - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } hide() { @@ -1791,7 +1793,8 @@ class WebShareTargetUI { } } } - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } } } @@ -1815,7 +1818,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); } } } diff --git a/public/scripts/util.js b/public/scripts/util.js index f6156f8..848dca7 100644 --- a/public/scripts/util.js +++ b/public/scripts/util.js @@ -402,3 +402,7 @@ const cyrb53 = function(str, seed = 0) { function onlyUnique (value, index, array) { return array.indexOf(value) === index; } + +function getUrlWithoutArguments() { + return `${window.location.protocol}//${window.location.host}${window.location.pathname}`; +} diff --git a/public/service-worker.js b/public/service-worker.js index 1927638..238e2e2 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -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 { - 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); } }); } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 971fe21..52055ad 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1025,7 +1025,8 @@ 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 } } @@ -1576,7 +1577,8 @@ class Base64ZipDialog extends Dialog { } clearBrowserHistory() { - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } hide() { @@ -1792,7 +1794,8 @@ class WebShareTargetUI { } } } - window.history.replaceState({}, "Rewrite URL", '/'); + const url = getUrlWithoutArguments(); + window.history.replaceState({}, "Rewrite URL", url); } } } @@ -1816,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); } } } diff --git a/public_included_ws_fallback/scripts/util.js b/public_included_ws_fallback/scripts/util.js index 9b7548c..a5266f8 100644 --- a/public_included_ws_fallback/scripts/util.js +++ b/public_included_ws_fallback/scripts/util.js @@ -403,6 +403,10 @@ 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); diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 347ec70..103a6f1 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -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 { - 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); } }); } From 3e2368c0c9f371e7001a38c68fed50d6493da7c4 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 1 Jun 2023 01:26:53 +0200 Subject: [PATCH 110/608] stabilize connection on reconnect by terminating websocket only on timeout and not always when peer leaves its ip room --- index.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/index.js b/index.js index e7e47f1..2e5bdcb 100644 --- a/index.js +++ b/index.js @@ -219,10 +219,15 @@ 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) { @@ -357,10 +362,6 @@ class PairDropServer { _joinRoom(peer, roomType = 'ip', roomSecret = '') { const room = roomType === 'ip' ? peer.ip : roomSecret; - if (this._rooms[room] && this._rooms[room][peer.id]) { - this._leaveRoom(peer, roomType, roomSecret); - } - // if room doesn't exist, create it if (!this._rooms[room]) { this._rooms[room] = {}; @@ -385,10 +386,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]; @@ -468,8 +465,7 @@ class PairDropServer { peer.lastBeat = Date.now(); } if (Date.now() - peer.lastBeat > 2 * timeout) { - this._leaveRoom(peer); - this._leaveAllSecretRooms(peer); + this._disconnect(peer); return; } From 3505f161c6797d05bc4e440319827ffe37fb7140 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 27 May 2023 01:13:49 +0200 Subject: [PATCH 111/608] strip 'NO-BREAK SPACE' (U+00A0) of received text as some browsers seem to add them when pasting text --- public/scripts/ui.js | 17 +++++++++-------- public_included_ws_fallback/scripts/ui.js | 17 +++++++++-------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index cb28677..b494f58 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1296,13 +1296,13 @@ class SendTextDialog extends Dialog { } async _onKeyDown(e) { - if (this.isShown()) { - 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(); } } @@ -1420,7 +1420,8 @@ class ReceiveTextDialog extends Dialog { } async _onCopy() { - await navigator.clipboard.writeText(this.$text.innerText); + const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); + await navigator.clipboard.writeText(sanitizedText); Events.fire('notify-user', 'Copied to clipboard'); this.hide(); } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index 52055ad..d355468 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1297,13 +1297,13 @@ class SendTextDialog extends Dialog { } async _onKeyDown(e) { - if (this.isShown()) { - 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(); } } @@ -1421,7 +1421,8 @@ class ReceiveTextDialog extends Dialog { } async _onCopy() { - await navigator.clipboard.writeText(this.$text.innerText); + const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); + await navigator.clipboard.writeText(sanitizedText); Events.fire('notify-user', 'Copied to clipboard'); this.hide(); } From f195c686e7fb9627d1f1aacb74afef9f0c7d2f96 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 1 Jun 2023 01:32:06 +0200 Subject: [PATCH 112/608] increase version to v1.7.5 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index c30c6e8..b2ccb52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.4", + "version": "1.7.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.4", + "version": "1.7.5", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 0958a8b..c466e69 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.4", + "version": "1.7.5", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index dfb79b4..20007bf 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.4
+
v1.7.5
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index 238e2e2..cfa4d1e 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.4'; +const cacheVersion = 'v1.7.5'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index e298b5a..db1022d 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.4
+
v1.7.5
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 103a6f1..ee0dcb0 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.4'; +const cacheVersion = 'v1.7.5'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 26bf4d6dc30d5314603cb45ab09894f43d48ac8e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 1 Jun 2023 01:47:28 +0200 Subject: [PATCH 113/608] ensure that otherPeers never receive `peer-left` after `peer-joined` on reconnect by leaving room before rejoining it --- index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.js b/index.js index 2e5bdcb..029a955 100644 --- a/index.js +++ b/index.js @@ -362,6 +362,11 @@ class PairDropServer { _joinRoom(peer, roomType = 'ip', roomSecret = '') { 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); + } + // if room doesn't exist, create it if (!this._rooms[room]) { this._rooms[room] = {}; From 29b91cb17aa7e830f1cb6a6659ac5e2c0b5b265e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 1 Jun 2023 01:51:51 +0200 Subject: [PATCH 114/608] increase version to v1.7.6 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2ccb52..c49aca5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.5", + "version": "1.7.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.5", + "version": "1.7.6", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index c466e69..aae3823 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.5", + "version": "1.7.6", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 20007bf..755673a 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.5
+
v1.7.6
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index cfa4d1e..b409118 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.5'; +const cacheVersion = 'v1.7.6'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index db1022d..6beae65 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.5
+
v1.7.6
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index ee0dcb0..78249b2 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.5'; +const cacheVersion = 'v1.7.6'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 2d0ea9a2f1005eb1b4b0edd950235633930cf99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Wed, 21 Jun 2023 22:55:42 +0000 Subject: [PATCH 115/608] Update FAQ reworked --- docs/faq.md | 124 +++++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index cc95f17..651a826 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -5,20 +5,18 @@ Help! I can't install the PWA! -if you are using a Chromium-based browser (Chrome, Edge, Brave, etc.), you can easily install PairDrop PWA on your desktop +if you are using a Chromium-based browser (Chrome, Edge, Vivaldi, Brave, etc.), you can easily install PairDrop PWA on your desktop by clicking the install-button in the top-right corner while on [pairdrop.net](https://pairdrop.net). Example on how to install a pwa with Edge On Firefox, PWAs are installable via [this browser extensions](https://addons.mozilla.org/de/firefox/addon/pwas-for-firefox/) -
Self-Hosted Instance? To be able to install the PWA from a self-hosted instance, the connection needs to be [established through HTTPS](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Installable_PWAs). -See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more information. - +See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#testing-pwa-related-features) for more info.
@@ -28,12 +26,11 @@ See [this host your own section](https://github.com/schlagmichdoch/PairDrop/blob Shortcuts? -Shortcuts! +Shortcuts - Send a message with `CTRL + ENTER` -- Close all send and pair dialogs by pressing `Escape`. -- Copy a received message to clipboard with `CTRL/⌘ + C`. -- Accept file transfer request with `Enter` and decline with `Escape`. - +- Close all "Send" and "Pair" dialogs by pressing `Esc`. +- Copy a received message to the clipboard with `CTRL/⌘ + C`. +- Accept file-transfer requests with `Enter` and decline with `Esc`.
@@ -44,28 +41,24 @@ Shortcuts! Apparently, iOS does not allow images shared from a website to be saved to the gallery directly. -It simply does not offer the option for images shared from a website. +It simply does not offer that option for images shared from a website. -iOS Shortcuts to the win: +iOS Shortcuts saves the day: I created a simple iOS shortcut that takes your photos and saves them to your gallery: https://routinehub.co/shortcut/13988/ - -
- Is it possible to send files or text directly from the context or share menu? + Is it possible to send files or text directly from the "Context" or "Share" menu? -Yes, it finally is! -* [Send files directly from context menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows) -* [Send directly from share menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) -* [Send directly from share menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android) - - +Yes, it finally is. +* [Send files directly from the "Context" menu on Windows](/docs/how-to.md#send-files-directly-from-context-menu-on-windows) +* [Send directly from the "Share" menu on iOS](/docs/how-to.md#send-directly-from-share-menu-on-ios) +* [Send directly from the "Share" menu on Android](/docs/how-to.md#send-directly-from-share-menu-on-android)
@@ -75,71 +68,81 @@ Yes, it finally is! Is it possible to send files or text directly via CLI? -Yes, it is! - -* [Send directly from command-line interface](/docs/how-to.md#send-directly-via-command-line-interface) - +Yes. +* [Send directly from a command-line interface](/docs/how-to.md#send-directly-via-command-line-interface)
- Are there any Third-Party Apps? + Are there any third-party Apps? -Here's a list of some third-party apps compatible with PairDrop: +These third-party apps are compatible with PairDrop: 1. [Snapdrop Android App](https://github.com/fm-sys/snapdrop-android) 2. [Snapdrop for Firefox (Addon)](https://github.com/ueen/SnapdropFirefoxAddon) 3. Feel free to make one :) -
- What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server? + What about the connection? Is it a P2P connection directly from device to device or is there any third-party-server? -It uses a WebRTC peer to peer connection. WebRTC needs a Signaling Server that is only used to establish a connection. The server is not involved in the file transfer. +It uses a WebRTC peer-to-peer connection. +WebRTC needs a signaling server that is only used to establish a connection. +The server is not involved in the file transfer. -If devices are on the same network, none of your files are ever sent to any server. +If the devices are on the same network, +none of your files are ever sent to any server. -If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC. - -If you host your own instance and want to support devices that do not support WebRTC, you can [start the PairDrop instance with an activated Websocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn). +If your devices are paired and behind a NAT, +the PairDrop TURN Server is used to route your files and messages. +See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) +to learn more about STUN, TURN and WebRTC. +If you host your own instance +and want to support devices that do not support WebRTC, +you can [start the PairDrop instance with an activated WebSocket fallback](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#websocket-fallback-for-vpn).
- What about privacy? Will files be saved on third-party-servers? + What about privacy? Will files be saved on third-party servers? -Files are sent directly between peers. PairDrop doesn't even use a database. If you are curious, have a look [at the Server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js). -WebRTC encrypts the files on transit. +Files are sent directly between peers. +PairDrop doesn't even use a database. +If curious, study [the server](https://github.com/schlagmichdoch/pairdrop/blob/master/index.js). +WebRTC encrypts the files in transit. -If devices are on the same network, none of your files are ever sent to any server. - -If your devices are paired and behind a NAT, the PairDrop TURN Server is used to route your files and messages. See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) to learn more about STUN, TURN and WebRTC. +If the devices are on the same network, +none of your files are ever sent to any server. +If your devices are paired and behind a NAT, +the PairDrop TURN Server is used to route your files and messages. +See the [Technical Documentation](technical-documentation.md#encryption-webrtc-stun-and-turn) +to learn more about STUN, TURN and WebRTC.
- What about security? Are my files encrypted while being sent between the computers? + What about security? Are my files encrypted while sent between the computers? -Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection. - - +Yes. Your files are sent using WebRTC, encrypting them in transit. +To ensure the connection is secure and there is no [MITM](https://wikiless.org/wiki/Man-in-the-middle_attack), +compare the security number shown under the device name on both devices. +The security number is different for every connection.
@@ -149,18 +152,18 @@ Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure Transferring many files with paired devices takes too long -Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases. +Naturally, if traffic needs to be routed through the TURN server +because your devices are behind different NATs, transfer speed decreases. -You can open a hotspot on one of your devices to bridge the connection which omits the need of the TURN server. +You can open a hotspot on one of your devices to bridge the connection, +which omits the need of the TURN server. - [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11) -- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) +- [How to open a hotspot on macOS](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac) - [Library to open a hotspot on Linux](https://github.com/lakinduakash/linux-wifi-hotspot) You can also use mobile hotspots on phones to do that. -Then, all data should be sent directly between devices and your data plan should not be charged. - - +Then, all data should be sent directly between devices and not use your data plan.
@@ -170,10 +173,16 @@ Then, all data should be sent directly between devices and your data plan should Why don't you implement feature xyz? -Snapdrop and PairDrop are a study in radical simplicity. The user interface is insanely simple. Features are chosen very carefully because complexity grows quadratically since every feature potentially interferes with each other feature. We focus very narrowly on a single use case: instant file transfer. -We are not trying to optimize for some edge-cases. We are optimizing the user flow of the average users. Don't be sad if we decline your feature request for the sake of simplicity. +Snapdrop and PairDrop are a study in radical simplicity. +The user interface is insanely simple. +Features are chosen very carefully because complexity grows quadratically +since every feature potentially interferes with each other feature. +We focus very narrowly on a single use case: instant file transfer. +Not facilitating optimal edge-cases means better flow for average users. +Don't be sad. We may decline your feature request for the sake of simplicity. -If you want to learn more about simplicity you can read *Insanely Simple: The Obsession that Drives Apple's Success* or *Thinking, Fast and Slow*. +Read *Insanely Simple: The Obsession that Drives Apple's Success*, +and/or *Thinking, Fast and Slow* to learn more.
@@ -182,17 +191,15 @@ If you want to learn more about simplicity you can read *Insanely Simple: The Ob
- Snapdrop and PairDrop are awesome! How can I support them? + Snapdrop and PairDrop are awesome. How can I support them? -* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support open source software +* [Buy me a coffee](https://www.buymeacoffee.com/pairdrop) to pay for the domain and the server, and support libre software. * [File bugs, give feedback, submit suggestions](https://github.com/schlagmichdoch/pairdrop/issues) * Share PairDrop on social media. * Fix bugs and make a pull request. -* Do security analysis and suggestions +* Do some security analysis and make suggestions. * To support the original Snapdrop and its creator go to [his GitHub page](https://github.com/RobinLinus/snapdrop) - -
@@ -202,8 +209,7 @@ If you want to learn more about simplicity you can read *Insanely Simple: The Ob How does it work? -[See here for Information about the Technical Implementation](/docs/technical-documentation.md) - +[See here for info about the technical implementation](/docs/technical-documentation.md)
From f50d7438b68e7a13d48016868fef4e54d4745b8e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 6 Jul 2023 21:29:36 +0200 Subject: [PATCH 116/608] implement localization --- public/index.html | 112 ++++++---- public/lang/en.json | 136 ++++++++++++ public/scripts/localization.js | 102 +++++++++ public/scripts/network.js | 18 +- public/scripts/ui.js | 194 +++++++++++------- public/styles.css | 34 +-- public_included_ws_fallback/index.html | 116 ++++++----- public_included_ws_fallback/lang/en.json | 136 ++++++++++++ .../scripts/localization.js | 102 +++++++++ .../scripts/network.js | 21 +- public_included_ws_fallback/scripts/ui.js | 194 +++++++++++------- public_included_ws_fallback/styles.css | 4 +- 12 files changed, 883 insertions(+), 286 deletions(-) create mode 100644 public/lang/en.json create mode 100644 public/scripts/localization.js create mode 100644 public_included_ws_fallback/lang/en.json create mode 100644 public_included_ws_fallback/scripts/localization.js diff --git a/public/index.html b/public/index.html index 755673a..15b82da 100644 --- a/public/index.html +++ b/public/index.html @@ -39,62 +39,66 @@
- +
-
+
-
+
-
+
-
- -

Open PairDrop on other devices to send files

-
Pair devices to be discoverable on other networks
+ +

Open PairDrop on other devices to send files

+
Pair devices to be discoverable on other networks
- +

@@ -104,15 +108,21 @@
- You are known as: -
+ You are known as: +
- You can be discovered by everyone on this network - +
+ You can be discovered by everyone + on this network +
+
@@ -120,10 +130,13 @@ -

Pair Devices

+

Pair Devices

000 000

-
Input this key on another device
or scan the QR-Code.
+
+ Input this key on another device + or scan the QR-Code. +

@@ -133,10 +146,10 @@
-
Enter key from another device to continue.
+
Enter key from another device to continue.
- - + +
@@ -147,13 +160,21 @@ -

Edit Paired Devices

-
+

Edit Paired Devices

+
-

Activate auto-accept to automatically accept all files sent from that device.

+

+ + Activate + + auto-accept + + to automatically accept all files sent from that device. + +

- +
@@ -167,7 +188,7 @@
- would like to share + would like to share
@@ -179,8 +200,8 @@
- - + +
@@ -193,7 +214,7 @@
-
The easiest way to transfer files across devices
+
The easiest way to transfer files across devices
@@ -373,6 +394,7 @@ + diff --git a/public/lang/en.json b/public/lang/en.json new file mode 100644 index 0000000..7ae2e56 --- /dev/null +++ b/public/lang/en.json @@ -0,0 +1,136 @@ +{ + "header": { + "about_title": "About PairDrop", + "about_aria-label": "Open About PairDrop", + "theme-auto_title": "Adapt Theme to System", + "theme-light_title": "Always Use Light-Theme", + "theme-dark_title": "Always Use Dark-Theme", + "notification_title": "Enable Notifications", + "install_title": "Install PairDrop", + "pair-device_title": "Pair Device", + "edit-paired-devices_title": "Edit Paired Devices", + "cancel-paste-mode": "Done" + }, + "instructions": { + "no-peers_data-drop-bg": "Release to select recipient", + "no-peers-title": "Open PairDrop on other devices to send files", + "no-peers-subtitle": "Pair devices to be discoverable on other networks", + "x-instructions_desktop": "Click to send files or right click to send a message", + "x-instructions_mobile": "Tap to send files or long tap to send a message", + "x-instructions_data-drop-peer": "Release to send to peer", + "x-instructions_data-drop-bg": "Release to select recipient", + "click-to-send": "Click to send", + "tap-to-send": "Tap to send" + }, + "footer": { + "known-as": "You are known as:", + "display-name_placeholder": "Loading...", + "display-name_title": "Edit your device name permanently", + "discovery-everyone": "You can be discovered by everyone", + "on-this-network": "on this network", + "and-by": "and by", + "paired-devices": "paired devices", + "traffic": "Traffic is", + "routed": "routed through the server", + "webrtc": "if WebRTC is not available." + }, + "dialogs": { + "activate-paste-mode-base": "Open PairDrop on other devices to send", + "activate-paste-mode-and-other-files": "and {{count}} other files", + "activate-paste-mode-activate-paste-mode-shared-text": "shared text", + "pair-devices-title": "Pair Devices", + "input-key-on-this-device": "Input this key on another device", + "scan-qr-code": "or scan the QR-Code.", + "enter-key-from-another-device": "Enter key from another device to continue.", + "pair": "Pair", + "cancel": "Cancel", + "edit-paired-devices-title": "Edit Paired Devices", + "paired-devices-wrapper_data-empty": "No paired devices.", + "auto-accept-instructions-1": "Activate", + "auto-accept": "auto-accept", + "auto-accept-instructions-2": "to automatically accept all files sent from that device.", + "close": "Close", + "would-like-to-share": "would like to share", + "accept": "Accept", + "decline": "Decline", + "has-sent": "has sent:", + "share": "Share", + "download": "Download", + "send-message-title": "Send Message", + "send-message-to": "Send a Message to", + "send": "Send", + "receive-text-title": "Message Received", + "copy": "Copy", + "base64-processing": "Processing...", + "base64-tap-to-paste": "Tap here to paste {{type}}", + "base64-paste-to-send": "Paste here to send {{type}}", + "base64-text": "text", + "base64-files": "files", + "file-other-description-image": "and 1 other image", + "file-other-description-file": "and 1 other file", + "file-other-description-image-plural": "and {{count}} other images", + "file-other-description-file-plural": "and {{count}} other files", + "title-image": "Image", + "title-file": "File", + "title-image-plural": "Images", + "title-file-plural": "Files", + "receive-title": "{{descriptor}} Received", + "download-again": "Download again" + }, + "about": { + "close-about-aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices" + }, + "notifications": { + "display-name-changed-permanently": "Display name is changed permanently.", + "display-name-changed-temporarily": "Display name is changed only for this session.", + "display-name-random-again": "Display name is randomly generated again.", + "download-successful": "{{descriptor}} downloaded successfully", + "pairing-tabs-error": "Pairing of two browser tabs is not possible.", + "pairing-success": "Devices paired successfully.", + "pairing-not-persistent": "Paired devices are not persistent.", + "pairing-key-invalid": "Key not valid", + "pairing-key-invalidated": "Key {{key}} invalidated.", + "pairing-cleared": "All Devices unpaired.", + "copied-to-clipboard": "Copied to clipboard", + "text-content-incorrect": "Text content is incorrect.", + "file-content-incorrect": "File content is incorrect.", + "clipboard-content-incorrect": "Clipboard content is incorrect.", + "notifications-enabled": "Notifications enabled.", + "link-received": "Link received by {{name}} - Click to open", + "message-received": "Message received by {{name}} - Click to copy", + "click-to-download": "Click to download", + "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", + "click-to-show": "Click to show", + "copied-text": "Copied text to clipboard", + "copied-text-error": "Writing to clipboard failed. Copy manually!", + "offline": "You are offline", + "online": "You are back online", + "connected": "Connected.", + "online-requirement": "You need to be online to pair devices.", + "connecting": "Connecting...", + "files-incorrect": "Files are incorrect.", + "file-transfer-completed": "File transfer completed.", + "ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once", + "message-transfer-completed": "Message transfer completed.", + "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", + "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", + "selected-peer-left": "Selected peer left." + }, + "document-titles": { + "file-received": "File Received", + "file-received-plural": "{{count}} Files Received", + "file-transfer-requested": "File Transfer Requested", + "message-received": "Message Received", + "message-received-plural": "{{count}} Messages Received" + }, + "peer-ui": { + "click-to-send-paste-mode": "Click to send {{descriptor}}", + "click-to-send": "Click to send files or right click to send a message", + "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", + "preparing": "Preparing...", + "waiting": "Waiting...", + "processing": "Processing...", + "transferring": "Transferring..." + } +} diff --git a/public/scripts/localization.js b/public/scripts/localization.js new file mode 100644 index 0000000..d09d5c0 --- /dev/null +++ b/public/scripts/localization.js @@ -0,0 +1,102 @@ +class Localization { + constructor() { + Localization.defaultLocale = "en"; + Localization.supportedLocales = ["en"]; + + Localization.translations = {}; + + const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); + + Localization.setLocale(initialLocale) + .then(_ => { + Localization.translatePage(); + }) + } + + static isSupported(locale) { + return Localization.supportedLocales.indexOf(locale) > -1; + } + + static supportedOrDefault(locales) { + return locales.find(Localization.isSupported) || Localization.defaultLocale; + } + + static browserLocales() { + return navigator.languages.map(locale => + locale.split("-")[0] + ); + } + + static async setLocale(newLocale) { + if (newLocale === Localization.locale) return false; + + const newTranslations = await Localization.fetchTranslationsFor(newLocale); + + if(!newTranslations) return false; + + const firstTranslation = !Localization.locale + + Localization.locale = newLocale; + Localization.translations = newTranslations; + + if (firstTranslation) { + Events.fire("translation-loaded"); + } + } + + static async fetchTranslationsFor(newLocale) { + const response = await fetch(`lang/${newLocale}.json`) + + if (response.redirected === true || response.status !== 200) return false; + + return await response.json(); + } + + static translatePage() { + document + .querySelectorAll("[data-i18n-key]") + .forEach(element => Localization.translateElement(element)); + } + + static async translateElement(element) { + const key = element.getAttribute("data-i18n-key"); + const attrs = element.getAttribute("data-i18n-attrs").split(" "); + + for (let i in attrs) { + let attr = attrs[i]; + if (attr === "text") { + element.innerText = await Localization.getTranslation(key); + } else { + element.attr = await Localization.getTranslation(key, attr); + } + } + + } + + static getTranslation(key, attr, data) { + const keys = key.split("."); + + let translationCandidates = Localization.translations; + + for (let i=0; i this._connect(), 1000); Events.fire('ws-disconnected'); @@ -488,7 +488,7 @@ class Peer { _abortTransfer() { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); - Events.fire('notify-user', 'Files are incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect")); this._filesReceived = []; this._requestAccepted = null; this._digester = null; @@ -546,7 +546,7 @@ class Peer { this._chunker = null; if (!this._filesQueue.length) { this._busy = false; - Events.fire('notify-user', 'File transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed")); Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app } else { this._dequeueFile(); @@ -558,7 +558,7 @@ class Peer { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); this._filesRequested = null; if (message.reason === 'ios-memory-limit') { - Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once"); + Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit")); } return; } @@ -568,7 +568,7 @@ class Peer { } _onMessageTransferCompleted() { - Events.fire('notify-user', 'Message transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed")); } sendText(text) { @@ -713,7 +713,7 @@ class RTCPeer extends Peer { _onBeforeUnload(e) { if (this._busy) { e.preventDefault(); - return "There are unfinished transfers. Are you sure you want to close?"; + return Localization.getTranslation("notifications.unfinished-transfers-warning"); } } diff --git a/public/scripts/ui.js b/public/scripts/ui.js index b494f58..f3d08d8 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -89,12 +89,12 @@ class PeersUI { if (newDisplayName) { PersistentStorage.set('editedDisplayName', newDisplayName) .then(_ => { - Events.fire('notify-user', 'Device name is changed permanently.'); + Events.fire('notify-user', Localization.getTranslation("notifications.display-name-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.'); + Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily")); }) .finally(_ => { Events.fire('self-display-name-changed', newDisplayName); @@ -105,10 +105,9 @@ class PeersUI { .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('notify-user', Localization.getTranslation("notifications.display-name-random-again")); Events.fire('self-display-name-changed', ''); Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); }); @@ -275,21 +274,22 @@ class PeersUI { let descriptor; let noPeersMessage; + const openPairDrop = Localization.getTranslation("dialogs.activate-paste-mode-base"); + const andOtherFiles = Localization.getTranslation("dialogs.activate-paste-mode-and-other-files", null, {count: files.length-1}); + const sharedText = Localization.getTranslation("dialogs.activate-paste-mode-shared-text"); + if (files.length === 1) { - descriptor = files[0].name; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name}`; } else if (files.length > 1) { - descriptor = `${files[0].name} and ${files.length-1} other files`; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name} ${andOtherFiles}`; } else { - descriptor = "shared text"; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${sharedText}`; } - this.$xInstructions.querySelector('p').innerHTML = `${descriptor}`; + this.$xInstructions.querySelector('p').innerHTML = noPeersMessage; this.$xInstructions.querySelector('p').style.display = 'block'; - this.$xInstructions.setAttribute('desktop', `Click to send`); - this.$xInstructions.setAttribute('mobile', `Tap to send`); + this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.click-to-send")); + this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.tap-to-send")); this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage; @@ -320,10 +320,10 @@ class PeersUI { this.$xInstructions.querySelector('p').innerText = ''; this.$xInstructions.querySelector('p').style.display = 'none'; - this.$xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message'); - this.$xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message'); + this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.x-instructions", "desktop")); + this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.x-instructions", "mobile")); - this.$xNoPeers.querySelector('h2').innerHTML = 'Open PairDrop on other devices to send files'; + this.$xNoPeers.querySelector('h2').innerHTML = Localization.getTranslation("instructions.no-peers-title"); this.$cancelPasteModeBtn.setAttribute('hidden', ""); @@ -368,9 +368,9 @@ class PeerUI { let title; let input = ''; if (window.pasteMode.activated) { - title = `Click to send ${window.pasteMode.descriptor}`; + title = Localization.getTranslation("peer-ui.click-to-send-paste-mode", null, {descriptor: window.pasteMode.descriptor}); } else { - title = 'Click to send files or right click to send a message'; + title = Localization.getTranslation("peer-ui.click-to-send"); input = ''; } this.$el.innerHTML = ` @@ -392,7 +392,7 @@ class PeerUI {
- +
`; @@ -509,10 +509,23 @@ class PeerUI { $progress.classList.remove('over50'); } if (progress < 1) { - this.$el.setAttribute('status', status); + if (status !== this.currentStatus) { + let statusName = { + "prepare": Localization.getTranslation("peer-ui.preparing"), + "transfer": Localization.getTranslation("peer-ui.transferring"), + "process": Localization.getTranslation("peer-ui.processing"), + "wait": Localization.getTranslation("peer-ui.waiting") + }[status]; + + this.$el.setAttribute('status', status); + this.$el.querySelector('.status').innerText = statusName; + this.currentStatus = status; + } } else { this.$el.removeAttribute('status'); + this.$el.querySelector('.status').innerHTML = ''; progress = 0; + this.currentStatus = null; } const degrees = `rotate(${360 * progress}deg)`; $progress.style.setProperty('--progress', degrees); @@ -595,7 +608,7 @@ class Dialog { _onPeerDisconnected(peerId) { if (this.isShown() && this.correspondingPeerId === peerId) { this.hide(); - Events.fire('notify-user', 'Selected peer left.') + Events.fire('notify-user', Localization.getTranslation("notifications.selected-peer-left")); } } } @@ -629,13 +642,17 @@ class ReceiveDialog extends Dialog { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { if (files.length > 1) { - let fileOtherText = ` and ${files.length - 1} other `; + let fileOther; if (files.length === 2) { - fileOtherText += imagesOnly ? 'image' : 'file'; + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); } else { - fileOtherText += imagesOnly ? 'images' : 'files'; + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); } - this.$fileOther.innerText = fileOtherText; + this.$fileOther.innerText = fileOther; } const fileName = files[0].name; @@ -727,11 +744,15 @@ class ReceiveFileDialog extends ReceiveDialog { let descriptor, url, filenameDownload; if (files.length === 1) { - descriptor = imagesOnly ? 'Image' : 'File'; + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image") + : Localization.getTranslation("dialogs.title-file"); } else { - descriptor = imagesOnly ? 'Images' : 'Files'; + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image-plural") + : Localization.getTranslation("dialogs.title-file-plural"); } - this.$receiveTitle.innerText = `${descriptor} Received`; + this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor}); const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); if (canShare) { @@ -781,7 +802,7 @@ class ReceiveFileDialog extends ReceiveDialog { } } - this.$downloadBtn.innerText = "Download"; + this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download"); this.$downloadBtn.onclick = _ => { if (downloadZipped) { let tmpZipBtn = document.createElement("a"); @@ -793,17 +814,18 @@ class ReceiveFileDialog extends ReceiveDialog { } if (!canShare) { - this.$downloadBtn.innerText = "Download again"; + this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download-again"); } - Events.fire('notify-user', `${descriptor} downloaded successfully`); + Events.fire('notify-user', Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor})); this.$downloadBtn.style.pointerEvents = "none"; setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); }; document.title = files.length === 1 - ? 'File received - PairDrop' - : `${files.length} Files received - PairDrop`; + ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop` + : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); + Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) this.show(); @@ -891,7 +913,7 @@ class ReceiveRequestDialog extends ReceiveDialog { this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` - document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`; + document.title = `${ Localization.getTranslation("document-titles.file-transfer-requested") } - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); this.show(); } @@ -1083,7 +1105,7 @@ class PairDeviceDialog extends Dialog { if (BrowserTabsConnector.peerIsSameBrowser(peerId)) { this._cleanUp(); this.hide(); - Events.fire('notify-user', 'Pairing of two browser tabs is not possible.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-tabs-error")); return; } @@ -1129,7 +1151,7 @@ class PairDeviceDialog extends Dialog { PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName) .then(_ => { - Events.fire('notify-user', 'Devices paired successfully.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-success")); this._evaluateNumberRoomSecrets(); }) .finally(_ => { @@ -1137,13 +1159,13 @@ class PairDeviceDialog extends Dialog { this.hide(); }) .catch(_ => { - Events.fire('notify-user', 'Paired devices are not persistent.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-not-persistent")); PersistentStorage.logBrowserNotCapable(); }); } _pairDeviceJoinKeyInvalid() { - Events.fire('notify-user', 'Key not valid'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalid")); } _pairDeviceCancel() { @@ -1153,7 +1175,7 @@ class PairDeviceDialog extends Dialog { } _pairDeviceCanceled(roomKey) { - Events.fire('notify-user', `Key ${roomKey} invalidated.`); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalidated", null, {key: roomKey})); } _cleanUp() { @@ -1260,7 +1282,7 @@ class EditPairedDevicesDialog extends Dialog { PersistentStorage.clearRoomSecrets().finally(_ => { Events.fire('room-secrets-deleted', roomSecrets); Events.fire('evaluate-number-room-secrets'); - Events.fire('notify-user', 'All Devices unpaired.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-cleared")); this.hide(); }) }); @@ -1415,14 +1437,14 @@ class ReceiveTextDialog extends Dialog { _setDocumentTitleMessages() { document.title = !this._receiveTextQueue.length - ? 'Message Received - PairDrop' - : `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`; + ? `${ Localization.getTranslation("document-titles.message-received") } - PairDrop` + : `${ Localization.getTranslation("document-titles.message-received-plural", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`; } async _onCopy() { const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); await navigator.clipboard.writeText(sanitizedText); - Events.fire('notify-user', 'Copied to clipboard'); + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); this.hide(); } @@ -1449,13 +1471,13 @@ class Base64ZipDialog extends Dialog { if (base64Text === "paste") { // ?base64text=paste // base64 encoded string is ready to be pasted from clipboard - this.preparePasting("text"); + this.preparePasting(Localization.getTranslation("dialogs.base64-text")); } else if (base64Text === "hash") { // ?base64text=hash#BASE64ENCODED // base64 encoded string is url hash which is never sent to server and faster (recommended) this.processBase64Text(base64Hash) .catch(_ => { - Events.fire('notify-user', 'Text content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); console.log("Text content incorrect."); }).finally(_ => { this.hide(); @@ -1465,7 +1487,7 @@ class Base64ZipDialog extends Dialog { // base64 encoded string was part of url param (not recommended) this.processBase64Text(base64Text) .catch(_ => { - Events.fire('notify-user', 'Text content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); console.log("Text content incorrect."); }).finally(_ => { this.hide(); @@ -1478,32 +1500,32 @@ class Base64ZipDialog extends Dialog { // base64 encoded zip file is url hash which is never sent to the server this.processBase64Zip(base64Hash) .catch(_ => { - Events.fire('notify-user', 'File content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect")); console.log("File content incorrect."); }).finally(_ => { this.hide(); }); } else { // ?base64zip=paste || ?base64zip=true - this.preparePasting('files'); + this.preparePasting(Localization.getTranslation("dialogs.base64-files")); } } } _setPasteBtnToProcessing() { this.$pasteBtn.style.pointerEvents = "none"; - this.$pasteBtn.innerText = "Processing..."; + this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing"); } preparePasting(type) { if (navigator.clipboard.readText) { - this.$pasteBtn.innerText = `Tap here to paste ${type}`; + this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", {type: type}); this._clickCallback = _ => this.processClipboard(type); this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); } else { console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") this.$pasteBtn.setAttribute('hidden', ''); - this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); + this.$fallbackTextarea.setAttribute('placeholder', Localization.getTranslation("dialogs.base64-paste-to-send", {type: type})); this.$fallbackTextarea.removeAttribute('hidden'); this._inputCallback = _ => this.processInput(type); this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); @@ -1543,7 +1565,7 @@ class Base64ZipDialog extends Dialog { await this.processBase64Zip(base64); } } catch(_) { - Events.fire('notify-user', 'Clipboard content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect")); console.log("Clipboard content is incorrect.") } this.hide(); @@ -1626,7 +1648,7 @@ class Notifications { Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); return; } - Events.fire('notify-user', 'Notifications enabled.'); + Events.fire('notify-user', Localization.getTranslation("notifications.notifications-enabled")); this.$button.setAttribute('hidden', 1); }); } @@ -1661,10 +1683,10 @@ class Notifications { if (document.visibilityState !== 'visible') { const peerDisplayName = $(peerId).ui._displayName(); if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { - const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); + const notification = this._notify(Localization.getTranslation("notifications.link-received", null, {name: peerDisplayName}), message); this._bind(notification, _ => window.open(message, '_blank', null, true)); } else { - const notification = this._notify(`Message received by ${peerDisplayName} - Click to copy`, message); + const notification = this._notify(Localization.getTranslation("notifications.message-received", null, {name: peerDisplayName}), message); this._bind(notification, _ => this._copyText(message, notification)); } } @@ -1679,13 +1701,23 @@ class Notifications { break; } } - let title = files[0].name; - if (files.length >= 2) { - title += ` and ${files.length - 1} other `; - title += imagesOnly ? 'image' : 'file'; - if (files.length > 2) title += "s"; + let title; + if (files.length === 1) { + title = `${files[0].name}`; + } else { + let fileOther; + if (files.length === 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); + } else { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); + } + title = `${files[0].name} ${fileOther}` } - const notification = this._notify(title, 'Click to download'); + const notification = this._notify(title, Localization.getTranslation("notifications.click-to-download")); this._bind(notification, _ => this._download(notification)); } } @@ -1699,15 +1731,27 @@ class Notifications { break; } } - let descriptor; - if (request.header.length > 1) { - descriptor = imagesOnly ? ' images' : ' files'; - } else { - descriptor = imagesOnly ? ' image' : ' file'; - } + let displayName = $(peerId).querySelector('.name').textContent - let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`; - const notification = this._notify(title, 'Click to show'); + + let descriptor; + if (request.header.length === 1) { + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image") + : Localization.getTranslation("dialogs.title-file"); + } else { + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image-plural") + : Localization.getTranslation("dialogs.title-file-plural"); + } + + let title = Localization.getTranslation("notifications.request-title", null, { + name: displayName, + count: request.header.length, + descriptor: descriptor.toLowerCase() + }); + + const notification = this._notify(title, Localization.getTranslation("notifications.click-to-show")); } } @@ -1719,10 +1763,9 @@ class Notifications { _copyText(message, notification) { if (navigator.clipboard.writeText(message)) { notification.close(); - this._notify('Copied text to clipboard'); + this._notify(Localization.getTranslation("notifications.copied-text")); } else { - this._notify('Writing to clipboard failed. Copy manually!'); - + this._notify(Localization.getTranslation("notifications.copied-text-error")); } } @@ -1746,11 +1789,11 @@ class NetworkStatusUI { } _showOfflineMessage() { - Events.fire('notify-user', 'You are offline'); + Events.fire('notify-user', Localization.getTranslation("notifications.offline")); } _showOnlineMessage() { - Events.fire('notify-user', 'You are back online'); + Events.fire('notify-user', Localization.getTranslation("notifications.online")); } } @@ -2208,7 +2251,7 @@ class BrowserTabsConnector { class PairDrop { constructor() { - Events.on('load', _ => { + Events.on('translation-loaded', _ => { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); @@ -2232,6 +2275,7 @@ class PairDrop { const persistentStorage = new PersistentStorage(); const pairDrop = new PairDrop(); +const localization = new Localization(); if ('serviceWorker' in navigator) { diff --git a/public/styles.css b/public/styles.css index db86b60..1375b46 100644 --- a/public/styles.css +++ b/public/styles.css @@ -442,7 +442,7 @@ x-no-peers::before { } x-no-peers[drop-bg]::before { - content: "Release to select recipient"; + content: attr(data-drop-bg); } x-no-peers[drop-bg] * { @@ -553,22 +553,6 @@ x-peer[status] x-icon { white-space: nowrap; } -x-peer[status=transfer] .status:before { - content: 'Transferring...'; -} - -x-peer[status=prepare] .status:before { - content: 'Preparing...'; -} - -x-peer[status=wait] .status:before { - content: 'Waiting...'; -} - -x-peer[status=process] .status:before { - content: 'Processing...'; -} - x-peer:not([status]) .status, x-peer[status] .device-name { display: none; @@ -626,11 +610,13 @@ footer .font-body2 { #on-this-network { border-bottom: solid 4px var(--primary-color); padding-bottom: 1px; + word-break: keep-all; } #paired-devices { border-bottom: solid 4px var(--paired-device-color); padding-bottom: 1px; + word-break: keep-all; } #display-name { @@ -723,10 +709,6 @@ x-dialog a { color: var(--primary-color); } -x-dialog .font-subheading { - margin-bottom: 5px; -} - /* Pair Devices Dialog */ #key-input-container { @@ -774,6 +756,10 @@ x-dialog .font-subheading { margin: 16px; } +#pair-instructions { + flex-direction: column; +} + x-dialog hr { margin: 40px -24px 30px -24px; border: solid 1.25px var(--border-color); @@ -785,7 +771,7 @@ x-dialog hr { /* Edit Paired Devices Dialog */ .paired-devices-wrapper:empty:before { - content: "No paired devices."; + content: attr(data-empty); } .paired-devices-wrapper:empty { @@ -1288,11 +1274,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before { } x-instructions[drop-peer]:before { - content: "Release to send to peer"; + content: attr(data-drop-peer); } x-instructions[drop-bg]:not([drop-peer]):before { - content: "Release to select recipient"; + content: attr(data-drop-bg); } x-instructions p { diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 6beae65..e42f324 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -39,62 +39,66 @@
- +
-
+
-
+
-
+
-
- -

Open PairDrop on other devices to send files

-
Pair devices to be discoverable on other networks
+ +

Open PairDrop on other devices to send files

+
Pair devices to be discoverable on other networks
- +

@@ -104,18 +108,26 @@
- You are known as: -
+ You are known as: +
- You can be discovered by everyone on this network - +
+ You can be discovered by everyone + on this network +
+
- Traffic is routed through the server if WebRTC is not available. + Traffic is + routed through the server + if WebRTC is not available.
@@ -123,10 +135,13 @@ -

Pair Devices

+

Pair Devices

000 000

-
Input this key on another device
or scan the QR-Code.
+
+ Input this key on another device + or scan the QR-Code. +

@@ -136,10 +151,10 @@
-
Enter key from another device to continue.
+
Enter key from another device to continue.
- - + +
@@ -150,13 +165,21 @@ -

Edit Paired Devices

-
+

Edit Paired Devices

+
-

Activate auto-accept to automatically accept all files sent from that device.

+

+ + Activate + + auto-accept + + to automatically accept all files sent from that device. + +

- +
@@ -170,7 +193,7 @@
- would like to share + would like to share
@@ -182,8 +205,8 @@
- - + +
@@ -196,7 +219,7 @@
- has sent + has sent
@@ -207,9 +230,9 @@
- - - + + +
@@ -219,16 +242,16 @@ -

Send Message

+

Send Message

- Send a Message to + Send a Message to
- - + +
@@ -238,16 +261,16 @@ -

Message Received

+

Message Received

- has sent: + has sent:
- - + +
@@ -256,9 +279,9 @@ - + - + @@ -269,7 +292,7 @@
- + @@ -283,7 +306,7 @@

PairDrop

v1.7.6
-
The easiest way to transfer files across devices
+
The easiest way to transfer files across devices
@@ -376,6 +399,7 @@ + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json new file mode 100644 index 0000000..7ae2e56 --- /dev/null +++ b/public_included_ws_fallback/lang/en.json @@ -0,0 +1,136 @@ +{ + "header": { + "about_title": "About PairDrop", + "about_aria-label": "Open About PairDrop", + "theme-auto_title": "Adapt Theme to System", + "theme-light_title": "Always Use Light-Theme", + "theme-dark_title": "Always Use Dark-Theme", + "notification_title": "Enable Notifications", + "install_title": "Install PairDrop", + "pair-device_title": "Pair Device", + "edit-paired-devices_title": "Edit Paired Devices", + "cancel-paste-mode": "Done" + }, + "instructions": { + "no-peers_data-drop-bg": "Release to select recipient", + "no-peers-title": "Open PairDrop on other devices to send files", + "no-peers-subtitle": "Pair devices to be discoverable on other networks", + "x-instructions_desktop": "Click to send files or right click to send a message", + "x-instructions_mobile": "Tap to send files or long tap to send a message", + "x-instructions_data-drop-peer": "Release to send to peer", + "x-instructions_data-drop-bg": "Release to select recipient", + "click-to-send": "Click to send", + "tap-to-send": "Tap to send" + }, + "footer": { + "known-as": "You are known as:", + "display-name_placeholder": "Loading...", + "display-name_title": "Edit your device name permanently", + "discovery-everyone": "You can be discovered by everyone", + "on-this-network": "on this network", + "and-by": "and by", + "paired-devices": "paired devices", + "traffic": "Traffic is", + "routed": "routed through the server", + "webrtc": "if WebRTC is not available." + }, + "dialogs": { + "activate-paste-mode-base": "Open PairDrop on other devices to send", + "activate-paste-mode-and-other-files": "and {{count}} other files", + "activate-paste-mode-activate-paste-mode-shared-text": "shared text", + "pair-devices-title": "Pair Devices", + "input-key-on-this-device": "Input this key on another device", + "scan-qr-code": "or scan the QR-Code.", + "enter-key-from-another-device": "Enter key from another device to continue.", + "pair": "Pair", + "cancel": "Cancel", + "edit-paired-devices-title": "Edit Paired Devices", + "paired-devices-wrapper_data-empty": "No paired devices.", + "auto-accept-instructions-1": "Activate", + "auto-accept": "auto-accept", + "auto-accept-instructions-2": "to automatically accept all files sent from that device.", + "close": "Close", + "would-like-to-share": "would like to share", + "accept": "Accept", + "decline": "Decline", + "has-sent": "has sent:", + "share": "Share", + "download": "Download", + "send-message-title": "Send Message", + "send-message-to": "Send a Message to", + "send": "Send", + "receive-text-title": "Message Received", + "copy": "Copy", + "base64-processing": "Processing...", + "base64-tap-to-paste": "Tap here to paste {{type}}", + "base64-paste-to-send": "Paste here to send {{type}}", + "base64-text": "text", + "base64-files": "files", + "file-other-description-image": "and 1 other image", + "file-other-description-file": "and 1 other file", + "file-other-description-image-plural": "and {{count}} other images", + "file-other-description-file-plural": "and {{count}} other files", + "title-image": "Image", + "title-file": "File", + "title-image-plural": "Images", + "title-file-plural": "Files", + "receive-title": "{{descriptor}} Received", + "download-again": "Download again" + }, + "about": { + "close-about-aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices" + }, + "notifications": { + "display-name-changed-permanently": "Display name is changed permanently.", + "display-name-changed-temporarily": "Display name is changed only for this session.", + "display-name-random-again": "Display name is randomly generated again.", + "download-successful": "{{descriptor}} downloaded successfully", + "pairing-tabs-error": "Pairing of two browser tabs is not possible.", + "pairing-success": "Devices paired successfully.", + "pairing-not-persistent": "Paired devices are not persistent.", + "pairing-key-invalid": "Key not valid", + "pairing-key-invalidated": "Key {{key}} invalidated.", + "pairing-cleared": "All Devices unpaired.", + "copied-to-clipboard": "Copied to clipboard", + "text-content-incorrect": "Text content is incorrect.", + "file-content-incorrect": "File content is incorrect.", + "clipboard-content-incorrect": "Clipboard content is incorrect.", + "notifications-enabled": "Notifications enabled.", + "link-received": "Link received by {{name}} - Click to open", + "message-received": "Message received by {{name}} - Click to copy", + "click-to-download": "Click to download", + "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", + "click-to-show": "Click to show", + "copied-text": "Copied text to clipboard", + "copied-text-error": "Writing to clipboard failed. Copy manually!", + "offline": "You are offline", + "online": "You are back online", + "connected": "Connected.", + "online-requirement": "You need to be online to pair devices.", + "connecting": "Connecting...", + "files-incorrect": "Files are incorrect.", + "file-transfer-completed": "File transfer completed.", + "ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once", + "message-transfer-completed": "Message transfer completed.", + "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", + "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", + "selected-peer-left": "Selected peer left." + }, + "document-titles": { + "file-received": "File Received", + "file-received-plural": "{{count}} Files Received", + "file-transfer-requested": "File Transfer Requested", + "message-received": "Message Received", + "message-received-plural": "{{count}} Messages Received" + }, + "peer-ui": { + "click-to-send-paste-mode": "Click to send {{descriptor}}", + "click-to-send": "Click to send files or right click to send a message", + "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", + "preparing": "Preparing...", + "waiting": "Waiting...", + "processing": "Processing...", + "transferring": "Transferring..." + } +} diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js new file mode 100644 index 0000000..d09d5c0 --- /dev/null +++ b/public_included_ws_fallback/scripts/localization.js @@ -0,0 +1,102 @@ +class Localization { + constructor() { + Localization.defaultLocale = "en"; + Localization.supportedLocales = ["en"]; + + Localization.translations = {}; + + const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); + + Localization.setLocale(initialLocale) + .then(_ => { + Localization.translatePage(); + }) + } + + static isSupported(locale) { + return Localization.supportedLocales.indexOf(locale) > -1; + } + + static supportedOrDefault(locales) { + return locales.find(Localization.isSupported) || Localization.defaultLocale; + } + + static browserLocales() { + return navigator.languages.map(locale => + locale.split("-")[0] + ); + } + + static async setLocale(newLocale) { + if (newLocale === Localization.locale) return false; + + const newTranslations = await Localization.fetchTranslationsFor(newLocale); + + if(!newTranslations) return false; + + const firstTranslation = !Localization.locale + + Localization.locale = newLocale; + Localization.translations = newTranslations; + + if (firstTranslation) { + Events.fire("translation-loaded"); + } + } + + static async fetchTranslationsFor(newLocale) { + const response = await fetch(`lang/${newLocale}.json`) + + if (response.redirected === true || response.status !== 200) return false; + + return await response.json(); + } + + static translatePage() { + document + .querySelectorAll("[data-i18n-key]") + .forEach(element => Localization.translateElement(element)); + } + + static async translateElement(element) { + const key = element.getAttribute("data-i18n-key"); + const attrs = element.getAttribute("data-i18n-attrs").split(" "); + + for (let i in attrs) { + let attr = attrs[i]; + if (attr === "text") { + element.innerText = await Localization.getTranslation(key); + } else { + element.attr = await Localization.getTranslation(key, attr); + } + } + + } + + static getTranslation(key, attr, data) { + const keys = key.split("."); + + let translationCandidates = Localization.translations; + + for (let i=0; i this._connect(), 1000); Events.fire('ws-disconnected'); @@ -505,7 +505,7 @@ class Peer { _abortTransfer() { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); - Events.fire('notify-user', 'Files are incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.files-incorrect")); this._filesReceived = []; this._requestAccepted = null; this._digester = null; @@ -546,7 +546,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); @@ -563,7 +563,8 @@ class Peer { this._chunker = null; if (!this._filesQueue.length) { this._busy = false; - Events.fire('notify-user', 'File transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed")); + Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app } else { this._dequeueFile(); } @@ -574,7 +575,7 @@ class Peer { Events.fire('set-progress', {peerId: this._peerId, progress: 1, status: 'wait'}); this._filesRequested = null; if (message.reason === 'ios-memory-limit') { - Events.fire('notify-user', "Sending files to iOS is only possible up to 200MB at once"); + Events.fire('notify-user', Localization.getTranslation("notifications.ios-memory-limit")); } return; } @@ -584,7 +585,7 @@ class Peer { } _onMessageTransferCompleted() { - Events.fire('notify-user', 'Message transfer completed.'); + Events.fire('notify-user', Localization.getTranslation("notifications.message-transfer-completed")); } sendText(text) { @@ -729,7 +730,7 @@ class RTCPeer extends Peer { _onBeforeUnload(e) { if (this._busy) { e.preventDefault(); - return "There are unfinished transfers. Are you sure you want to close?"; + return Localization.getTranslation("notifications.unfinished-transfers-warning"); } } diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index d355468..b3afac4 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -89,12 +89,12 @@ class PeersUI { if (newDisplayName) { PersistentStorage.set('editedDisplayName', newDisplayName) .then(_ => { - Events.fire('notify-user', 'Device name is changed permanently.'); + Events.fire('notify-user', Localization.getTranslation("notifications.display-name-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.'); + Events.fire('notify-user', Localization.getTranslation("notifications.display-name-changed-temporarily")); }) .finally(_ => { Events.fire('self-display-name-changed', newDisplayName); @@ -105,10 +105,9 @@ class PeersUI { .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('notify-user', Localization.getTranslation("notifications.display-name-random-again")); Events.fire('self-display-name-changed', ''); Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''}); }); @@ -275,21 +274,22 @@ class PeersUI { let descriptor; let noPeersMessage; + const openPairDrop = Localization.getTranslation("dialogs.activate-paste-mode-base"); + const andOtherFiles = Localization.getTranslation("dialogs.activate-paste-mode-and-other-files", null, {count: files.length-1}); + const sharedText = Localization.getTranslation("dialogs.activate-paste-mode-shared-text"); + if (files.length === 1) { - descriptor = files[0].name; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name}`; } else if (files.length > 1) { - descriptor = `${files[0].name} and ${files.length-1} other files`; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${files[0].name} ${andOtherFiles}`; } else { - descriptor = "shared text"; - noPeersMessage = `Open PairDrop on other devices to send
${descriptor}`; + noPeersMessage = `${openPairDrop}
${sharedText}`; } - this.$xInstructions.querySelector('p').innerHTML = `${descriptor}`; + this.$xInstructions.querySelector('p').innerHTML = noPeersMessage; this.$xInstructions.querySelector('p').style.display = 'block'; - this.$xInstructions.setAttribute('desktop', `Click to send`); - this.$xInstructions.setAttribute('mobile', `Tap to send`); + this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.click-to-send")); + this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.tap-to-send")); this.$xNoPeers.querySelector('h2').innerHTML = noPeersMessage; @@ -320,10 +320,10 @@ class PeersUI { this.$xInstructions.querySelector('p').innerText = ''; this.$xInstructions.querySelector('p').style.display = 'none'; - this.$xInstructions.setAttribute('desktop', 'Click to send files or right click to send a message'); - this.$xInstructions.setAttribute('mobile', 'Tap to send files or long tap to send a message'); + this.$xInstructions.setAttribute('desktop', Localization.getTranslation("instructions.x-instructions", "desktop")); + this.$xInstructions.setAttribute('mobile', Localization.getTranslation("instructions.x-instructions", "mobile")); - this.$xNoPeers.querySelector('h2').innerHTML = 'Open PairDrop on other devices to send files'; + this.$xNoPeers.querySelector('h2').innerHTML = Localization.getTranslation("instructions.no-peers-title"); this.$cancelPasteModeBtn.setAttribute('hidden', ""); @@ -368,9 +368,9 @@ class PeerUI { let title; let input = ''; if (window.pasteMode.activated) { - title = `Click to send ${window.pasteMode.descriptor}`; + title = Localization.getTranslation("peer-ui.click-to-send-paste-mode", null, {descriptor: window.pasteMode.descriptor}); } else { - title = 'Click to send files or right click to send a message'; + title = Localization.getTranslation("peer-ui.click-to-send"); input = ''; } this.$el.innerHTML = ` @@ -392,7 +392,7 @@ class PeerUI {
- +
`; @@ -510,10 +510,23 @@ class PeerUI { $progress.classList.remove('over50'); } if (progress < 1) { - this.$el.setAttribute('status', status); + if (status !== this.currentStatus) { + let statusName = { + "prepare": Localization.getTranslation("peer-ui.preparing"), + "transfer": Localization.getTranslation("peer-ui.transferring"), + "process": Localization.getTranslation("peer-ui.processing"), + "wait": Localization.getTranslation("peer-ui.waiting") + }[status]; + + this.$el.setAttribute('status', status); + this.$el.querySelector('.status').innerText = statusName; + this.currentStatus = status; + } } else { this.$el.removeAttribute('status'); + this.$el.querySelector('.status').innerHTML = ''; progress = 0; + this.currentStatus = null; } const degrees = `rotate(${360 * progress}deg)`; $progress.style.setProperty('--progress', degrees); @@ -596,7 +609,7 @@ class Dialog { _onPeerDisconnected(peerId) { if (this.isShown() && this.correspondingPeerId === peerId) { this.hide(); - Events.fire('notify-user', 'Selected peer left.') + Events.fire('notify-user', Localization.getTranslation("notifications.selected-peer-left")); } } } @@ -630,13 +643,17 @@ class ReceiveDialog extends Dialog { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { if (files.length > 1) { - let fileOtherText = ` and ${files.length - 1} other `; + let fileOther; if (files.length === 2) { - fileOtherText += imagesOnly ? 'image' : 'file'; + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); } else { - fileOtherText += imagesOnly ? 'images' : 'files'; + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); } - this.$fileOther.innerText = fileOtherText; + this.$fileOther.innerText = fileOther; } const fileName = files[0].name; @@ -728,11 +745,15 @@ class ReceiveFileDialog extends ReceiveDialog { let descriptor, url, filenameDownload; if (files.length === 1) { - descriptor = imagesOnly ? 'Image' : 'File'; + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image") + : Localization.getTranslation("dialogs.title-file"); } else { - descriptor = imagesOnly ? 'Images' : 'Files'; + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image-plural") + : Localization.getTranslation("dialogs.title-file-plural"); } - this.$receiveTitle.innerText = `${descriptor} Received`; + this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor}); const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); if (canShare) { @@ -782,7 +803,7 @@ class ReceiveFileDialog extends ReceiveDialog { } } - this.$downloadBtn.innerText = "Download"; + this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download"); this.$downloadBtn.onclick = _ => { if (downloadZipped) { let tmpZipBtn = document.createElement("a"); @@ -794,17 +815,18 @@ class ReceiveFileDialog extends ReceiveDialog { } if (!canShare) { - this.$downloadBtn.innerText = "Download again"; + this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download-again"); } - Events.fire('notify-user', `${descriptor} downloaded successfully`); + Events.fire('notify-user', Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor})); this.$downloadBtn.style.pointerEvents = "none"; setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); }; document.title = files.length === 1 - ? 'File received - PairDrop' - : `${files.length} Files received - PairDrop`; + ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop` + : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); + Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) this.show(); @@ -892,7 +914,7 @@ class ReceiveRequestDialog extends ReceiveDialog { this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` - document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`; + document.title = `${ Localization.getTranslation("document-titles.file-transfer-requested") } - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); this.show(); } @@ -1084,7 +1106,7 @@ class PairDeviceDialog extends Dialog { if (BrowserTabsConnector.peerIsSameBrowser(peerId)) { this._cleanUp(); this.hide(); - Events.fire('notify-user', 'Pairing of two browser tabs is not possible.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-tabs-error")); return; } @@ -1130,7 +1152,7 @@ class PairDeviceDialog extends Dialog { PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName) .then(_ => { - Events.fire('notify-user', 'Devices paired successfully.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-success")); this._evaluateNumberRoomSecrets(); }) .finally(_ => { @@ -1138,13 +1160,13 @@ class PairDeviceDialog extends Dialog { this.hide(); }) .catch(_ => { - Events.fire('notify-user', 'Paired devices are not persistent.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-not-persistent")); PersistentStorage.logBrowserNotCapable(); }); } _pairDeviceJoinKeyInvalid() { - Events.fire('notify-user', 'Key not valid'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalid")); } _pairDeviceCancel() { @@ -1154,7 +1176,7 @@ class PairDeviceDialog extends Dialog { } _pairDeviceCanceled(roomKey) { - Events.fire('notify-user', `Key ${roomKey} invalidated.`); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalidated", null, {key: roomKey})); } _cleanUp() { @@ -1261,7 +1283,7 @@ class EditPairedDevicesDialog extends Dialog { PersistentStorage.clearRoomSecrets().finally(_ => { Events.fire('room-secrets-deleted', roomSecrets); Events.fire('evaluate-number-room-secrets'); - Events.fire('notify-user', 'All Devices unpaired.'); + Events.fire('notify-user', Localization.getTranslation("notifications.pairing-cleared")); this.hide(); }) }); @@ -1416,14 +1438,14 @@ class ReceiveTextDialog extends Dialog { _setDocumentTitleMessages() { document.title = !this._receiveTextQueue.length - ? 'Message Received - PairDrop' - : `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`; + ? `${ Localization.getTranslation("document-titles.message-received") } - PairDrop` + : `${ Localization.getTranslation("document-titles.message-received-plural", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`; } async _onCopy() { const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); await navigator.clipboard.writeText(sanitizedText); - Events.fire('notify-user', 'Copied to clipboard'); + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); this.hide(); } @@ -1450,13 +1472,13 @@ class Base64ZipDialog extends Dialog { if (base64Text === "paste") { // ?base64text=paste // base64 encoded string is ready to be pasted from clipboard - this.preparePasting("text"); + this.preparePasting(Localization.getTranslation("dialogs.base64-text")); } else if (base64Text === "hash") { // ?base64text=hash#BASE64ENCODED // base64 encoded string is url hash which is never sent to server and faster (recommended) this.processBase64Text(base64Hash) .catch(_ => { - Events.fire('notify-user', 'Text content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); console.log("Text content incorrect."); }).finally(_ => { this.hide(); @@ -1466,7 +1488,7 @@ class Base64ZipDialog extends Dialog { // base64 encoded string was part of url param (not recommended) this.processBase64Text(base64Text) .catch(_ => { - Events.fire('notify-user', 'Text content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect")); console.log("Text content incorrect."); }).finally(_ => { this.hide(); @@ -1479,32 +1501,32 @@ class Base64ZipDialog extends Dialog { // base64 encoded zip file is url hash which is never sent to the server this.processBase64Zip(base64Hash) .catch(_ => { - Events.fire('notify-user', 'File content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect")); console.log("File content incorrect."); }).finally(_ => { this.hide(); }); } else { // ?base64zip=paste || ?base64zip=true - this.preparePasting('files'); + this.preparePasting(Localization.getTranslation("dialogs.base64-files")); } } } _setPasteBtnToProcessing() { this.$pasteBtn.style.pointerEvents = "none"; - this.$pasteBtn.innerText = "Processing..."; + this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing"); } preparePasting(type) { if (navigator.clipboard.readText) { - this.$pasteBtn.innerText = `Tap here to paste ${type}`; + this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", {type: type}); this._clickCallback = _ => this.processClipboard(type); this.$pasteBtn.addEventListener('click', _ => this._clickCallback()); } else { console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.") this.$pasteBtn.setAttribute('hidden', ''); - this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`); + this.$fallbackTextarea.setAttribute('placeholder', Localization.getTranslation("dialogs.base64-paste-to-send", {type: type})); this.$fallbackTextarea.removeAttribute('hidden'); this._inputCallback = _ => this.processInput(type); this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback()); @@ -1544,7 +1566,7 @@ class Base64ZipDialog extends Dialog { await this.processBase64Zip(base64); } } catch(_) { - Events.fire('notify-user', 'Clipboard content is incorrect.'); + Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect")); console.log("Clipboard content is incorrect.") } this.hide(); @@ -1627,7 +1649,7 @@ class Notifications { Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error'); return; } - Events.fire('notify-user', 'Notifications enabled.'); + Events.fire('notify-user', Localization.getTranslation("notifications.notifications-enabled")); this.$button.setAttribute('hidden', 1); }); } @@ -1662,10 +1684,10 @@ class Notifications { if (document.visibilityState !== 'visible') { const peerDisplayName = $(peerId).ui._displayName(); if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { - const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); + const notification = this._notify(Localization.getTranslation("notifications.link-received", null, {name: peerDisplayName}), message); this._bind(notification, _ => window.open(message, '_blank', null, true)); } else { - const notification = this._notify(`Message received by ${peerDisplayName} - Click to copy`, message); + const notification = this._notify(Localization.getTranslation("notifications.message-received", null, {name: peerDisplayName}), message); this._bind(notification, _ => this._copyText(message, notification)); } } @@ -1680,13 +1702,23 @@ class Notifications { break; } } - let title = files[0].name; - if (files.length >= 2) { - title += ` and ${files.length - 1} other `; - title += imagesOnly ? 'image' : 'file'; - if (files.length > 2) title += "s"; + let title; + if (files.length === 1) { + title = `${files[0].name}`; + } else { + let fileOther; + if (files.length === 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); + } else { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); + } + title = `${files[0].name} ${fileOther}` } - const notification = this._notify(title, 'Click to download'); + const notification = this._notify(title, Localization.getTranslation("notifications.click-to-download")); this._bind(notification, _ => this._download(notification)); } } @@ -1700,15 +1732,27 @@ class Notifications { break; } } - let descriptor; - if (request.header.length > 1) { - descriptor = imagesOnly ? ' images' : ' files'; - } else { - descriptor = imagesOnly ? ' image' : ' file'; - } + let displayName = $(peerId).querySelector('.name').textContent - let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`; - const notification = this._notify(title, 'Click to show'); + + let descriptor; + if (request.header.length === 1) { + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image") + : Localization.getTranslation("dialogs.title-file"); + } else { + descriptor = imagesOnly + ? Localization.getTranslation("dialogs.title-image-plural") + : Localization.getTranslation("dialogs.title-file-plural"); + } + + let title = Localization.getTranslation("notifications.request-title", null, { + name: displayName, + count: request.header.length, + descriptor: descriptor.toLowerCase() + }); + + const notification = this._notify(title, Localization.getTranslation("notifications.click-to-show")); } } @@ -1720,10 +1764,9 @@ class Notifications { _copyText(message, notification) { if (navigator.clipboard.writeText(message)) { notification.close(); - this._notify('Copied text to clipboard'); + this._notify(Localization.getTranslation("notifications.copied-text")); } else { - this._notify('Writing to clipboard failed. Copy manually!'); - + this._notify(Localization.getTranslation("notifications.copied-text-error")); } } @@ -1747,11 +1790,11 @@ class NetworkStatusUI { } _showOfflineMessage() { - Events.fire('notify-user', 'You are offline'); + Events.fire('notify-user', Localization.getTranslation("notifications.offline")); } _showOnlineMessage() { - Events.fire('notify-user', 'You are back online'); + Events.fire('notify-user', Localization.getTranslation("notifications.online")); } } @@ -2209,7 +2252,7 @@ class BrowserTabsConnector { class PairDrop { constructor() { - Events.on('load', _ => { + Events.on('translation-loaded', _ => { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); @@ -2233,6 +2276,7 @@ class PairDrop { const persistentStorage = new PersistentStorage(); const pairDrop = new PairDrop(); +const localization = new Localization(); if ('serviceWorker' in navigator) { diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index e384b51..2e8fbb8 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -1345,11 +1345,11 @@ x-peers:empty~x-instructions { transition: opacity 300ms; } -#websocket-fallback > span { +#websocket-fallback { margin: 2px; } -#websocket-fallback > span > span { +#websocket-fallback > span:nth-child(2) { border-bottom: solid 4px var(--ws-peer-color); } From ba46befde4d3e3657254690cfa6d7a3cc307614a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 7 Jul 2023 14:58:15 +0200 Subject: [PATCH 117/608] include translations for about buttons and implement translation fallback if used translation is not complete --- public/index.html | 14 ++++----- public/lang/en.json | 8 +++-- public/scripts/localization.js | 30 +++++++++++++------ public_included_ws_fallback/index.html | 14 ++++----- public_included_ws_fallback/lang/en.json | 8 +++-- .../scripts/localization.js | 30 +++++++++++++------ 6 files changed, 68 insertions(+), 36 deletions(-) diff --git a/public/index.html b/public/index.html index 15b82da..21f6005 100644 --- a/public/index.html +++ b/public/index.html @@ -83,7 +83,7 @@
- +
@@ -161,7 +161,7 @@

Edit Paired Devices

-
+
The easiest way to transfer files across devices
- + - + - + - + diff --git a/public/lang/en.json b/public/lang/en.json index 7ae2e56..8ad7b7c 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -78,8 +78,12 @@ "download-again": "Download again" }, "about": { - "close-about-aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices" + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on Github", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" }, "notifications": { "display-name-changed-permanently": "Display name is changed permanently.", diff --git a/public/scripts/localization.js b/public/scripts/localization.js index d09d5c0..c7d9716 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -2,8 +2,8 @@ class Localization { constructor() { Localization.defaultLocale = "en"; Localization.supportedLocales = ["en"]; - Localization.translations = {}; + Localization.defaultTranslations = {}; const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); @@ -29,13 +29,13 @@ class Localization { static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; + const firstTranslation = !Localization.locale + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale); if(!newTranslations) return false; - const firstTranslation = !Localization.locale - Localization.locale = newLocale; Localization.translations = newTranslations; @@ -65,18 +65,20 @@ class Localization { for (let i in attrs) { let attr = attrs[i]; if (attr === "text") { - element.innerText = await Localization.getTranslation(key); + element.innerText = Localization.getTranslation(key); } else { - element.attr = await Localization.getTranslation(key, attr); + element.attr = Localization.getTranslation(key, attr); } } } - static getTranslation(key, attr, data) { + static getTranslation(key, attr, data, useDefault=false) { const keys = key.split("."); - let translationCandidates = Localization.translations; + let translationCandidates = useDefault + ? Localization.defaultTranslations + : Localization.translations; for (let i=0; i
- +
@@ -166,7 +166,7 @@

Edit Paired Devices

-
+
The easiest way to transfer files across devices
- + - + - + - + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index 7ae2e56..8ad7b7c 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -78,8 +78,12 @@ "download-again": "Download again" }, "about": { - "close-about-aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices" + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on Github", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" }, "notifications": { "display-name-changed-permanently": "Display name is changed permanently.", diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index d09d5c0..c7d9716 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -2,8 +2,8 @@ class Localization { constructor() { Localization.defaultLocale = "en"; Localization.supportedLocales = ["en"]; - Localization.translations = {}; + Localization.defaultTranslations = {}; const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); @@ -29,13 +29,13 @@ class Localization { static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; + const firstTranslation = !Localization.locale + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale); if(!newTranslations) return false; - const firstTranslation = !Localization.locale - Localization.locale = newLocale; Localization.translations = newTranslations; @@ -65,18 +65,20 @@ class Localization { for (let i in attrs) { let attr = attrs[i]; if (attr === "text") { - element.innerText = await Localization.getTranslation(key); + element.innerText = Localization.getTranslation(key); } else { - element.attr = await Localization.getTranslation(key, attr); + element.attr = Localization.getTranslation(key, attr); } } } - static getTranslation(key, attr, data) { + static getTranslation(key, attr, data, useDefault=false) { const keys = key.split("."); - let translationCandidates = Localization.translations; + let translationCandidates = useDefault + ? Localization.defaultTranslations + : Localization.translations; for (let i=0; i Date: Fri, 7 Jul 2023 00:06:38 +0200 Subject: [PATCH 118/608] Added translation using Weblate (German) --- public/lang/de.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/de.json diff --git a/public/lang/de.json b/public/lang/de.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/de.json @@ -0,0 +1 @@ +{} From 1d333c850c37e6df5b97764b432d6078ce3380e4 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 7 Jul 2023 00:07:04 +0200 Subject: [PATCH 119/608] Added translation using Weblate (Russian) --- public/lang/ru.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/ru.json diff --git a/public/lang/ru.json b/public/lang/ru.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/ru.json @@ -0,0 +1 @@ +{} From 410936dcd888aaa4c140f71b9faf499e7720561c Mon Sep 17 00:00:00 2001 From: kek Date: Fri, 7 Jul 2023 00:04:00 +0000 Subject: [PATCH 120/608] Translated using Weblate (Russian) Currently translated at 100.0% (118 of 118 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/ --- public/lang/ru.json | 137 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/public/lang/ru.json b/public/lang/ru.json index 0967ef4..bcd0103 100644 --- a/public/lang/ru.json +++ b/public/lang/ru.json @@ -1 +1,136 @@ -{} +{ + "header": { + "about_aria-label": "Открыть страницу \"О сервисе\"", + "pair-device_title": "Подключить устройство", + "install_title": "Установить PairDrop", + "cancel-paste-mode": "Выполнено", + "edit-paired-devices_title": "Редактировать сопряженные устройства", + "notification_title": "Включить уведомления", + "about_title": "О сервисе", + "theme-auto_title": "Адаптировать тему к системной", + "theme-dark_title": "Всегда использовать темную тему", + "theme-light_title": "Всегда использовать светлую тему" + }, + "instructions": { + "x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение", + "no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя", + "click-to-send": "Нажмите, чтобы отправить", + "x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя", + "tap-to-send": "Нажмите, чтобы отправить", + "x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу", + "x-instructions_mobile": "Нажмите, чтобы отправить файлы, или долго нажмите, чтобы отправить сообщение", + "no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы", + "no-peers-subtitle": "Сопрягите устройства из разных сетей." + }, + "footer": { + "discovery-everyone": "О вас может узнать любой", + "display-name_placeholder": "Загрузка...", + "routed": "направляется через сервер", + "webrtc": "есть WebRTC недоступен.", + "traffic": "Трафик:", + "and-by": "и от", + "paired-devices": "сопряженные устройства", + "known-as": "Вы известны под именем:", + "on-this-network": "в этой сети", + "display-name_title": "Изменить имя вашего устройства навсегда" + }, + "dialogs": { + "activate-paste-mode-and-other-files": "и {{count}} других файлов", + "activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить", + "activate-paste-mode-activate-paste-mode-shared-text": "общий текст", + "edit-paired-devices-title": "Редактировать сопряженные устройства", + "auto-accept": "автоприем", + "close": "Закрыть", + "decline": "Отклонить", + "share": "Поделиться", + "would-like-to-share": "хотел бы поделиться", + "has-sent": "отправил:", + "paired-devices-wrapper_data-empty": "Нет сопряженных устройств.", + "download": "Скачать", + "receive-text-title": "Сообщение получено", + "send": "Отправить", + "send-message-to": "Отправить сообщение", + "send-message-title": "Отправить сообщение", + "copy": "Копировать", + "base64-files": "файлы", + "base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}", + "base64-processing": "Обработка...", + "base64-tap-to-paste": "Нажмите здесь, чтобы вставить {{type}}", + "base64-text": "текст", + "title-file": "Файл", + "title-file-plural": "Файлы", + "title-image": "Изображение", + "title-image-plural": "Изображения", + "download-again": "Скачать снова", + "auto-accept-instructions-2": "чтобы автоматически принимать все файлы, отправленные с этого устройства.", + "enter-key-from-another-device": "Для продолжения введите ключ с другого устройства.", + "pair-devices-title": "Сопрягите устройства", + "input-key-on-this-device": "Введите этот ключ на другом устройстве", + "scan-qr-code": "или отсканируйте QR-код.", + "cancel": "Отменить", + "pair": "Подключить", + "accept": "Принять", + "auto-accept-instructions-1": "Активировать", + "file-other-description-file": "и 1 другой файл", + "file-other-description-image-plural": "и {{count}} других изображений", + "file-other-description-image": "и 1 другое изображение", + "file-other-description-file-plural": "и {{count}} других файлов", + "receive-title": "{{descriptor}} получен" + }, + "about": { + "close-about-aria-label": "Закрыть страницу \"О сервисе\"", + "claim": "Самый простой способ передачи файлов между устройствами" + }, + "notifications": { + "display-name-changed-permanently": "Отображаемое имя изменено навсегда.", + "display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.", + "pairing-success": "Устройства сопряжены успешно.", + "pairing-tabs-error": "Сопряжение двух вкладок браузера невозможно.", + "copied-to-clipboard": "Скопировано в буфер обмена", + "pairing-not-persistent": "Сопряженные устройства непостоянны.", + "link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть", + "notifications-enabled": "Уведомления включены.", + "text-content-incorrect": "Содержание текста неверно.", + "message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать", + "connected": "Подключено.", + "copied-text": "Текст скопирован в буфер обмена", + "online": "Вы снова в сети", + "offline": "Вы находитесь вне сети", + "online-requirement": "Для сопряжения устройств вам нужно быть в сети.", + "files-incorrect": "Файлы неверны.", + "message-transfer-completed": "Передача сообщения завершена.", + "ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз", + "selected-peer-left": "Выбранный узел вышел.", + "request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}", + "rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.", + "unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть?", + "copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!", + "pairing-cleared": "Все устройства не сопряжены.", + "pairing-key-invalid": "Ключ недействителен", + "pairing-key-invalidated": "Ключ {{key}} признан недействительным.", + "click-to-download": "Нажмите, чтобы скачать", + "clipboard-content-incorrect": "Содержание буфера обмена неверно.", + "click-to-show": "Нажмите, чтобы показать", + "connecting": "Подключение...", + "download-successful": "{{descriptor}} успешно загружен", + "display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.", + "file-content-incorrect": "Содержимое файла неверно.", + "file-transfer-completed": "Передача файла завершена." + }, + "peer-ui": { + "click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}", + "preparing": "Подготовка...", + "transferring": "Передача...", + "processing": "Обработка...", + "waiting": "Ожидание...", + "connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах", + "click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение" + }, + "document-titles": { + "file-received-plural": "{{count}} файлов получено", + "message-received-plural": "{{count}} сообщений получено", + "file-received": "Файл получен", + "file-transfer-requested": "Запрошена передача файлов", + "message-received": "Сообщение получено" + } +} From 525fd295b7e264ed332c12bb9a06ca39595fa1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 7 Jul 2023 16:08:09 +0200 Subject: [PATCH 121/608] =?UTF-8?q?Added=20translation=20using=20Weblate?= =?UTF-8?q?=20(Norwegian=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/lang/nb-NO.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/nb-NO.json diff --git a/public/lang/nb-NO.json b/public/lang/nb-NO.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/nb-NO.json @@ -0,0 +1 @@ +{} From 99faa6bbfd9f7982a0abd9bac1440b5d7761fc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 7 Jul 2023 14:25:46 +0000 Subject: [PATCH 122/608] Translated using Weblate (English) Currently translated at 100.0% (122 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/en/ --- public/lang/en.json | 276 ++++++++++++++++++++++---------------------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/public/lang/en.json b/public/lang/en.json index 8ad7b7c..ff8294d 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -1,140 +1,140 @@ { - "header": { - "about_title": "About PairDrop", - "about_aria-label": "Open About PairDrop", - "theme-auto_title": "Adapt Theme to System", - "theme-light_title": "Always Use Light-Theme", - "theme-dark_title": "Always Use Dark-Theme", - "notification_title": "Enable Notifications", - "install_title": "Install PairDrop", - "pair-device_title": "Pair Device", - "edit-paired-devices_title": "Edit Paired Devices", - "cancel-paste-mode": "Done" - }, - "instructions": { - "no-peers_data-drop-bg": "Release to select recipient", - "no-peers-title": "Open PairDrop on other devices to send files", - "no-peers-subtitle": "Pair devices to be discoverable on other networks", - "x-instructions_desktop": "Click to send files or right click to send a message", - "x-instructions_mobile": "Tap to send files or long tap to send a message", - "x-instructions_data-drop-peer": "Release to send to peer", - "x-instructions_data-drop-bg": "Release to select recipient", - "click-to-send": "Click to send", - "tap-to-send": "Tap to send" - }, - "footer": { - "known-as": "You are known as:", - "display-name_placeholder": "Loading...", - "display-name_title": "Edit your device name permanently", - "discovery-everyone": "You can be discovered by everyone", - "on-this-network": "on this network", - "and-by": "and by", - "paired-devices": "paired devices", - "traffic": "Traffic is", - "routed": "routed through the server", - "webrtc": "if WebRTC is not available." - }, - "dialogs": { - "activate-paste-mode-base": "Open PairDrop on other devices to send", - "activate-paste-mode-and-other-files": "and {{count}} other files", - "activate-paste-mode-activate-paste-mode-shared-text": "shared text", - "pair-devices-title": "Pair Devices", - "input-key-on-this-device": "Input this key on another device", - "scan-qr-code": "or scan the QR-Code.", - "enter-key-from-another-device": "Enter key from another device to continue.", - "pair": "Pair", - "cancel": "Cancel", - "edit-paired-devices-title": "Edit Paired Devices", - "paired-devices-wrapper_data-empty": "No paired devices.", - "auto-accept-instructions-1": "Activate", - "auto-accept": "auto-accept", - "auto-accept-instructions-2": "to automatically accept all files sent from that device.", - "close": "Close", - "would-like-to-share": "would like to share", - "accept": "Accept", - "decline": "Decline", - "has-sent": "has sent:", - "share": "Share", - "download": "Download", - "send-message-title": "Send Message", - "send-message-to": "Send a Message to", - "send": "Send", - "receive-text-title": "Message Received", - "copy": "Copy", - "base64-processing": "Processing...", - "base64-tap-to-paste": "Tap here to paste {{type}}", - "base64-paste-to-send": "Paste here to send {{type}}", - "base64-text": "text", - "base64-files": "files", - "file-other-description-image": "and 1 other image", - "file-other-description-file": "and 1 other file", - "file-other-description-image-plural": "and {{count}} other images", - "file-other-description-file-plural": "and {{count}} other files", - "title-image": "Image", - "title-file": "File", - "title-image-plural": "Images", - "title-file-plural": "Files", - "receive-title": "{{descriptor}} Received", - "download-again": "Download again" - }, - "about": { - "close-about_aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices", - "github_title": "PairDrop on Github", - "buy-me-a-coffee_title": "Buy me a coffee!", - "tweet_title": "Tweet about PairDrop", - "faq_title": "Frequently asked questions" - }, - "notifications": { - "display-name-changed-permanently": "Display name is changed permanently.", - "display-name-changed-temporarily": "Display name is changed only for this session.", - "display-name-random-again": "Display name is randomly generated again.", - "download-successful": "{{descriptor}} downloaded successfully", - "pairing-tabs-error": "Pairing of two browser tabs is not possible.", - "pairing-success": "Devices paired successfully.", - "pairing-not-persistent": "Paired devices are not persistent.", - "pairing-key-invalid": "Key not valid", - "pairing-key-invalidated": "Key {{key}} invalidated.", - "pairing-cleared": "All Devices unpaired.", - "copied-to-clipboard": "Copied to clipboard", - "text-content-incorrect": "Text content is incorrect.", - "file-content-incorrect": "File content is incorrect.", - "clipboard-content-incorrect": "Clipboard content is incorrect.", - "notifications-enabled": "Notifications enabled.", - "link-received": "Link received by {{name}} - Click to open", - "message-received": "Message received by {{name}} - Click to copy", - "click-to-download": "Click to download", - "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", - "click-to-show": "Click to show", - "copied-text": "Copied text to clipboard", - "copied-text-error": "Writing to clipboard failed. Copy manually!", - "offline": "You are offline", - "online": "You are back online", - "connected": "Connected.", - "online-requirement": "You need to be online to pair devices.", - "connecting": "Connecting...", - "files-incorrect": "Files are incorrect.", - "file-transfer-completed": "File transfer completed.", - "ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once", - "message-transfer-completed": "Message transfer completed.", - "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", - "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", - "selected-peer-left": "Selected peer left." - }, - "document-titles": { - "file-received": "File Received", - "file-received-plural": "{{count}} Files Received", - "file-transfer-requested": "File Transfer Requested", - "message-received": "Message Received", - "message-received-plural": "{{count}} Messages Received" - }, - "peer-ui": { - "click-to-send-paste-mode": "Click to send {{descriptor}}", - "click-to-send": "Click to send files or right click to send a message", - "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", - "preparing": "Preparing...", - "waiting": "Waiting...", - "processing": "Processing...", - "transferring": "Transferring..." - } + "header": { + "about_title": "About PairDrop", + "about_aria-label": "Open About PairDrop", + "theme-auto_title": "Adapt Theme to System", + "theme-light_title": "Always Use Light-Theme", + "theme-dark_title": "Always Use Dark-Theme", + "notification_title": "Enable Notifications", + "install_title": "Install PairDrop", + "pair-device_title": "Pair Device", + "edit-paired-devices_title": "Edit Paired Devices", + "cancel-paste-mode": "Done" + }, + "instructions": { + "no-peers_data-drop-bg": "Release to select recipient", + "no-peers-title": "Open PairDrop on other devices to send files", + "no-peers-subtitle": "Pair devices to be discoverable on other networks", + "x-instructions_desktop": "Click to send files or right click to send a message", + "x-instructions_mobile": "Tap to send files or long tap to send a message", + "x-instructions_data-drop-peer": "Release to send to peer", + "x-instructions_data-drop-bg": "Release to select recipient", + "click-to-send": "Click to send", + "tap-to-send": "Tap to send" + }, + "footer": { + "known-as": "You are known as:", + "display-name_placeholder": "Loading…", + "display-name_title": "Edit your device name permanently", + "discovery-everyone": "You can be discovered by everyone", + "on-this-network": "on this network", + "and-by": "and by", + "paired-devices": "paired devices", + "traffic": "Traffic is", + "routed": "routed through the server", + "webrtc": "if WebRTC is not available." + }, + "dialogs": { + "activate-paste-mode-base": "Open PairDrop on other devices to send", + "activate-paste-mode-and-other-files": "and {{count}} other files", + "activate-paste-mode-activate-paste-mode-shared-text": "shared text", + "pair-devices-title": "Pair Devices", + "input-key-on-this-device": "Input this key on another device", + "scan-qr-code": "or scan the QR-code.", + "enter-key-from-another-device": "Enter key from another device to continue.", + "pair": "Pair", + "cancel": "Cancel", + "edit-paired-devices-title": "Edit Paired Devices", + "paired-devices-wrapper_data-empty": "No paired devices.", + "auto-accept-instructions-1": "Activate", + "auto-accept": "auto-accept", + "auto-accept-instructions-2": "to automatically accept all files sent from that device.", + "close": "Close", + "would-like-to-share": "would like to share", + "accept": "Accept", + "decline": "Decline", + "has-sent": "has sent:", + "share": "Share", + "download": "Download", + "send-message-title": "Send Message", + "send-message-to": "Send a Message to", + "send": "Send", + "receive-text-title": "Message Received", + "copy": "Copy", + "base64-processing": "Processing...", + "base64-tap-to-paste": "Tap here to paste {{type}}", + "base64-paste-to-send": "Paste here to send {{type}}", + "base64-text": "text", + "base64-files": "files", + "file-other-description-image": "and 1 other image", + "file-other-description-file": "and 1 other file", + "file-other-description-image-plural": "and {{count}} other images", + "file-other-description-file-plural": "and {{count}} other files", + "title-image": "Image", + "title-file": "File", + "title-image-plural": "Images", + "title-file-plural": "Files", + "receive-title": "{{descriptor}} Received", + "download-again": "Download again" + }, + "about": { + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on GitHub", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" + }, + "notifications": { + "display-name-changed-permanently": "Display name is changed permanently.", + "display-name-changed-temporarily": "Display name is changed only for this session.", + "display-name-random-again": "Display name is randomly generated again.", + "download-successful": "{{descriptor}} downloaded", + "pairing-tabs-error": "Pairing two web browser tabs is impossible.", + "pairing-success": "Devices paired.", + "pairing-not-persistent": "Paired devices are not persistent.", + "pairing-key-invalid": "Invalid key", + "pairing-key-invalidated": "Key {{key}} invalidated.", + "pairing-cleared": "All Devices unpaired.", + "copied-to-clipboard": "Copied to clipboard", + "text-content-incorrect": "Text content is incorrect.", + "file-content-incorrect": "File content is incorrect.", + "clipboard-content-incorrect": "Clipboard content is incorrect.", + "notifications-enabled": "Notifications enabled.", + "link-received": "Link received by {{name}} - Click to open", + "message-received": "Message received by {{name}} - Click to copy", + "click-to-download": "Click to download", + "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", + "click-to-show": "Click to show", + "copied-text": "Copied text to clipboard", + "copied-text-error": "Writing to clipboard failed. Copy manually!", + "offline": "You are offline", + "online": "You are back online", + "connected": "Connected.", + "online-requirement": "You need to be online to pair devices.", + "connecting": "Connecting…", + "files-incorrect": "Files are incorrect.", + "file-transfer-completed": "File transfer completed.", + "ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once", + "message-transfer-completed": "Message transfer completed.", + "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", + "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", + "selected-peer-left": "Selected peer left." + }, + "document-titles": { + "file-received": "File Received", + "file-received-plural": "{{count}} Files Received", + "file-transfer-requested": "File Transfer Requested", + "message-received": "Message Received", + "message-received-plural": "{{count}} Messages Received" + }, + "peer-ui": { + "click-to-send-paste-mode": "Click to send {{descriptor}}", + "click-to-send": "Click to send files or right click to send a message", + "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", + "preparing": "Preparing…", + "waiting": "Waiting…", + "processing": "Processing…", + "transferring": "Transferring…" + } } From 65ec416646b4e58e1a27bfb775d4f11b5b65db3a Mon Sep 17 00:00:00 2001 From: kek Date: Fri, 7 Jul 2023 23:20:33 +0000 Subject: [PATCH 123/608] Translated using Weblate (Russian) Currently translated at 100.0% (122 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ru/ --- public/lang/ru.json | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/public/lang/ru.json b/public/lang/ru.json index bcd0103..8617ec2 100644 --- a/public/lang/ru.json +++ b/public/lang/ru.json @@ -16,19 +16,19 @@ "no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя", "click-to-send": "Нажмите, чтобы отправить", "x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя", - "tap-to-send": "Нажмите, чтобы отправить", + "tap-to-send": "Прикоснитесь, чтобы отправить", "x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу", - "x-instructions_mobile": "Нажмите, чтобы отправить файлы, или долго нажмите, чтобы отправить сообщение", + "x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение", "no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы", "no-peers-subtitle": "Сопрягите устройства из разных сетей." }, "footer": { - "discovery-everyone": "О вас может узнать любой", - "display-name_placeholder": "Загрузка...", + "discovery-everyone": "О вас может узнать каждый", + "display-name_placeholder": "Загрузка…", "routed": "направляется через сервер", - "webrtc": "есть WebRTC недоступен.", - "traffic": "Трафик:", - "and-by": "и от", + "webrtc": ", если WebRTC недоступен.", + "traffic": "Трафик", + "and-by": "и", "paired-devices": "сопряженные устройства", "known-as": "Вы известны под именем:", "on-this-network": "в этой сети", @@ -55,13 +55,13 @@ "base64-files": "файлы", "base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}", "base64-processing": "Обработка...", - "base64-tap-to-paste": "Нажмите здесь, чтобы вставить {{type}}", + "base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}", "base64-text": "текст", "title-file": "Файл", "title-file-plural": "Файлы", "title-image": "Изображение", "title-image-plural": "Изображения", - "download-again": "Скачать снова", + "download-again": "Скачать еще раз", "auto-accept-instructions-2": "чтобы автоматически принимать все файлы, отправленные с этого устройства.", "enter-key-from-another-device": "Для продолжения введите ключ с другого устройства.", "pair-devices-title": "Сопрягите устройства", @@ -79,12 +79,17 @@ }, "about": { "close-about-aria-label": "Закрыть страницу \"О сервисе\"", - "claim": "Самый простой способ передачи файлов между устройствами" + "claim": "Самый простой способ передачи файлов между устройствами", + "close-about_aria-label": "Закрыть страницу \"О сервисе\"", + "buy-me-a-coffee_title": "Купить мне кофе!", + "github_title": "PairDrop на GitHub", + "tweet_title": "Твит о PairDrop", + "faq_title": "Часто задаваемые вопросы" }, "notifications": { - "display-name-changed-permanently": "Отображаемое имя изменено навсегда.", + "display-name-changed-permanently": "Отображаемое имя было изменено навсегда.", "display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.", - "pairing-success": "Устройства сопряжены успешно.", + "pairing-success": "Устройства сопряжены.", "pairing-tabs-error": "Сопряжение двух вкладок браузера невозможно.", "copied-to-clipboard": "Скопировано в буфер обмена", "pairing-not-persistent": "Сопряженные устройства непостоянны.", @@ -106,23 +111,23 @@ "unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть?", "copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!", "pairing-cleared": "Все устройства не сопряжены.", - "pairing-key-invalid": "Ключ недействителен", + "pairing-key-invalid": "Неверный ключ", "pairing-key-invalidated": "Ключ {{key}} признан недействительным.", "click-to-download": "Нажмите, чтобы скачать", "clipboard-content-incorrect": "Содержание буфера обмена неверно.", "click-to-show": "Нажмите, чтобы показать", - "connecting": "Подключение...", - "download-successful": "{{descriptor}} успешно загружен", + "connecting": "Подключение…", + "download-successful": "{{descriptor}} загружен", "display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.", "file-content-incorrect": "Содержимое файла неверно.", "file-transfer-completed": "Передача файла завершена." }, "peer-ui": { "click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}", - "preparing": "Подготовка...", - "transferring": "Передача...", - "processing": "Обработка...", - "waiting": "Ожидание...", + "preparing": "Подготовка…", + "transferring": "Передача…", + "processing": "Обработка…", + "waiting": "Ожидание…", "connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах", "click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение" }, From 9b71d93dd3dff789c77456a9ff3691dfbb3d8230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 7 Jul 2023 14:09:16 +0000 Subject: [PATCH 124/608] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 80.3% (98 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/ --- public/lang/nb-NO.json | 130 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/public/lang/nb-NO.json b/public/lang/nb-NO.json index 0967ef4..52f1d3d 100644 --- a/public/lang/nb-NO.json +++ b/public/lang/nb-NO.json @@ -1 +1,129 @@ -{} +{ + "header": { + "edit-paired-devices_title": "Rediger sammenkoblede enheter", + "about_title": "Om PairDrop", + "about_aria-label": "Åpne «Om PairDrop»", + "theme-auto_title": "Juster drakt til system", + "theme-light_title": "Alltid bruk lys drakt", + "theme-dark_title": "Alltid bruk mørk drakt", + "notification_title": "Skru på merknader", + "cancel-paste-mode": "Ferdig", + "install_title": "Installer PairDrop", + "pair-device_title": "Sammenkoble enhet" + }, + "footer": { + "discovery-everyone": "Du kan oppdages av alle", + "and-by": "og av", + "webrtc": "hvis WebRTC ikke er tilgjengelig.", + "display-name_placeholder": "Laster inn …", + "display-name_title": "Rediger det vedvarende enhetsnavnet ditt", + "traffic": "Trafikken", + "on-this-network": "på dette nettverket", + "known-as": "Du er kjent som:", + "paired-devices": "sammenkoblede enheter", + "routed": "Sendes gjennom tjeneren" + }, + "instructions": { + "x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding", + "x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding", + "x-instructions_data-drop-bg": "Slipp for å velge mottager", + "click-to-send": "Klikk for å sende", + "no-peers_data-drop-bg": "Slipp for å velge mottager", + "no-peers-title": "Åpne PairDrop på andre enheter for å sende filer", + "no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk", + "x-instructions_data-drop-peer": "Slipp for å sende til likemann", + "tap-to-send": "Trykk for å sende" + }, + "dialogs": { + "input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet", + "pair-devices-title": "Sammenkoble enheter", + "would-like-to-share": "ønsker å dele", + "auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.", + "paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter", + "enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.", + "edit-paired-devices-title": "Rediger sammenkoblede enheter", + "accept": "Godta", + "has-sent": "har sendt:", + "base64-paste-to-send": "Trykk her for å sende {{type}}", + "base64-text": "tekst", + "base64-files": "filer", + "file-other-description-image-plural": "og {{count}} andre bilder", + "receive-title": "{{descriptor}} mottatt", + "send-message-title": "Send melding", + "base64-processing": "Behandler …", + "close": "Lukk", + "decline": "Avslå", + "download": "Last ned", + "copy": "Kopier", + "pair": "Sammenkoble", + "cancel": "Avbryt", + "scan-qr-code": "eller skann QR-koden.", + "auto-accept-instructions-1": "Aktiver", + "receive-text-title": "Melding mottatt", + "auto-accept": "auto-godkjenn", + "share": "Del", + "send-message-to": "Send en melding til", + "send": "Send", + "base64-tap-to-paste": "Trykk her for å lime inn {{type]]", + "file-other-description-image": "og ett annet bilde", + "file-other-description-file-plural": "og {{count}} andre filer", + "title-file-plural": "Filer", + "download-again": "Last ned igjen", + "file-other-description-file": "og én annen fil", + "title-image": "Bilde", + "title-file": "Fil", + "title-image-plural": "Bilder", + "activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende", + "activate-paste-mode-and-other-files": "og {{count}} andre filer", + "activate-paste-mode-activate-paste-mode-shared-text": "delt tekst" + }, + "about": { + "close-about_aria-label": "Lukk «Om PairDrop»", + "faq_title": "Ofte stilte spørsmål", + "claim": "Den enkleste måten å overføre filer mellom enheter", + "buy-me-a-coffee_title": "Spander drikke.", + "tweet_title": "Tvitre om PairDrop", + "github_title": "PairDrop på GitHub" + }, + "notifications": { + "copied-to-clipboard": "Kopiert til utklippstavlen", + "pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.", + "notifications-enabled": "Merknader påskrudd.", + "click-to-show": "Klikk for å vise", + "copied-text": "Tekst kopiert til utklippstavlen", + "connected": "Tilkoblet.", + "online": "Du er tilbake på nett", + "file-transfer-completed": "Filoverføring utført.", + "selected-peer-left": "Valgt likemann dro.", + "pairing-key-invalid": "Ugyldig nøkkel", + "connecting": "Kobler til …", + "pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.", + "offline": "Du er frakoblet", + "online-requirement": "Du må være på nett for å koble sammen enheter.", + "display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.", + "display-name-changed-permanently": "Visningsnavnet er endret for godt.", + "display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.", + "text-content-incorrect": "Tekstinnholdet er uriktig.", + "file-content-incorrect": "Filinnholdet er uriktig.", + "click-to-download": "Klikk for å laste ned", + "message-transfer-completed": "Meldingsoverføring utført.", + "download-successful": "{{descriptor}} nedlastet", + "pairing-success": "Enheter sammenkoblet.", + "pairing-cleared": "Sammenkobling av alle enheter opphevet." + }, + "document-titles": { + "file-received": "Fil mottatt", + "file-received-plural": "{{count}} filer mottatt", + "message-received": "Melding mottatt", + "file-transfer-requested": "Filoverføring forespurt", + "message-received-plural": "{{count}} meldinger mottatt" + }, + "peer-ui": { + "preparing": "Forbereder …", + "waiting": "Venter", + "processing": "Behandler …", + "transferring": "Overfører …", + "click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding", + "click-to-send-paste-mode": "Klikk for å sende {{descriptor}}" + } +} From 044d7aa20da61538448d8f2c586add8b38959062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 12:59:58 +0000 Subject: [PATCH 125/608] docker-swarm-usage reworked --- docs/docker-swarm-usage.md | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/docker-swarm-usage.md b/docs/docker-swarm-usage.md index ae2c97e..218ae82 100644 --- a/docs/docker-swarm-usage.md +++ b/docs/docker-swarm-usage.md @@ -2,42 +2,46 @@ ## Healthcheck -The [Docker Image](../Dockerfile) includes a Healthcheck with the following options: +The [Docker Image](../Dockerfile) includes a health check with the following options: ``` --interval=30s ``` -> Specifies the time interval at which the health check should be performed. In this case, the health check will be performed every 30 seconds. - +> Specifies the time interval to run the health check. \ +> In this case, the health check is performed every 30 seconds.
``` --timeout=10s ``` -> Specifies the amount of time to wait for a response from the health check command. If the response does not arrive within 10 seconds, the health check will be considered a failure. - +> Specifies the amount of time to wait for a response from the \"healthcheck\" command. \ +> If the response does not arrive within 10 seconds, the health check fails.
``` --start-period=5s ``` -> Specifies the amount of time to wait before starting the health check process. In this case, the health check process will begin 5 seconds after the container is started. - +> Specifies the amount of time to wait before starting the health check process. \ +> In this case, the health check process will begin 5 seconds after the container is started.
``` --retries=3 ``` -> Specifies the number of times Docker should retry the health check before considering the container to be unhealthy. - +> Specifies the number of times Docker should retry the health check \ +> before considering the container to be unhealthy.
-The CMD instruction is used to define the command that will be run as part of the health check. -In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. This command will attempt to connect to `http://localhost:3000/` -and if it fails it will exit with a status code of `1`. If this command returns a status code other than `0`, the health check will be considered a failure. +The CMD instruction is used to define the command that will be run as part of the health check. \ +In this case, the command is `wget --quiet --tries=1 --spider http://localhost:3000/ || exit 1`. \ +This command will attempt to connect to `http://localhost:3000/` \ +and if it fails it will exit with a status code of `1`. \ +If this command returns a status code other than `0`, the health check fails. -Overall, this HEALTHCHECK instruction is defining a health check process that will run every 30 seconds, wait up to 10 seconds for a response, -begin 5 seconds after the container is started, and retry up to 3 times. -The health check will consist of attempting to connect to http://localhost:3000/ and will consider the container to be unhealthy if it is unable to connect. +Overall, this HEALTHCHECK instruction is defining a health check process \ +that runs every 30 seconds, and waits up to 10 seconds for a response, \ +begins 5 seconds after the container is started, and retries up to 3 times. \ +The health check attempts to connect to http://localhost:3000/ \ +and will considers the container unhealthy if unable to connect. From 9424f704bf6eae38fed9b13ec985703feb7c3f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 13:14:07 +0000 Subject: [PATCH 126/608] host-your-own reworked --- docs/host-your-own.md | 171 +++++++++++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 60 deletions(-) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 41ebe9b..5816a95 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -3,9 +3,10 @@ The easiest way to get PairDrop up and running is by using Docker. > TURN server for Internet Transfer > -> Beware that you have to host your own TURN server in order to enable transfers between different networks. +> Beware that you have to host your own TURN server to enable transfers between different networks. > -> You can follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) or deploy it via docker-compose (Step 5). +> Follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) \ +> or deploy it via docker-compose (Step 5). ## Deployment with Docker @@ -15,9 +16,11 @@ The easiest way to get PairDrop up and running is by using Docker. docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 lscr.io/linuxserver/pairdrop ``` -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For \ +> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the docker container directly, \ +> `127.0.0.1` is specified in the run command. #### Options / Flags Set options by using the following flags in the `docker run` command: @@ -39,21 +42,30 @@ Set options by using the following flags in the `docker run` command: ```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. +> 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. +> 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 ``` -> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. +> Provides PairDrop to clients with an included websocket fallback \ +> if the peer to peer WebRTC connection is not available to the client. > -> This is not used on the official https://pairdrop.net, but you can activate it on your self-hosted instance. -> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). +> This is not used on the official https://pairdrop.net website, \ +> but you can activate it on your self-hosted instance. +> This is especially useful if you connect to your instance via a VPN (as most VPN services block WebRTC completely in order to hide your real IP address). ([Read more here](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). > -> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer! -> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. +> **Warning:** All traffic sent between devices using this fallback \ +> is routed through the server and therefor not peer to peer! \ +> Beware that the traffic routed via this fallback is readable by the server. \ +> Only ever use this on instances you can trust. \ > Additionally, beware that all traffic using this fallback debits the servers data plan. ##### Specify STUN/TURN Servers @@ -61,7 +73,8 @@ Set options by using the following flags in the `docker run` command: -e RTC_CONFIG="rtc_config.json" ``` -> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration. +> Specify the STUN/TURN servers PairDrop clients use by setting \ +> `RTC_CONFIG` to a JSON file including the configuration. \ > You can use `pairdrop/rtc_config_example.json` as a starting point. > > To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ @@ -83,8 +96,10 @@ Set options by using the following flags in the `docker run` command: -e DEBUG_MODE="true" ``` -> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server) -> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status. +> Use this flag to enable debugging information about the connecting peers IP addresses. \ +> This is quite useful to check whether the [#HTTP-Server](#http-server) \ +> is configured correctly, so the auto-discovery feature works correctly. \ +> Otherwise, all clients discover each other mutually, independently of their network status. > > If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this: > ``` @@ -97,7 +112,7 @@ Set options by using the following flags in the `docker run` command: > if IP is private, '127.0.0.1' is used instead > ----DEBUGGING-PEER-IP-END---- > ``` -> If the IP PairDrop uses is the public IP of your device everything is correctly setup. +> If the IP PairDrop uses is the public IP of your device, everything is set up correctly. \ >To find out your devices public IP visit https://www.whatismyip.com/. > > To preserve your clients' privacy, **never use this flag in production!** @@ -109,13 +124,17 @@ Set options by using the following flags in the `docker run` command: ```bash docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 ghcr.io/schlagmichdoch/pairdrop npm run start:prod ``` -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For to prevent \ +> all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the Docker container directly, \ +> `127.0.0.1` is specified in the run command. > -> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) +> To specify options replace `npm run start:prod` \ +> according to [the documentation below.](#options--flags-1) -> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage) +> The Docker Image includes a healthcheck. \ +> Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage). ### Docker Image self-built #### Build the image @@ -130,13 +149,17 @@ docker build --pull . -f Dockerfile -t pairdrop ```bash docker run -d --restart=unless-stopped --name=pairdrop -p 127.0.0.1:3000:3000 -it pairdrop npm run start:prod ``` -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For \ +> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the Docker container \ +> directly, `127.0.0.1` is specified in the run command. > -> To specify options replace `npm run start:prod` according to [the documentation below.](#options--flags-1) +> To specify options replace `npm run start:prod` \ +> according to [the documentation below.](#options--flags-1) -> The Docker Image includes a Healthcheck. To learn more see [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage) +> The Docker Image includes a Healthcheck. \ +Read more about [Docker Swarm Usage](docker-swarm-usage.md#docker-swarm-usage).
@@ -162,9 +185,11 @@ services: Run the compose file with `docker compose up -d`. -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For \ +> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> To prevent bypassing the proxy by reaching the docker container directly, `127.0.0.1` is specified in the run command. +> To prevent bypassing the proxy by reaching the Docker container \ +> directly, `127.0.0.1` is specified in the run command.
@@ -190,7 +215,7 @@ or npm start ``` -> Remember to check your IP Address using your OS command to see where you can access the server. +> Remember to check your IP address using your OS command to see where you can access the server. > By default, the node server listens on port 3000. @@ -212,7 +237,8 @@ $env:PORT=3010; npm start ```bash IPV6_LOCALIZE=4 ``` -> Truncate a portion of the client IPv6 address to make peers more discoverable. See [Options/Flags](#options--flags) above. +> 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 @@ -223,10 +249,12 @@ On Windows ```bash $env:RTC_CONFIG="rtc_config.json"; npm start ``` -> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` to a JSON file including the configuration. +> Specify the STUN/TURN servers PairDrop clients use by setting `RTC_CONFIG` \ +> to a JSON file including the configuration. \ > You can use `pairdrop/rtc_config_example.json` as a starting point. > -> To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ +> To host your own TURN server you can follow this guide: \ +> https://gabrieltanner.org/blog/turn-server/ > > Default configuration: > ```json @@ -250,10 +278,13 @@ On Windows $env:DEBUG_MODE="true"; npm start ``` -> Use this flag to enable debugging information about the connecting peers IP addresses. This is quite useful to check whether the [#HTTP-Server](#http-server) -> is configured correctly, so the auto discovery feature works correctly. Otherwise, all clients discover each other mutually, independently of their network status. +> Use this flag to enable debugging info about the connecting peers IP addresses. \ +> This is quite useful to check whether the [#HTTP-Server](#http-server) \ +> is configured correctly, so the auto discovery feature works correctly. \ +> Otherwise, all clients discover each other mutually, independently of their network status. > -> If this flag is set to `"true"` each peer that connects to the PairDrop server will produce a log to STDOUT like this: +> If this flag is set to `"true"` each peer that connects to the \ +> PairDrop server will produce a log to STDOUT like this: > ``` > ----DEBUGGING-PEER-IP-START---- > remoteAddress: ::ffff:172.17.0.1 @@ -264,10 +295,10 @@ $env:DEBUG_MODE="true"; npm start > if IP is private, '127.0.0.1' is used instead > ----DEBUGGING-PEER-IP-END---- > ``` -> If the IP PairDrop uses is the public IP of your device everything is correctly setup. ->To find out your devices public IP visit https://www.whatismyip.com/. +> If the IP PairDrop uses is the public IP of your device everything is set up correctly. \ +>Find your devices public IP by visiting https://www.whatismyip.com/. > -> To preserve your clients' privacy, **never use this flag in production!** +> Preserve your clients' privacy. **Never use this flag in production!** ### Options / Flags @@ -277,9 +308,11 @@ npm start -- --localhost-only ``` > Only allow connections from localhost. > -> You must use a server proxy to set the X-Forwarded-For to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). +> You must use a server proxy to set the X-Forwarded-For \ +> to prevent all clients from discovering each other (See [#HTTP-Server](#http-server)). > -> Use this when deploying PairDrop with node to prevent bypassing the proxy by reaching the docker container directly. +> Use this when deploying PairDrop with node to prevent \ +> bypassing the proxy by reaching the Docker container directly. #### Automatic restart on error ```bash @@ -301,13 +334,19 @@ npm start -- --rate-limit ```bash npm start -- --include-ws-fallback ``` -> Provides PairDrop to clients with an included websocket fallback if the peer to peer WebRTC connection is not available to the client. +> Provides PairDrop to clients with an included websocket fallback \ +> if the peer to peer WebRTC connection is not available to the client. > -> This is not used on the official https://pairdrop.net, but you can activate it on your self-hosted instance. -> This is especially useful if you connect to your instance via a VPN as most VPN services block WebRTC completely in order to hide your real IP address ([read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). +> This is not used on the official https://pairdrop.net, \ +but you can activate it on your self-hosted instance. \ +> This is especially useful if you connect to your instance \ +> via a VPN as most VPN services block WebRTC completely in order to hide your real IP address. +> ([Read more](https://privacysavvy.com/security/safe-browsing/disable-webrtc-chrome-firefox-safari-opera-edge/)). > -> **Warning:** All traffic sent between devices using this fallback is routed through the server and therefor not peer to peer! -> Beware that the traffic routed via this fallback is readable by the server. Only ever use this on instances you can trust. +> **Warning:** All traffic sent between devices using this fallback \ +> is routed through the server and therefor not peer to peer! \ +> Beware that the traffic routed via this fallback is readable by the server. \ +> Only ever use this on instances you can trust. \ > Additionally, beware that all traffic using this fallback debits the servers data plan.
@@ -321,10 +360,12 @@ npm run start:prod ```bash npm run start:prod -- --localhost-only --include-ws-fallback ``` -> To prevent connections to the node server from bypassing the proxy server you should always use "--localhost-only" on production. +> To prevent connections to the node server from bypassing \ +> the proxy server you should always use "--localhost-only" on production. ## HTTP-Server -When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. Otherwise, all clients will be mutually visible. +When running PairDrop, the `X-Forwarded-For` header has to be set by a proxy. \ +Otherwise, all clients will be mutually visible. To check if your setup is configured correctly [use the environment variable `DEBUG_MODE="true"`](#debug-mode). @@ -405,10 +446,10 @@ a2enmod proxy_wstunnel
-Create a new configuration file under `/etc/apache2/sites-available` (on debian) +Create a new configuration file under `/etc/apache2/sites-available` (on Debian) **pairdrop.conf** -#### Allow http and https requests +#### Allow HTTP and HTTPS requests ```apacheconf ProxyPass / http://127.0.0.1:3000/ @@ -425,7 +466,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian) RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L] ``` -#### Automatic http to https redirect: +#### Automatic HTTP to HTTPS redirect: ```apacheconf Redirect permanent / https://127.0.0.1:3000/ @@ -438,7 +479,7 @@ Create a new configuration file under `/etc/apache2/sites-available` (on debian) RewriteRule ^/?(.*) "wws://127.0.0.1:3000/$1" [P,L] ``` -Activate the new virtual host and reload apache: +Activate the new virtual host and reload Apache: ```bash a2ensite pairdrop ``` @@ -462,28 +503,38 @@ Then, clone the repository and run docker-compose: docker-compose up -d ``` -Now point your browser to `http://localhost:8080`. +Now point your web browser to `http://localhost:8080`. -- To restart the containers run `docker-compose restart`. -- To stop the containers run `docker-compose stop`. -- To debug the NodeJS server run `docker logs pairdrop_node_1`. +- To restart the containers, run `docker-compose restart`. +- To stop the containers, run `docker-compose stop`. +- To debug the NodeJS server, run `docker logs pairdrop_node_1`.
## Testing PWA related features -PWAs require that the app is served under a correctly set up and trusted TLS endpoint. +PWAs requires the app to be served under a correctly set up and trusted TLS endpoint. -The nginx container creates a CA certificate and a website certificate for you. To correctly set the common name of the certificate, you need to change the FQDN environment variable in `docker/fqdn.env` to the fully qualified domain name of your workstation. +The NGINX container creates a CA certificate and a website certificate for you. \ +To correctly set the common name of the certificate, \ +you need to change the FQDN environment variable in `docker/fqdn.env` \ +to the fully qualified domain name of your workstation. -If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. For your convenience, you can download the crt file from `http://:8080/ca.crt`. Install that certificate to the trust store of your operating system. -- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. -- On macOS, double-click the installed CA certificate in `Keychain Access`, expand `Trust`, and select `Always Trust` for SSL. -- Firefox uses its own trust store. To install the CA, point Firefox at `http://:8080/ca.crt`. When prompted, select `Trust this CA to identify websites` and click OK. -- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). Additionally, after installing a new cert, you need to clear the Storage (DevTools -> Application -> Clear storage -> Clear site data). +If you want to test PWA features, you need to trust the CA of the certificate for your local deployment. \ +For your convenience, you can download the crt file from `http://:8080/ca.crt`. \ +Install that certificate to the trust store of your operating system. \ +- On Windows, make sure to install it to the `Trusted Root Certification Authorities` store. \ +- On macOS, double-click the installed CA certificate in `Keychain Access`, \ +- expand `Trust`, and select `Always Trust` for SSL. \ +- Firefox uses its own trust store. To install the CA, \ +- point Firefox at `http://:8080/ca.crt`. \ +- When prompted, select `Trust this CA to identify websites` and click \"OK\". \ +- When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). \ +- Additionally, after installing a new cert, \ +- you need to clear the Storage (DevTools → Application → Clear storage → Clear site data). Please note that the certificates (CA and webserver cert) expire after a day. -Also, whenever you restart the nginx docker, container new certificates are created. +Also, whenever you restart the NGINX Docker, container new certificates are created. The site is served on `https://:8443`. From 913b60b71270919f66923a74d855bc11938e8c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 13:17:35 +0000 Subject: [PATCH 127/608] how-to reworked --- docs/how-to.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/how-to.md b/docs/how-to.md index a764816..f38d5f2 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -5,7 +5,7 @@ The [File Handling API](https://learn.microsoft.com/en-us/microsoft-edge/progres This is still experimental and must be enabled via a flag **before** the PWA is installed to Windows. 1. [Enabled feature in Edge](https://learn.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files#enable-the-file-handling-api) -2. Install PairDrop by visiting https://pairdrop.net/ with the Edge browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-). +2. Install PairDrop by visiting https://pairdrop.net/ with the Edge web browser and install it as described [here](faq.md#help--i-cant-install-the-pwa-). 3. You are done! You can now send most files one at a time via PairDrop: _context menu > Open with > PairDrop_ @@ -13,7 +13,8 @@ This is still experimental and must be enabled via a flag **before** the PWA is [//]: # (Todo: add screenshots) ### Sending multiple files to PairDrop -Outstandingly, it is also possible to send multiple files to PairDrop via the context menu by adding PairDrop to the `Send to` menu: +Outstandingly, it is also possible to send multiple files to PairDrop \ +via the context menu by adding PairDrop to the `Send to` menu: 1. [Register PairDrop as file handler](#registering-to-open-files-with-pairdrop) 2. Hit Windows Key+R, type: `shell:programs` and hit Enter. 3. Copy the PairDrop shortcut from the directory @@ -26,7 +27,8 @@ Outstandingly, it is also possible to send multiple files to PairDrop via the co [//]: # (Todo: add screenshots) ## Send directly from share menu on iOS -I created an iOS shortcut to send images, files, folder, URLs or text directly from the share-menu +I created an iOS shortcut to send images, files, folder, URLs \ +or text directly from the share-menu https://routinehub.co/shortcut/13990/ [//]: # (Todo: add doku with screenshots) @@ -63,7 +65,7 @@ On Windows Command Prompt you need to use bash: `bash pairdrop -h` Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop). #### Linux -1. Put file in a preferred folder e.g. `/usr/local/bin` +1. Put the file in a preferred folder e.g. `/usr/local/bin` 2. Make sure the bash file is executable. Otherwise, use `chmod +x pairdrop` 3. Add absolute path of the folder to PATH variable to make `pairdrop` available globally by executing `export PATH=$PATH:/opt/pairdrop-cli` @@ -74,7 +76,7 @@ Download the bash file: [pairdrop-cli/pairdrop](/pairdrop-cli/pairdrop). #### Windows 1. Put file in a preferred folder e.g. `C:\Users\Public\pairdrop-cli` 2. Search for and open `Edit environment variables for your account` -3. Click `Environment Variables...` +3. Click `Environment Variables…` 4. Under *System Variables* select `Path` and click *Edit...* 5. Click *New*, insert the preferred folder (`C:\Users\Public\pairdrop-cli`), click *OK* until all windows are closed 6. Reopen Command prompt window From 17a12baa2a48643f7c015c4b2dcbb67e31bbdfa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 13:24:42 +0000 Subject: [PATCH 128/608] technical-documentation reworked --- docs/technical-documentation.md | 80 +++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/docs/technical-documentation.md b/docs/technical-documentation.md index bf050ae..b8783c0 100644 --- a/docs/technical-documentation.md +++ b/docs/technical-documentation.md @@ -3,48 +3,80 @@ Encryption is mandatory for WebRTC connections and completely done by the browser itself. -When the peers are first connecting, a channel is created by exchanging their signaling information. -This signaling information includes some sort of public key and is specific to the clients ip address. -That is what the STUN Server is used for: it simply returns your public IP address as you only know your local ip address +When the peers are first connecting, \ +a channel is created by exchanging their signaling info. \ +This signaling information includes some sort of public key \ +and is specific to the clients IP address. \ +That is what the STUN Server is used for: \ +it simply returns your public IP address \ +as you only know your local ip address \ if behind a NAT (router). -The transfer of the signaling information is done by the PairDrop / Snapdrop server using secure websockets. -After that the channel itself is completely peer-2-peer and all information can only be decrypted by the receiver. -When the two peers are on the same network or when they are not behind any NAT system (which they are always for classic -Snapdrop and for not paired users on PairDrop) the files are send directly peer to peer. +The transfer of the signaling info is done by the \ +PairDrop / Snapdrop server using secure websockets. \ +After that the channel itself is completely peer-to-peer \ +and all info can only be decrypted by the receiver. \ +When the two peers are on the same network \ +or when they are not behind any NAT system \ +(which they are always for classic \ +Snapdrop and for not paired users on PairDrop) \ +the files are send directly peer-to-peer. -When a user is behind a NAT (behind a router) the contents are channeled through a TURN server. -But again, the contents send via the channel can only be decrypted by the receiver. So a rogue TURN server could only -see that there is a connection, but not what is sent. Obviously, connections which are channeled through a TURN server -are not as fast as peer to peer. +When a user is behind a NAT (behind a router) \ +the contents are channeled through a TURN server. \ +But again, the contents send via the channel \ +can only be decrypted by the receiver. \ +So a rogue TURN server could only \ +see that there is a connection, but not what is sent. \ +Obviously, connections which are channeled through a TURN server \ +are not as fast as peer-to-peer. -The selection whether a TURN server is needed or not is also done automatically by the browser. -It simply iterated through the configured RTC iceServers and checks what works. Only if the STUN server is not sufficient, +The selection whether a TURN server is needed \ +or not is also done automatically by the web browser. \ +It simply iterated through the configured \ +RTC iceServers and checks what works. \ +Only if the STUN server is not sufficient, \ the TURN server is used. ![img](https://www.wowza.com/wp-content/uploads/WeRTC-Encryption-Diagrams-01.jpg) _Diagram created by wowza.com_ -Good thing: if your device has an IPv6 address it is uniquely reachable by that address. As I understand it, when both devices are using IPv6 addresses there is no need for a TURN server in any scenario. +Good thing: if your device has an IPv6 address \ +it is uniquely reachable by that address. \ +As I understand it, when both devices are using \ +IPv6 addresses there is no need for a TURN server in any scenario. -To learn more take a look at https://www.wowza.com/blog/webrtc-encryption-and-security which gives a good insight into stun, turn and webrtc +Learn more by reading https://www.wowza.com/blog/webrtc-encryption-and-security \ +which gives a good insight into STUN, TURN and WebRTC. ## Device Pairing The pairing functionality uses the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). -It works by creating long secrets that are served by the server to the initiating and requesting pair peer, -when the inserted key is correct. These long secrets are then saved to an indexedDB database in the browser. -IndexedDB is somewhat the successor of localStorage as saved data is shared between all tabs. -It goes one step further by making the data persistent and available offline if implemented to a PWA. +It works by creating long secrets that are served \ +by the server to the initiating and requesting pair peer, \ +when the inserted key is correct. \ +These long secrets are then saved to an \ +indexedDB database in the web browser. \ +IndexedDB is somewhat the successor of localStorage \ +as saved data is shared between all tabs. \ +It goes one step further by making the data persistent \ +and available offline if implemented to a PWA. -All secrets a client has saved to its database are send to the PairDrop server. Peers with a common secret are discoverable -to each other analog to peers with the same ip-address are discoverable to each other. +All secrets a client has saved to its database \ +are sent to the PairDrop server. \ +Peers with a common secret are discoverable \ +to each other analog to peers with the same \ +IP address are discoverable by each other. -What I really like about this approach, and the reason why I implemented it, is that devices on the same network are always -visible regardless whether any devices are paired or not. The main user flow is never obstructed. Paired devices are simply -shown additionally. This makes it in my idea better than the idea of using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214). +What I really like about this approach (and the reason I implemented it) \ +is that devices on the same network are always \ +visible regardless whether any devices are paired or not. \ +The main user flow is never obstructed. \ +Paired devices are simply shown additionally. \ +This makes it in my idea better than the idea of \ +using a room system as [discussed here](https://github.com/RobinLinus/snapdrop/pull/214). [< Back](/README.md) From 6563ec98b39fdf4aa3abcdd34c3fad60577d74ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Sat, 8 Jul 2023 13:26:48 +0000 Subject: [PATCH 129/608] Bold button scheme --- docs/host-your-own.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 5816a95..2a63446 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -528,7 +528,7 @@ Install that certificate to the trust store of your operating system. \ - expand `Trust`, and select `Always Trust` for SSL. \ - Firefox uses its own trust store. To install the CA, \ - point Firefox at `http://:8080/ca.crt`. \ -- When prompted, select `Trust this CA to identify websites` and click \"OK\". \ +- When prompted, select `Trust this CA to identify websites` and click *OK*. \ - When using Chrome, you need to restart Chrome so it reloads the trust store (`chrome://restart`). \ - Additionally, after installing a new cert, \ - you need to clear the Storage (DevTools → Application → Clear storage → Clear site data). From dccc17400cb493581bef27ffd51e916e3bf020da Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 19 Jul 2023 10:50:01 +0200 Subject: [PATCH 130/608] Added translation using Weblate (Chinese (Simplified)) --- public/lang/zh-Hans.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/zh-Hans.json diff --git a/public/lang/zh-Hans.json b/public/lang/zh-Hans.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/zh-Hans.json @@ -0,0 +1 @@ +{} From 6d7c13775f2a2ccbc0a16fc5499983ac94ddd811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=8D=E5=BB=BA=E5=85=B4?= Date: Wed, 19 Jul 2023 09:18:40 +0000 Subject: [PATCH 131/608] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (122 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/zh_Hans/ --- public/lang/zh-Hans.json | 141 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/public/lang/zh-Hans.json b/public/lang/zh-Hans.json index 0967ef4..4191c7d 100644 --- a/public/lang/zh-Hans.json +++ b/public/lang/zh-Hans.json @@ -1 +1,140 @@ -{} +{ + "header": { + "about_title": "关于 PairDrop", + "about_aria-label": "打开 关于 PairDrop", + "theme-light_title": "总是使用明亮主题", + "install_title": "安装 PairDrop", + "pair-device_title": "配对新设备", + "theme-auto_title": "主题适应系统", + "theme-dark_title": "总是使用暗黑主题", + "notification_title": "开启通知", + "edit-paired-devices_title": "管理已配对设备", + "cancel-paste-mode": "完成" + }, + "instructions": { + "x-instructions_data-drop-peer": "释放以发送到此设备", + "no-peers_data-drop-bg": "释放来选择接收者", + "no-peers-subtitle": "配对新设备使在其他网络上可见", + "no-peers-title": "在其他设备上打开 PairDrop 来发送文件", + "x-instructions_desktop": "点击以发送文件 或 右键来发送信息", + "x-instructions_mobile": "轻触以发送文件 或 长按来发送信息", + "x-instructions_data-drop-bg": "释放来选择接收者", + "click-to-send": "点击发送", + "tap-to-send": "轻触发送" + }, + "footer": { + "routed": "途径服务器", + "webrtc": "如果 WebRTC 不可用。", + "known-as": "你的名字是:", + "display-name_placeholder": "加载中…", + "and-by": "和", + "display-name_title": "长久修改你的设备名", + "discovery-everyone": "你对所有人可见", + "on-this-network": "在此网络上", + "paired-devices": "已配对的设备", + "traffic": "流量将" + }, + "dialogs": { + "activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送", + "activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件", + "activate-paste-mode-activate-paste-mode-shared-text": "分享文本", + "pair-devices-title": "配对新设备", + "input-key-on-this-device": "在另一个设备上输入这串数字", + "base64-text": "信息", + "enter-key-from-another-device": "输入从另一个设备上获得的数字以继续。", + "edit-paired-devices-title": "管理已配对的设备", + "pair": "配对", + "cancel": "取消", + "scan-qr-code": "或者 扫描二维码。", + "paired-devices-wrapper_data-empty": "无已配对设备。", + "auto-accept-instructions-1": "启用", + "auto-accept": "自动接收", + "decline": "拒绝", + "base64-processing": "处理中...", + "base64-tap-to-paste": "轻触此处粘贴{{type}}", + "base64-paste-to-send": "粘贴到此处以发送 {{type}}", + "auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。", + "would-like-to-share": "想要分享", + "accept": "接收", + "close": "关闭", + "share": "分享", + "download": "保存", + "send": "发送", + "receive-text-title": "收到信息", + "copy": "复制", + "send-message-title": "发送信息", + "send-message-to": "发了一条信息给", + "has-sent": "发送了:", + "base64-files": "文件", + "file-other-description-file": "和 1 个其他的文件", + "file-other-description-image": "和 1 个其他的图片", + "file-other-description-image-plural": "和 {{count}} 个其他的图片", + "file-other-description-file-plural": "和 {{count}} 个其他的文件", + "title-image-plural": "图片", + "receive-title": "收到 {{descriptor}}", + "title-image": "图片", + "title-file": "文件", + "title-file-plural": "文件", + "download-again": "再次保存" + }, + "about": { + "faq_title": "常见问题", + "close-about_aria-label": "关闭 关于 PairDrop", + "github_title": "PairDrop 在 GitHub 上开源", + "claim": "最简单的跨设备传输方案", + "buy-me-a-coffee_title": "帮我买杯咖啡!", + "tweet_title": "关于 PairDrop 的推特" + }, + "notifications": { + "display-name-changed-permanently": "展示的名字已经长久变更。", + "display-name-changed-temporarily": "展示的名字已经变更 仅在此会话中。", + "display-name-random-again": "展示的名字再次随机生成。", + "download-successful": "{{descriptor}} 已下载", + "pairing-tabs-error": "无法配对两个浏览器标签页。", + "pairing-success": "新设备已配对。", + "pairing-not-persistent": "配对的设备不是持久的。", + "pairing-key-invalid": "无效配对码", + "pairing-key-invalidated": "配对码 {{key}} 已失效。", + "text-content-incorrect": "文本内容不合法。", + "file-content-incorrect": "文件内容不合法。", + "clipboard-content-incorrect": "剪贴板内容不合法。", + "link-received": "收到来自 {{name}} 的链接 - 点击打开", + "message-received": "收到来自 {{name}} 的信息 - 点击复制", + "request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}", + "click-to-show": "点击展示", + "copied-text": "复制到剪贴板", + "selected-peer-left": "选择的设备已离开。", + "pairing-cleared": "所有设备已解除配对。", + "copied-to-clipboard": "已复制到剪贴板", + "notifications-enabled": "通知已启用。", + "copied-text-error": "写入剪贴板失败。请手动复制!", + "click-to-download": "点击以保存", + "unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭吗?", + "message-transfer-completed": "信息传输已完成。", + "offline": "你未连接到网络", + "online": "你已重新连接到网络", + "connected": "已连接。", + "online-requirement": "你需要连接网络来配对新设备。", + "files-incorrect": "文件不合法。", + "file-transfer-completed": "文件传输已完成。", + "connecting": "连接中…", + "ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB", + "rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。" + }, + "document-titles": { + "message-received": "收到信息", + "message-received-plural": "收到 {{count}} 条信息", + "file-transfer-requested": "文件传输请求", + "file-received-plural": "收到 {{count}} 个文件", + "file-received": "收到文件" + }, + "peer-ui": { + "click-to-send-paste-mode": "点击发送 {{descriptor}}", + "click-to-send": "点击以发送文件 或 右键来发送信息", + "connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号", + "preparing": "准备中…", + "waiting": "请等待…", + "transferring": "传输中…", + "processing": "处理中…" + } +} From 445a29540498f7bc8fa764e099dde8af5528cccc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 04:58:27 +0000 Subject: [PATCH 132/608] Bump express-rate-limit from 6.7.0 to 6.8.0 Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 6.7.0 to 6.8.0. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v6.7.0...v6.8.0) --- updated-dependencies: - dependency-name: express-rate-limit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index c49aca5..b96bb32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", - "express-rate-limit": "^6.7.0", + "express-rate-limit": "^6.8.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", "ws": "^8.13.0" @@ -204,11 +204,11 @@ } }, "node_modules/express-rate-limit": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", - "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz", + "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==", "engines": { - "node": ">= 12.9.0" + "node": ">= 14.0.0" }, "peerDependencies": { "express": "^4 || ^5" @@ -801,9 +801,9 @@ } }, "express-rate-limit": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", - "integrity": "sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA==", + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz", + "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==", "requires": {} }, "finalhandler": { diff --git a/package.json b/package.json index aae3823..0b8860a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", - "express-rate-limit": "^6.7.0", + "express-rate-limit": "^6.8.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", "ws": "^8.13.0" From 471278f7b0727b3ec9476e88be7422db301101ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Mon, 24 Jul 2023 16:02:13 +0000 Subject: [PATCH 133/608] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 81.9% (100 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/nb_NO/ --- public/lang/nb-NO.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/public/lang/nb-NO.json b/public/lang/nb-NO.json index 52f1d3d..b11b664 100644 --- a/public/lang/nb-NO.json +++ b/public/lang/nb-NO.json @@ -109,7 +109,17 @@ "message-transfer-completed": "Meldingsoverføring utført.", "download-successful": "{{descriptor}} nedlastet", "pairing-success": "Enheter sammenkoblet.", - "pairing-cleared": "Sammenkobling av alle enheter opphevet." + "pairing-cleared": "Sammenkobling av alle enheter opphevet.", + "pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.", + "copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt.", + "clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.", + "link-received": "Lenke mottatt av {{name}}. Klikk for å åpne.", + "request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}", + "message-received": "Melding mottatt av {{name}}. Klikk for å åpne.", + "files-incorrect": "Filene er uriktige.", + "ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.", + "unfinished-transfers-warning": "Lukk med ufullførte overføringer?", + "rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen." }, "document-titles": { "file-received": "Fil mottatt", @@ -124,6 +134,7 @@ "processing": "Behandler …", "transferring": "Overfører …", "click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding", - "click-to-send-paste-mode": "Klikk for å sende {{descriptor}}" + "click-to-send-paste-mode": "Klikk for å sende {{descriptor}}", + "connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen." } } From 714608ce978abf568d3334320e6143c89b8b6fe6 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 25 Jul 2023 18:56:56 +0200 Subject: [PATCH 134/608] Added translation using Weblate (Turkish) --- public/lang/tr.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 public/lang/tr.json diff --git a/public/lang/tr.json b/public/lang/tr.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public/lang/tr.json @@ -0,0 +1 @@ +{} From b319fbe1560bbd147a615261275d3977a43f1de6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 25 Jul 2023 22:58:58 +0000 Subject: [PATCH 135/608] Translated using Weblate (Turkish) Currently translated at 12.2% (15 of 122 strings) Translation: PairDrop/pairdrop-spa Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/tr/ --- public/lang/tr.json | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/public/lang/tr.json b/public/lang/tr.json index 0967ef4..783e25b 100644 --- a/public/lang/tr.json +++ b/public/lang/tr.json @@ -1 +1,25 @@ -{} +{ + "header": { + "about_title": "PairDrop Hakkında", + "about_aria-label": "PairDrop Hakkında Aç", + "theme-auto_title": "Temayı Sisteme Uyarla", + "theme-light_title": "Daima Açık Tema Kullan", + "theme-dark_title": "Daima Koyu Tema Kullan", + "notification_title": "Bildirimleri Etkinleştir", + "install_title": "PairDrop'u Yükle", + "pair-device_title": "Cihaz Eşleştir", + "edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle", + "cancel-paste-mode": "Bitti" + }, + "instructions": { + "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" + }, + "footer": { + "display-name_placeholder": "Yükleniyor…", + "display-name_title": "Cihazının adını kalıcı olarak düzenle" + }, + "dialogs": { + "cancel": "İptal", + "edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle" + } +} From da5038a51a015057c925ee7762e192f3dfd91fd5 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 7 Jul 2023 14:58:15 +0200 Subject: [PATCH 136/608] include translations for about buttons and implement translation fallback if used translation is not complete --- public/index.html | 14 +++--- public/lang/en.json | 8 +++- public/scripts/localization.js | 43 +++++++++++-------- public_included_ws_fallback/index.html | 14 +++--- public_included_ws_fallback/lang/en.json | 8 +++- .../scripts/localization.js | 43 +++++++++++-------- 6 files changed, 78 insertions(+), 52 deletions(-) diff --git a/public/index.html b/public/index.html index 15b82da..21f6005 100644 --- a/public/index.html +++ b/public/index.html @@ -83,7 +83,7 @@
- +
@@ -161,7 +161,7 @@

Edit Paired Devices

-
+
The easiest way to transfer files across devices
- + - + - + - + diff --git a/public/lang/en.json b/public/lang/en.json index 7ae2e56..8ad7b7c 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -78,8 +78,12 @@ "download-again": "Download again" }, "about": { - "close-about-aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices" + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on Github", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" }, "notifications": { "display-name-changed-permanently": "Display name is changed permanently.", diff --git a/public/scripts/localization.js b/public/scripts/localization.js index d09d5c0..5f2730b 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -2,10 +2,10 @@ class Localization { constructor() { Localization.defaultLocale = "en"; Localization.supportedLocales = ["en"]; - Localization.translations = {}; + Localization.defaultTranslations = {}; - const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); + const initialLocale = Localization.supportedOrDefault(navigator.languages); Localization.setLocale(initialLocale) .then(_ => { @@ -21,25 +21,21 @@ class Localization { return locales.find(Localization.isSupported) || Localization.defaultLocale; } - static browserLocales() { - return navigator.languages.map(locale => - locale.split("-")[0] - ); - } - static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; + const isFirstTranslation = !Localization.locale + + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); + const newTranslations = await Localization.fetchTranslationsFor(newLocale); if(!newTranslations) return false; - const firstTranslation = !Localization.locale - Localization.locale = newLocale; Localization.translations = newTranslations; - if (firstTranslation) { + if (isFirstTranslation) { Events.fire("translation-loaded"); } } @@ -65,30 +61,43 @@ class Localization { for (let i in attrs) { let attr = attrs[i]; if (attr === "text") { - element.innerText = await Localization.getTranslation(key); + element.innerText = Localization.getTranslation(key); } else { - element.attr = await Localization.getTranslation(key, attr); + element.attr = Localization.getTranslation(key, attr); } } } - static getTranslation(key, attr, data) { + static getTranslation(key, attr, data, useDefault=false) { const keys = key.split("."); - let translationCandidates = Localization.translations; + let translationCandidates = useDefault + ? Localization.defaultTranslations + : Localization.translations; for (let i=0; i
- +
@@ -166,7 +166,7 @@

Edit Paired Devices

-
+
The easiest way to transfer files across devices
- + - + - + - + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index 7ae2e56..8ad7b7c 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -78,8 +78,12 @@ "download-again": "Download again" }, "about": { - "close-about-aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices" + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on Github", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" }, "notifications": { "display-name-changed-permanently": "Display name is changed permanently.", diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index d09d5c0..5f2730b 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -2,10 +2,10 @@ class Localization { constructor() { Localization.defaultLocale = "en"; Localization.supportedLocales = ["en"]; - Localization.translations = {}; + Localization.defaultTranslations = {}; - const initialLocale = Localization.supportedOrDefault(Localization.browserLocales()); + const initialLocale = Localization.supportedOrDefault(navigator.languages); Localization.setLocale(initialLocale) .then(_ => { @@ -21,25 +21,21 @@ class Localization { return locales.find(Localization.isSupported) || Localization.defaultLocale; } - static browserLocales() { - return navigator.languages.map(locale => - locale.split("-")[0] - ); - } - static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; + const isFirstTranslation = !Localization.locale + + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); + const newTranslations = await Localization.fetchTranslationsFor(newLocale); if(!newTranslations) return false; - const firstTranslation = !Localization.locale - Localization.locale = newLocale; Localization.translations = newTranslations; - if (firstTranslation) { + if (isFirstTranslation) { Events.fire("translation-loaded"); } } @@ -65,30 +61,43 @@ class Localization { for (let i in attrs) { let attr = attrs[i]; if (attr === "text") { - element.innerText = await Localization.getTranslation(key); + element.innerText = Localization.getTranslation(key); } else { - element.attr = await Localization.getTranslation(key, attr); + element.attr = Localization.getTranslation(key, attr); } } } - static getTranslation(key, attr, data) { + static getTranslation(key, attr, data, useDefault=false) { const keys = key.split("."); - let translationCandidates = Localization.translations; + let translationCandidates = useDefault + ? Localization.defaultTranslations + : Localization.translations; for (let i=0; i Date: Sun, 30 Jul 2023 17:55:35 +0200 Subject: [PATCH 137/608] Add Weblate and a mention of translations to the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0602ff0..f2a093b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop) * Lots of stability fixes (Thanks [@MWY001](https://github.com/MWY001) [@skiby7](https://github.com/skiby7) and [@willstott101](https://github.com/willstott101)) * To host PairDrop on your local network (e.g. on Raspberry Pi): [All peers connected with private IPs are discoverable by each other](https://github.com/RobinLinus/snapdrop/pull/558) * When hosting PairDrop yourself you can [set your own STUN/TURN servers](/docs/host-your-own.md#specify-stunturn-servers) +* Built-in translations ## Screenshots
@@ -82,6 +83,7 @@ Developed based on [Snapdrop](https://github.com/RobinLinus/snapdrop) * [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 +* [Weblate](https://weblate.org/) Web based localization tool Have any questions? Read our [FAQ](/docs/faq.md). From 8869c3c27e68fb3e0ea6cac28f85888923775df7 Mon Sep 17 00:00:00 2001 From: zhongbing Date: Sun, 6 Aug 2023 00:47:01 +0800 Subject: [PATCH 138/608] revise the command line tool --- pairdrop-cli/pairdrop | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/pairdrop-cli/pairdrop b/pairdrop-cli/pairdrop index 9172412..b5e80bd 100644 --- a/pairdrop-cli/pairdrop +++ b/pairdrop-cli/pairdrop @@ -38,7 +38,10 @@ openPairDrop() else xdg-open "$url" fi + + exit + } setOs() @@ -98,13 +101,19 @@ sendFiles() [[ -a "$zipPath" ]] && echo "Cannot overwrite $zipPath. Please remove first." && exit if [[ -d $path ]]; then - zipPathTemp="temp_${zipPath}" + zipPathTemp="${path}_pairdrop_temp.zip" [[ -a "$zipPathTemp" ]] && echo "Cannot overwrite $zipPathTemp. Please remove first." && exit echo "Processing directory..." # Create zip files temporarily to send directory - zip -q -b /tmp/ -r "$zipPath" "$path" - zip -q -b /tmp/ "$zipPathTemp" "$zipPath" + if [[ $OS == "Windows" ]];then + powershell.exe -Command "Compress-Archive -Path ${path} -DestinationPath ${zipPath}" + echo "Compress-Archive -Path ${zipPath} -DestinationPath ${zipPathTemp}" + powershell.exe -Command "Compress-Archive -Path ${zipPath} -DestinationPath ${zipPathTemp}" + else + zip -q -b /tmp/ -r "$zipPath" "$path" + zip -q -b /tmp/ "$zipPathTemp" "$zipPath" + fi if [[ $OS == "Mac" ]];then hash=$(base64 -i "$zipPathTemp") @@ -118,8 +127,12 @@ sendFiles() echo "Processing file..." # Create zip file temporarily to send file - zip -q -b /tmp/ "$zipPath" "$path" + if [[ $OS == "Windows" ]];then + powershell.exe -Command "Compress-Archive -Path ${path} -DestinationPath ${zipPath} -CompressionLevel Optimal" + else + zip -q -b /tmp/ "$zipPath" "$path" + fi if [[ $OS == "Mac" ]];then hash=$(base64 -i "$zipPath") else @@ -142,6 +155,7 @@ sendFiles() hash= fi + openPairDrop exit } From 395c3e00a4c23320ced4849ceabc12b220cef80e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 04:24:11 +0000 Subject: [PATCH 139/608] Bump express-rate-limit from 6.8.0 to 6.9.0 Bumps [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) from 6.8.0 to 6.9.0. - [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases) - [Changelog](https://github.com/express-rate-limit/express-rate-limit/blob/main/changelog.md) - [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v6.8.0...v6.9.0) --- updated-dependencies: - dependency-name: express-rate-limit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index b96bb32..d5b6737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", - "express-rate-limit": "^6.8.0", + "express-rate-limit": "^6.9.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", "ws": "^8.13.0" @@ -204,9 +204,9 @@ } }, "node_modules/express-rate-limit": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz", - "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.9.0.tgz", + "integrity": "sha512-AnISR3V8qy4gpKM62/TzYdoFO9NV84fBx0POXzTryHU/qGUJBWuVGd+JhbvtVmKBv37t8/afmqdnv16xWoQxag==", "engines": { "node": ">= 14.0.0" }, @@ -801,9 +801,9 @@ } }, "express-rate-limit": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.8.0.tgz", - "integrity": "sha512-yVeDWczkh8qgo9INJB1tT4j7LFu+n6ei/oqSMsqpsUIGYjTM+gk+Q3wv19TMUdo8chvus8XohAuOhG7RYRM9ZQ==", + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.9.0.tgz", + "integrity": "sha512-AnISR3V8qy4gpKM62/TzYdoFO9NV84fBx0POXzTryHU/qGUJBWuVGd+JhbvtVmKBv37t8/afmqdnv16xWoQxag==", "requires": {} }, "finalhandler": { diff --git a/package.json b/package.json index 0b8860a..015e006 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "express": "^4.18.2", - "express-rate-limit": "^6.8.0", + "express-rate-limit": "^6.9.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", "ws": "^8.13.0" From 43824d0de20d1fecf6525ff5e79b99a429b8936a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Thu, 10 Aug 2023 17:09:51 +0200 Subject: [PATCH 140/608] increase version to v1.7.7 --- package-lock.json | 4 ++-- package.json | 2 +- public/index.html | 2 +- public/service-worker.js | 2 +- public_included_ws_fallback/index.html | 2 +- public_included_ws_fallback/service-worker.js | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5b6737..6ea5f15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.7.6", + "version": "1.7.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.7.6", + "version": "1.7.7", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 015e006..baca71c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.7.6", + "version": "1.7.7", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 755673a..ab0c9a2 100644 --- a/public/index.html +++ b/public/index.html @@ -278,7 +278,7 @@

PairDrop

-
v1.7.6
+
v1.7.7
The easiest way to transfer files across devices
diff --git a/public/service-worker.js b/public/service-worker.js index b409118..88f2a13 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.6'; +const cacheVersion = 'v1.7.7'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 6beae65..838d8d2 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -281,7 +281,7 @@

PairDrop

-
v1.7.6
+
v1.7.7
The easiest way to transfer files across devices
diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 78249b2..51c320d 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.7.6'; +const cacheVersion = 'v1.7.7'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', From 69f1688dfe7ae6c703b71e91e22fc2f112f241f4 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 11 Aug 2023 13:11:51 +0200 Subject: [PATCH 141/608] Update bug-report.md --- .github/ISSUE_TEMPLATE/bug-report.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 39a6624..f5aa0fb 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,8 +1,8 @@ --- name: Bug Report about: Create a report to help us improve. Please check the FAQ first. -title: 'Bug:/Enhancement:/Feature Request: ' -labels: '' +title: '[Bug] ' +labels: 'bug' assignees: '' --- @@ -34,12 +34,17 @@ If applicable, add screenshots to help explain your problem. - Browser [e.g. stock browser, safari] - Version [e.g. 22] -**Self-Hosted** +**Bug occurs on official PairDrop instance https://pairdrop.net/** +No | Yes +Version: v1.7.7 + +**Bug occurs on self-hosted PairDrop instance** No | Yes **Self-Hosted Setup** Proxy: Nginx | Apache2 Deployment: docker run | docker-compose | npm run start:prod +Version: v1.7.7 **Additional context** Add any other context about the problem here. From f95181c057e75722ef3de61ffe083edb52d663a0 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 11 Aug 2023 13:20:38 +0200 Subject: [PATCH 142/608] Create enhancement issue template --- .github/ISSUE_TEMPLATE/enhancement | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/enhancement diff --git a/.github/ISSUE_TEMPLATE/enhancement b/.github/ISSUE_TEMPLATE/enhancement new file mode 100644 index 0000000..ba85fef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement @@ -0,0 +1,20 @@ +--- +name: Enhancement +about: Enhancements and feature requests are always welcome. See discussions regarding central topics. +title: '[Enhancement] ' +labels: 'enhancement' +assignees: '' + +--- + +**What problem is solved by the new feature** +What's the motivation for this topic + +**Describe the feature** +A clear and concise description of what the new feature/enhancement is. + +**Drafts** +Screenshots of Draw.io graph or drawn sketch. + +**Additional context** +Add any other context here. From e0210b030793e91e3c75763d249c74856c93b07a Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 11 Aug 2023 13:21:10 +0200 Subject: [PATCH 143/608] Rename enhancement to enhancement.md --- .github/ISSUE_TEMPLATE/{enhancement => enhancement.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/ISSUE_TEMPLATE/{enhancement => enhancement.md} (100%) diff --git a/.github/ISSUE_TEMPLATE/enhancement b/.github/ISSUE_TEMPLATE/enhancement.md similarity index 100% rename from .github/ISSUE_TEMPLATE/enhancement rename to .github/ISSUE_TEMPLATE/enhancement.md From efeff843202cad67cd79d3e960bd90736cdd8c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 11 Aug 2023 18:59:26 +0000 Subject: [PATCH 144/608] "HEALTHCHECK" --- docs/docker-swarm-usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docker-swarm-usage.md b/docs/docker-swarm-usage.md index 218ae82..4d1abf2 100644 --- a/docs/docker-swarm-usage.md +++ b/docs/docker-swarm-usage.md @@ -14,7 +14,7 @@ The [Docker Image](../Dockerfile) includes a health check with the following opt ``` --timeout=10s ``` -> Specifies the amount of time to wait for a response from the \"healthcheck\" command. \ +> Specifies the amount of time to wait for a response from the \"HEALTHCHECK\" command. \ > If the response does not arrive within 10 seconds, the health check fails.
@@ -39,7 +39,7 @@ This command will attempt to connect to `http://localhost:3000/` \ and if it fails it will exit with a status code of `1`. \ If this command returns a status code other than `0`, the health check fails. -Overall, this HEALTHCHECK instruction is defining a health check process \ +Overall, this \"HEALTHCHECK\" instruction is defining a health check process \ that runs every 30 seconds, and waits up to 10 seconds for a response, \ begins 5 seconds after the container is started, and retries up to 3 times. \ The health check attempts to connect to http://localhost:3000/ \ From 22bf1be2b711c3be31146fd4d716177addd68dfb Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Mon, 28 Aug 2023 13:41:12 +0200 Subject: [PATCH 145/608] Add TLS requirement to host-your-own.md --- docs/host-your-own.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index 2a63446..e0e33f7 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -8,6 +8,10 @@ The easiest way to get PairDrop up and running is by using Docker. > Follow [this guide](https://gabrieltanner.org/blog/turn-server/) to either install coturn directly on your system (Step 1) \ > or deploy it via docker-compose (Step 5). +> PairDrop via HTTPS +> +> On some browsers PairDrop must be served over TLS in order for some feautures to work properly. These may include copying an incoming message via the 'copy' button, installing PairDrop as PWA, persistent pairing of devices and changing of the display name, and notifications. Naturally, this is also recommended to increase security. + ## Deployment with Docker ### Docker Image from Docker Hub From 161bd2be84f1f03b713a1c6221aa30021cb9fe52 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 29 Aug 2023 01:49:09 +0200 Subject: [PATCH 146/608] rename language files to map language codes properly --- public/lang/{nb-NO.json => nb.json} | 0 public/lang/{zh-Hans.json => zh-CN.json} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename public/lang/{nb-NO.json => nb.json} (100%) rename public/lang/{zh-Hans.json => zh-CN.json} (100%) diff --git a/public/lang/nb-NO.json b/public/lang/nb.json similarity index 100% rename from public/lang/nb-NO.json rename to public/lang/nb.json diff --git a/public/lang/zh-Hans.json b/public/lang/zh-CN.json similarity index 100% rename from public/lang/zh-Hans.json rename to public/lang/zh-CN.json From 72f0aff60e541c273d3d71c905c597bb89b5a82e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 29 Aug 2023 01:49:32 +0200 Subject: [PATCH 147/608] copy lang files to ws_fallback version --- public_included_ws_fallback/lang/de.json | 1 + public_included_ws_fallback/lang/en.json | 276 ++++++++++---------- public_included_ws_fallback/lang/nb.json | 140 ++++++++++ public_included_ws_fallback/lang/ru.json | 141 ++++++++++ public_included_ws_fallback/lang/tr.json | 25 ++ public_included_ws_fallback/lang/zh-CN.json | 140 ++++++++++ 6 files changed, 585 insertions(+), 138 deletions(-) create mode 100644 public_included_ws_fallback/lang/de.json create mode 100644 public_included_ws_fallback/lang/nb.json create mode 100644 public_included_ws_fallback/lang/ru.json create mode 100644 public_included_ws_fallback/lang/tr.json create mode 100644 public_included_ws_fallback/lang/zh-CN.json diff --git a/public_included_ws_fallback/lang/de.json b/public_included_ws_fallback/lang/de.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/public_included_ws_fallback/lang/de.json @@ -0,0 +1 @@ +{} diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index 8ad7b7c..ff8294d 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -1,140 +1,140 @@ { - "header": { - "about_title": "About PairDrop", - "about_aria-label": "Open About PairDrop", - "theme-auto_title": "Adapt Theme to System", - "theme-light_title": "Always Use Light-Theme", - "theme-dark_title": "Always Use Dark-Theme", - "notification_title": "Enable Notifications", - "install_title": "Install PairDrop", - "pair-device_title": "Pair Device", - "edit-paired-devices_title": "Edit Paired Devices", - "cancel-paste-mode": "Done" - }, - "instructions": { - "no-peers_data-drop-bg": "Release to select recipient", - "no-peers-title": "Open PairDrop on other devices to send files", - "no-peers-subtitle": "Pair devices to be discoverable on other networks", - "x-instructions_desktop": "Click to send files or right click to send a message", - "x-instructions_mobile": "Tap to send files or long tap to send a message", - "x-instructions_data-drop-peer": "Release to send to peer", - "x-instructions_data-drop-bg": "Release to select recipient", - "click-to-send": "Click to send", - "tap-to-send": "Tap to send" - }, - "footer": { - "known-as": "You are known as:", - "display-name_placeholder": "Loading...", - "display-name_title": "Edit your device name permanently", - "discovery-everyone": "You can be discovered by everyone", - "on-this-network": "on this network", - "and-by": "and by", - "paired-devices": "paired devices", - "traffic": "Traffic is", - "routed": "routed through the server", - "webrtc": "if WebRTC is not available." - }, - "dialogs": { - "activate-paste-mode-base": "Open PairDrop on other devices to send", - "activate-paste-mode-and-other-files": "and {{count}} other files", - "activate-paste-mode-activate-paste-mode-shared-text": "shared text", - "pair-devices-title": "Pair Devices", - "input-key-on-this-device": "Input this key on another device", - "scan-qr-code": "or scan the QR-Code.", - "enter-key-from-another-device": "Enter key from another device to continue.", - "pair": "Pair", - "cancel": "Cancel", - "edit-paired-devices-title": "Edit Paired Devices", - "paired-devices-wrapper_data-empty": "No paired devices.", - "auto-accept-instructions-1": "Activate", - "auto-accept": "auto-accept", - "auto-accept-instructions-2": "to automatically accept all files sent from that device.", - "close": "Close", - "would-like-to-share": "would like to share", - "accept": "Accept", - "decline": "Decline", - "has-sent": "has sent:", - "share": "Share", - "download": "Download", - "send-message-title": "Send Message", - "send-message-to": "Send a Message to", - "send": "Send", - "receive-text-title": "Message Received", - "copy": "Copy", - "base64-processing": "Processing...", - "base64-tap-to-paste": "Tap here to paste {{type}}", - "base64-paste-to-send": "Paste here to send {{type}}", - "base64-text": "text", - "base64-files": "files", - "file-other-description-image": "and 1 other image", - "file-other-description-file": "and 1 other file", - "file-other-description-image-plural": "and {{count}} other images", - "file-other-description-file-plural": "and {{count}} other files", - "title-image": "Image", - "title-file": "File", - "title-image-plural": "Images", - "title-file-plural": "Files", - "receive-title": "{{descriptor}} Received", - "download-again": "Download again" - }, - "about": { - "close-about_aria-label": "Close About PairDrop", - "claim": "The easiest way to transfer files across devices", - "github_title": "PairDrop on Github", - "buy-me-a-coffee_title": "Buy me a coffee!", - "tweet_title": "Tweet about PairDrop", - "faq_title": "Frequently asked questions" - }, - "notifications": { - "display-name-changed-permanently": "Display name is changed permanently.", - "display-name-changed-temporarily": "Display name is changed only for this session.", - "display-name-random-again": "Display name is randomly generated again.", - "download-successful": "{{descriptor}} downloaded successfully", - "pairing-tabs-error": "Pairing of two browser tabs is not possible.", - "pairing-success": "Devices paired successfully.", - "pairing-not-persistent": "Paired devices are not persistent.", - "pairing-key-invalid": "Key not valid", - "pairing-key-invalidated": "Key {{key}} invalidated.", - "pairing-cleared": "All Devices unpaired.", - "copied-to-clipboard": "Copied to clipboard", - "text-content-incorrect": "Text content is incorrect.", - "file-content-incorrect": "File content is incorrect.", - "clipboard-content-incorrect": "Clipboard content is incorrect.", - "notifications-enabled": "Notifications enabled.", - "link-received": "Link received by {{name}} - Click to open", - "message-received": "Message received by {{name}} - Click to copy", - "click-to-download": "Click to download", - "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", - "click-to-show": "Click to show", - "copied-text": "Copied text to clipboard", - "copied-text-error": "Writing to clipboard failed. Copy manually!", - "offline": "You are offline", - "online": "You are back online", - "connected": "Connected.", - "online-requirement": "You need to be online to pair devices.", - "connecting": "Connecting...", - "files-incorrect": "Files are incorrect.", - "file-transfer-completed": "File transfer completed.", - "ios-memory-limit": "Sending files to iOS is only possible up to 200MB at once", - "message-transfer-completed": "Message transfer completed.", - "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", - "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", - "selected-peer-left": "Selected peer left." - }, - "document-titles": { - "file-received": "File Received", - "file-received-plural": "{{count}} Files Received", - "file-transfer-requested": "File Transfer Requested", - "message-received": "Message Received", - "message-received-plural": "{{count}} Messages Received" - }, - "peer-ui": { - "click-to-send-paste-mode": "Click to send {{descriptor}}", - "click-to-send": "Click to send files or right click to send a message", - "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", - "preparing": "Preparing...", - "waiting": "Waiting...", - "processing": "Processing...", - "transferring": "Transferring..." - } + "header": { + "about_title": "About PairDrop", + "about_aria-label": "Open About PairDrop", + "theme-auto_title": "Adapt Theme to System", + "theme-light_title": "Always Use Light-Theme", + "theme-dark_title": "Always Use Dark-Theme", + "notification_title": "Enable Notifications", + "install_title": "Install PairDrop", + "pair-device_title": "Pair Device", + "edit-paired-devices_title": "Edit Paired Devices", + "cancel-paste-mode": "Done" + }, + "instructions": { + "no-peers_data-drop-bg": "Release to select recipient", + "no-peers-title": "Open PairDrop on other devices to send files", + "no-peers-subtitle": "Pair devices to be discoverable on other networks", + "x-instructions_desktop": "Click to send files or right click to send a message", + "x-instructions_mobile": "Tap to send files or long tap to send a message", + "x-instructions_data-drop-peer": "Release to send to peer", + "x-instructions_data-drop-bg": "Release to select recipient", + "click-to-send": "Click to send", + "tap-to-send": "Tap to send" + }, + "footer": { + "known-as": "You are known as:", + "display-name_placeholder": "Loading…", + "display-name_title": "Edit your device name permanently", + "discovery-everyone": "You can be discovered by everyone", + "on-this-network": "on this network", + "and-by": "and by", + "paired-devices": "paired devices", + "traffic": "Traffic is", + "routed": "routed through the server", + "webrtc": "if WebRTC is not available." + }, + "dialogs": { + "activate-paste-mode-base": "Open PairDrop on other devices to send", + "activate-paste-mode-and-other-files": "and {{count}} other files", + "activate-paste-mode-activate-paste-mode-shared-text": "shared text", + "pair-devices-title": "Pair Devices", + "input-key-on-this-device": "Input this key on another device", + "scan-qr-code": "or scan the QR-code.", + "enter-key-from-another-device": "Enter key from another device to continue.", + "pair": "Pair", + "cancel": "Cancel", + "edit-paired-devices-title": "Edit Paired Devices", + "paired-devices-wrapper_data-empty": "No paired devices.", + "auto-accept-instructions-1": "Activate", + "auto-accept": "auto-accept", + "auto-accept-instructions-2": "to automatically accept all files sent from that device.", + "close": "Close", + "would-like-to-share": "would like to share", + "accept": "Accept", + "decline": "Decline", + "has-sent": "has sent:", + "share": "Share", + "download": "Download", + "send-message-title": "Send Message", + "send-message-to": "Send a Message to", + "send": "Send", + "receive-text-title": "Message Received", + "copy": "Copy", + "base64-processing": "Processing...", + "base64-tap-to-paste": "Tap here to paste {{type}}", + "base64-paste-to-send": "Paste here to send {{type}}", + "base64-text": "text", + "base64-files": "files", + "file-other-description-image": "and 1 other image", + "file-other-description-file": "and 1 other file", + "file-other-description-image-plural": "and {{count}} other images", + "file-other-description-file-plural": "and {{count}} other files", + "title-image": "Image", + "title-file": "File", + "title-image-plural": "Images", + "title-file-plural": "Files", + "receive-title": "{{descriptor}} Received", + "download-again": "Download again" + }, + "about": { + "close-about_aria-label": "Close About PairDrop", + "claim": "The easiest way to transfer files across devices", + "github_title": "PairDrop on GitHub", + "buy-me-a-coffee_title": "Buy me a coffee!", + "tweet_title": "Tweet about PairDrop", + "faq_title": "Frequently asked questions" + }, + "notifications": { + "display-name-changed-permanently": "Display name is changed permanently.", + "display-name-changed-temporarily": "Display name is changed only for this session.", + "display-name-random-again": "Display name is randomly generated again.", + "download-successful": "{{descriptor}} downloaded", + "pairing-tabs-error": "Pairing two web browser tabs is impossible.", + "pairing-success": "Devices paired.", + "pairing-not-persistent": "Paired devices are not persistent.", + "pairing-key-invalid": "Invalid key", + "pairing-key-invalidated": "Key {{key}} invalidated.", + "pairing-cleared": "All Devices unpaired.", + "copied-to-clipboard": "Copied to clipboard", + "text-content-incorrect": "Text content is incorrect.", + "file-content-incorrect": "File content is incorrect.", + "clipboard-content-incorrect": "Clipboard content is incorrect.", + "notifications-enabled": "Notifications enabled.", + "link-received": "Link received by {{name}} - Click to open", + "message-received": "Message received by {{name}} - Click to copy", + "click-to-download": "Click to download", + "request-title": "{{name}} would like to transfer {{count}} {{descriptor}}", + "click-to-show": "Click to show", + "copied-text": "Copied text to clipboard", + "copied-text-error": "Writing to clipboard failed. Copy manually!", + "offline": "You are offline", + "online": "You are back online", + "connected": "Connected.", + "online-requirement": "You need to be online to pair devices.", + "connecting": "Connecting…", + "files-incorrect": "Files are incorrect.", + "file-transfer-completed": "File transfer completed.", + "ios-memory-limit": "Sending files to iOS is only possible up to 200 MB at once", + "message-transfer-completed": "Message transfer completed.", + "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close?", + "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", + "selected-peer-left": "Selected peer left." + }, + "document-titles": { + "file-received": "File Received", + "file-received-plural": "{{count}} Files Received", + "file-transfer-requested": "File Transfer Requested", + "message-received": "Message Received", + "message-received-plural": "{{count}} Messages Received" + }, + "peer-ui": { + "click-to-send-paste-mode": "Click to send {{descriptor}}", + "click-to-send": "Click to send files or right click to send a message", + "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", + "preparing": "Preparing…", + "waiting": "Waiting…", + "processing": "Processing…", + "transferring": "Transferring…" + } } diff --git a/public_included_ws_fallback/lang/nb.json b/public_included_ws_fallback/lang/nb.json new file mode 100644 index 0000000..b11b664 --- /dev/null +++ b/public_included_ws_fallback/lang/nb.json @@ -0,0 +1,140 @@ +{ + "header": { + "edit-paired-devices_title": "Rediger sammenkoblede enheter", + "about_title": "Om PairDrop", + "about_aria-label": "Åpne «Om PairDrop»", + "theme-auto_title": "Juster drakt til system", + "theme-light_title": "Alltid bruk lys drakt", + "theme-dark_title": "Alltid bruk mørk drakt", + "notification_title": "Skru på merknader", + "cancel-paste-mode": "Ferdig", + "install_title": "Installer PairDrop", + "pair-device_title": "Sammenkoble enhet" + }, + "footer": { + "discovery-everyone": "Du kan oppdages av alle", + "and-by": "og av", + "webrtc": "hvis WebRTC ikke er tilgjengelig.", + "display-name_placeholder": "Laster inn …", + "display-name_title": "Rediger det vedvarende enhetsnavnet ditt", + "traffic": "Trafikken", + "on-this-network": "på dette nettverket", + "known-as": "Du er kjent som:", + "paired-devices": "sammenkoblede enheter", + "routed": "Sendes gjennom tjeneren" + }, + "instructions": { + "x-instructions_desktop": "Klikk for å sende filer, eller høyreklikk for å sende en melding", + "x-instructions_mobile": "Trykk for å sende filer, eller lang-trykk for å sende en melding", + "x-instructions_data-drop-bg": "Slipp for å velge mottager", + "click-to-send": "Klikk for å sende", + "no-peers_data-drop-bg": "Slipp for å velge mottager", + "no-peers-title": "Åpne PairDrop på andre enheter for å sende filer", + "no-peers-subtitle": "Sammenkoble enheter for å kunne oppdages på andre nettverk", + "x-instructions_data-drop-peer": "Slipp for å sende til likemann", + "tap-to-send": "Trykk for å sende" + }, + "dialogs": { + "input-key-on-this-device": "Skriv inn denne nøkkelen på en annen enhet", + "pair-devices-title": "Sammenkoble enheter", + "would-like-to-share": "ønsker å dele", + "auto-accept-instructions-2": "for å godkjenne alle filer sendt fra den enheten automatisk.", + "paired-devices-wrapper_data-empty": "Ingen sammenkoblede enheter", + "enter-key-from-another-device": "Skriv inn nøkkel fra en annen enhet for å fortsette.", + "edit-paired-devices-title": "Rediger sammenkoblede enheter", + "accept": "Godta", + "has-sent": "har sendt:", + "base64-paste-to-send": "Trykk her for å sende {{type}}", + "base64-text": "tekst", + "base64-files": "filer", + "file-other-description-image-plural": "og {{count}} andre bilder", + "receive-title": "{{descriptor}} mottatt", + "send-message-title": "Send melding", + "base64-processing": "Behandler …", + "close": "Lukk", + "decline": "Avslå", + "download": "Last ned", + "copy": "Kopier", + "pair": "Sammenkoble", + "cancel": "Avbryt", + "scan-qr-code": "eller skann QR-koden.", + "auto-accept-instructions-1": "Aktiver", + "receive-text-title": "Melding mottatt", + "auto-accept": "auto-godkjenn", + "share": "Del", + "send-message-to": "Send en melding til", + "send": "Send", + "base64-tap-to-paste": "Trykk her for å lime inn {{type]]", + "file-other-description-image": "og ett annet bilde", + "file-other-description-file-plural": "og {{count}} andre filer", + "title-file-plural": "Filer", + "download-again": "Last ned igjen", + "file-other-description-file": "og én annen fil", + "title-image": "Bilde", + "title-file": "Fil", + "title-image-plural": "Bilder", + "activate-paste-mode-base": "Åpne PairDrop på andre enheter for å sende", + "activate-paste-mode-and-other-files": "og {{count}} andre filer", + "activate-paste-mode-activate-paste-mode-shared-text": "delt tekst" + }, + "about": { + "close-about_aria-label": "Lukk «Om PairDrop»", + "faq_title": "Ofte stilte spørsmål", + "claim": "Den enkleste måten å overføre filer mellom enheter", + "buy-me-a-coffee_title": "Spander drikke.", + "tweet_title": "Tvitre om PairDrop", + "github_title": "PairDrop på GitHub" + }, + "notifications": { + "copied-to-clipboard": "Kopiert til utklippstavlen", + "pairing-tabs-error": "Sammenkobling av to nettleserfaner er ikke mulig.", + "notifications-enabled": "Merknader påskrudd.", + "click-to-show": "Klikk for å vise", + "copied-text": "Tekst kopiert til utklippstavlen", + "connected": "Tilkoblet.", + "online": "Du er tilbake på nett", + "file-transfer-completed": "Filoverføring utført.", + "selected-peer-left": "Valgt likemann dro.", + "pairing-key-invalid": "Ugyldig nøkkel", + "connecting": "Kobler til …", + "pairing-not-persistent": "Sammenkoblede enheter er ikke vedvarende.", + "offline": "Du er frakoblet", + "online-requirement": "Du må være på nett for å koble sammen enheter.", + "display-name-random-again": "Visningsnavnet er tilfeldig generert igjen.", + "display-name-changed-permanently": "Visningsnavnet er endret for godt.", + "display-name-changed-temporarily": "Visningsnavnet er endret kun for denne økten.", + "text-content-incorrect": "Tekstinnholdet er uriktig.", + "file-content-incorrect": "Filinnholdet er uriktig.", + "click-to-download": "Klikk for å laste ned", + "message-transfer-completed": "Meldingsoverføring utført.", + "download-successful": "{{descriptor}} nedlastet", + "pairing-success": "Enheter sammenkoblet.", + "pairing-cleared": "Sammenkobling av alle enheter opphevet.", + "pairing-key-invalidated": "Nøkkel {{key}} ugyldiggjort.", + "copied-text-error": "Kunne ikke legge innhold i utklkippstavlen. Kopier manuelt.", + "clipboard-content-incorrect": "Utklippstavleinnholdet er uriktig.", + "link-received": "Lenke mottatt av {{name}}. Klikk for å åpne.", + "request-title": "{{name}} ønsker å overføre {{count}} {{descriptor}}", + "message-received": "Melding mottatt av {{name}}. Klikk for å åpne.", + "files-incorrect": "Filene er uriktige.", + "ios-memory-limit": "Forsendelse av filer til iOS er kun mulig opptil 200 MB av gangen.", + "unfinished-transfers-warning": "Lukk med ufullførte overføringer?", + "rate-limit-join-key": "Forsøksgrense overskredet. Vent 10 sek. og prøv igjen." + }, + "document-titles": { + "file-received": "Fil mottatt", + "file-received-plural": "{{count}} filer mottatt", + "message-received": "Melding mottatt", + "file-transfer-requested": "Filoverføring forespurt", + "message-received-plural": "{{count}} meldinger mottatt" + }, + "peer-ui": { + "preparing": "Forbereder …", + "waiting": "Venter", + "processing": "Behandler …", + "transferring": "Overfører …", + "click-to-send": "Klikk for å sende filer, eller høyreklikk for å sende en melding", + "click-to-send-paste-mode": "Klikk for å sende {{descriptor}}", + "connection-hash": "Sammenlign dette sikkerhetsnummeret på begge enhetene for å bekrefte ende-til-ende -krypteringen." + } +} diff --git a/public_included_ws_fallback/lang/ru.json b/public_included_ws_fallback/lang/ru.json new file mode 100644 index 0000000..8617ec2 --- /dev/null +++ b/public_included_ws_fallback/lang/ru.json @@ -0,0 +1,141 @@ +{ + "header": { + "about_aria-label": "Открыть страницу \"О сервисе\"", + "pair-device_title": "Подключить устройство", + "install_title": "Установить PairDrop", + "cancel-paste-mode": "Выполнено", + "edit-paired-devices_title": "Редактировать сопряженные устройства", + "notification_title": "Включить уведомления", + "about_title": "О сервисе", + "theme-auto_title": "Адаптировать тему к системной", + "theme-dark_title": "Всегда использовать темную тему", + "theme-light_title": "Всегда использовать светлую тему" + }, + "instructions": { + "x-instructions_desktop": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение", + "no-peers_data-drop-bg": "Отпустите, чтобы выбрать получателя", + "click-to-send": "Нажмите, чтобы отправить", + "x-instructions_data-drop-bg": "Отпустите, чтобы выбрать получателя", + "tap-to-send": "Прикоснитесь, чтобы отправить", + "x-instructions_data-drop-peer": "Отпустите, чтобы послать узлу", + "x-instructions_mobile": "Прикоснитесь коротко, чтобы отправить файлы, или долго, чтобы отправить сообщение", + "no-peers-title": "Откройте PairDrop на других устройствах, чтобы отправить файлы", + "no-peers-subtitle": "Сопрягите устройства из разных сетей." + }, + "footer": { + "discovery-everyone": "О вас может узнать каждый", + "display-name_placeholder": "Загрузка…", + "routed": "направляется через сервер", + "webrtc": ", если WebRTC недоступен.", + "traffic": "Трафик", + "and-by": "и", + "paired-devices": "сопряженные устройства", + "known-as": "Вы известны под именем:", + "on-this-network": "в этой сети", + "display-name_title": "Изменить имя вашего устройства навсегда" + }, + "dialogs": { + "activate-paste-mode-and-other-files": "и {{count}} других файлов", + "activate-paste-mode-base": "Откройте PairDrop на других устройствах, чтобы отправить", + "activate-paste-mode-activate-paste-mode-shared-text": "общий текст", + "edit-paired-devices-title": "Редактировать сопряженные устройства", + "auto-accept": "автоприем", + "close": "Закрыть", + "decline": "Отклонить", + "share": "Поделиться", + "would-like-to-share": "хотел бы поделиться", + "has-sent": "отправил:", + "paired-devices-wrapper_data-empty": "Нет сопряженных устройств.", + "download": "Скачать", + "receive-text-title": "Сообщение получено", + "send": "Отправить", + "send-message-to": "Отправить сообщение", + "send-message-title": "Отправить сообщение", + "copy": "Копировать", + "base64-files": "файлы", + "base64-paste-to-send": "Вставьте здесь, чтобы отправить {{type}}", + "base64-processing": "Обработка...", + "base64-tap-to-paste": "Прикоснитесь здесь, чтобы вставить {{type}}", + "base64-text": "текст", + "title-file": "Файл", + "title-file-plural": "Файлы", + "title-image": "Изображение", + "title-image-plural": "Изображения", + "download-again": "Скачать еще раз", + "auto-accept-instructions-2": "чтобы автоматически принимать все файлы, отправленные с этого устройства.", + "enter-key-from-another-device": "Для продолжения введите ключ с другого устройства.", + "pair-devices-title": "Сопрягите устройства", + "input-key-on-this-device": "Введите этот ключ на другом устройстве", + "scan-qr-code": "или отсканируйте QR-код.", + "cancel": "Отменить", + "pair": "Подключить", + "accept": "Принять", + "auto-accept-instructions-1": "Активировать", + "file-other-description-file": "и 1 другой файл", + "file-other-description-image-plural": "и {{count}} других изображений", + "file-other-description-image": "и 1 другое изображение", + "file-other-description-file-plural": "и {{count}} других файлов", + "receive-title": "{{descriptor}} получен" + }, + "about": { + "close-about-aria-label": "Закрыть страницу \"О сервисе\"", + "claim": "Самый простой способ передачи файлов между устройствами", + "close-about_aria-label": "Закрыть страницу \"О сервисе\"", + "buy-me-a-coffee_title": "Купить мне кофе!", + "github_title": "PairDrop на GitHub", + "tweet_title": "Твит о PairDrop", + "faq_title": "Часто задаваемые вопросы" + }, + "notifications": { + "display-name-changed-permanently": "Отображаемое имя было изменено навсегда.", + "display-name-random-again": "Отображаемое имя сгенерировалось случайным образом снова.", + "pairing-success": "Устройства сопряжены.", + "pairing-tabs-error": "Сопряжение двух вкладок браузера невозможно.", + "copied-to-clipboard": "Скопировано в буфер обмена", + "pairing-not-persistent": "Сопряженные устройства непостоянны.", + "link-received": "Получена ссылка от {{name}} - нажмите, чтобы открыть", + "notifications-enabled": "Уведомления включены.", + "text-content-incorrect": "Содержание текста неверно.", + "message-received": "Получено сообщение от {{name}} - нажмите, чтобы скопировать", + "connected": "Подключено.", + "copied-text": "Текст скопирован в буфер обмена", + "online": "Вы снова в сети", + "offline": "Вы находитесь вне сети", + "online-requirement": "Для сопряжения устройств вам нужно быть в сети.", + "files-incorrect": "Файлы неверны.", + "message-transfer-completed": "Передача сообщения завершена.", + "ios-memory-limit": "Отправка файлов на iOS устройства возможна только до 200 МБ за один раз", + "selected-peer-left": "Выбранный узел вышел.", + "request-title": "{{name}} хотел бы передать {{count}} {{descriptor}}", + "rate-limit-join-key": "Достигнут предел скорости. Подождите 10 секунд и повторите попытку.", + "unfinished-transfers-warning": "Есть незавершенные передачи. Вы уверены, что хотите закрыть?", + "copied-text-error": "Запись в буфер обмена не удалась. Скопируйте вручную!", + "pairing-cleared": "Все устройства не сопряжены.", + "pairing-key-invalid": "Неверный ключ", + "pairing-key-invalidated": "Ключ {{key}} признан недействительным.", + "click-to-download": "Нажмите, чтобы скачать", + "clipboard-content-incorrect": "Содержание буфера обмена неверно.", + "click-to-show": "Нажмите, чтобы показать", + "connecting": "Подключение…", + "download-successful": "{{descriptor}} загружен", + "display-name-changed-temporarily": "Отображаемое имя было изменено только для этой сессии.", + "file-content-incorrect": "Содержимое файла неверно.", + "file-transfer-completed": "Передача файла завершена." + }, + "peer-ui": { + "click-to-send-paste-mode": "Нажмите, чтобы отправить {{descriptor}}", + "preparing": "Подготовка…", + "transferring": "Передача…", + "processing": "Обработка…", + "waiting": "Ожидание…", + "connection-hash": "Чтобы проверить безопасность сквозного шифрования, сравните этот номер безопасности на обоих устройствах", + "click-to-send": "Нажмите, чтобы отправить файлы, или щелкните правой кнопкой мыши, чтобы отправить сообщение" + }, + "document-titles": { + "file-received-plural": "{{count}} файлов получено", + "message-received-plural": "{{count}} сообщений получено", + "file-received": "Файл получен", + "file-transfer-requested": "Запрошена передача файлов", + "message-received": "Сообщение получено" + } +} diff --git a/public_included_ws_fallback/lang/tr.json b/public_included_ws_fallback/lang/tr.json new file mode 100644 index 0000000..783e25b --- /dev/null +++ b/public_included_ws_fallback/lang/tr.json @@ -0,0 +1,25 @@ +{ + "header": { + "about_title": "PairDrop Hakkında", + "about_aria-label": "PairDrop Hakkında Aç", + "theme-auto_title": "Temayı Sisteme Uyarla", + "theme-light_title": "Daima Açık Tema Kullan", + "theme-dark_title": "Daima Koyu Tema Kullan", + "notification_title": "Bildirimleri Etkinleştir", + "install_title": "PairDrop'u Yükle", + "pair-device_title": "Cihaz Eşleştir", + "edit-paired-devices_title": "Eşleştirilmiş Cihazları Düzenle", + "cancel-paste-mode": "Bitti" + }, + "instructions": { + "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" + }, + "footer": { + "display-name_placeholder": "Yükleniyor…", + "display-name_title": "Cihazının adını kalıcı olarak düzenle" + }, + "dialogs": { + "cancel": "İptal", + "edit-paired-devices-title": "Eşleştirilmiş Cihazları Düzenle" + } +} diff --git a/public_included_ws_fallback/lang/zh-CN.json b/public_included_ws_fallback/lang/zh-CN.json new file mode 100644 index 0000000..4191c7d --- /dev/null +++ b/public_included_ws_fallback/lang/zh-CN.json @@ -0,0 +1,140 @@ +{ + "header": { + "about_title": "关于 PairDrop", + "about_aria-label": "打开 关于 PairDrop", + "theme-light_title": "总是使用明亮主题", + "install_title": "安装 PairDrop", + "pair-device_title": "配对新设备", + "theme-auto_title": "主题适应系统", + "theme-dark_title": "总是使用暗黑主题", + "notification_title": "开启通知", + "edit-paired-devices_title": "管理已配对设备", + "cancel-paste-mode": "完成" + }, + "instructions": { + "x-instructions_data-drop-peer": "释放以发送到此设备", + "no-peers_data-drop-bg": "释放来选择接收者", + "no-peers-subtitle": "配对新设备使在其他网络上可见", + "no-peers-title": "在其他设备上打开 PairDrop 来发送文件", + "x-instructions_desktop": "点击以发送文件 或 右键来发送信息", + "x-instructions_mobile": "轻触以发送文件 或 长按来发送信息", + "x-instructions_data-drop-bg": "释放来选择接收者", + "click-to-send": "点击发送", + "tap-to-send": "轻触发送" + }, + "footer": { + "routed": "途径服务器", + "webrtc": "如果 WebRTC 不可用。", + "known-as": "你的名字是:", + "display-name_placeholder": "加载中…", + "and-by": "和", + "display-name_title": "长久修改你的设备名", + "discovery-everyone": "你对所有人可见", + "on-this-network": "在此网络上", + "paired-devices": "已配对的设备", + "traffic": "流量将" + }, + "dialogs": { + "activate-paste-mode-base": "在其他设备上打开 PairDrop 来发送", + "activate-paste-mode-and-other-files": "和 {{count}} 个其他的文件", + "activate-paste-mode-activate-paste-mode-shared-text": "分享文本", + "pair-devices-title": "配对新设备", + "input-key-on-this-device": "在另一个设备上输入这串数字", + "base64-text": "信息", + "enter-key-from-another-device": "输入从另一个设备上获得的数字以继续。", + "edit-paired-devices-title": "管理已配对的设备", + "pair": "配对", + "cancel": "取消", + "scan-qr-code": "或者 扫描二维码。", + "paired-devices-wrapper_data-empty": "无已配对设备。", + "auto-accept-instructions-1": "启用", + "auto-accept": "自动接收", + "decline": "拒绝", + "base64-processing": "处理中...", + "base64-tap-to-paste": "轻触此处粘贴{{type}}", + "base64-paste-to-send": "粘贴到此处以发送 {{type}}", + "auto-accept-instructions-2": "以无需同意而自动接收从那个设备上发送的所有文件。", + "would-like-to-share": "想要分享", + "accept": "接收", + "close": "关闭", + "share": "分享", + "download": "保存", + "send": "发送", + "receive-text-title": "收到信息", + "copy": "复制", + "send-message-title": "发送信息", + "send-message-to": "发了一条信息给", + "has-sent": "发送了:", + "base64-files": "文件", + "file-other-description-file": "和 1 个其他的文件", + "file-other-description-image": "和 1 个其他的图片", + "file-other-description-image-plural": "和 {{count}} 个其他的图片", + "file-other-description-file-plural": "和 {{count}} 个其他的文件", + "title-image-plural": "图片", + "receive-title": "收到 {{descriptor}}", + "title-image": "图片", + "title-file": "文件", + "title-file-plural": "文件", + "download-again": "再次保存" + }, + "about": { + "faq_title": "常见问题", + "close-about_aria-label": "关闭 关于 PairDrop", + "github_title": "PairDrop 在 GitHub 上开源", + "claim": "最简单的跨设备传输方案", + "buy-me-a-coffee_title": "帮我买杯咖啡!", + "tweet_title": "关于 PairDrop 的推特" + }, + "notifications": { + "display-name-changed-permanently": "展示的名字已经长久变更。", + "display-name-changed-temporarily": "展示的名字已经变更 仅在此会话中。", + "display-name-random-again": "展示的名字再次随机生成。", + "download-successful": "{{descriptor}} 已下载", + "pairing-tabs-error": "无法配对两个浏览器标签页。", + "pairing-success": "新设备已配对。", + "pairing-not-persistent": "配对的设备不是持久的。", + "pairing-key-invalid": "无效配对码", + "pairing-key-invalidated": "配对码 {{key}} 已失效。", + "text-content-incorrect": "文本内容不合法。", + "file-content-incorrect": "文件内容不合法。", + "clipboard-content-incorrect": "剪贴板内容不合法。", + "link-received": "收到来自 {{name}} 的链接 - 点击打开", + "message-received": "收到来自 {{name}} 的信息 - 点击复制", + "request-title": "{{name}} 想要发送 {{count}} 个 {{descriptor}}", + "click-to-show": "点击展示", + "copied-text": "复制到剪贴板", + "selected-peer-left": "选择的设备已离开。", + "pairing-cleared": "所有设备已解除配对。", + "copied-to-clipboard": "已复制到剪贴板", + "notifications-enabled": "通知已启用。", + "copied-text-error": "写入剪贴板失败。请手动复制!", + "click-to-download": "点击以保存", + "unfinished-transfers-warning": "还有未完成的传输任务。你确定要关闭吗?", + "message-transfer-completed": "信息传输已完成。", + "offline": "你未连接到网络", + "online": "你已重新连接到网络", + "connected": "已连接。", + "online-requirement": "你需要连接网络来配对新设备。", + "files-incorrect": "文件不合法。", + "file-transfer-completed": "文件传输已完成。", + "connecting": "连接中…", + "ios-memory-limit": "向 iOS 发送文件 一次最多只能发送 200 MB", + "rate-limit-join-key": "已达连接限制。请等待 10秒 后再试。" + }, + "document-titles": { + "message-received": "收到信息", + "message-received-plural": "收到 {{count}} 条信息", + "file-transfer-requested": "文件传输请求", + "file-received-plural": "收到 {{count}} 个文件", + "file-received": "收到文件" + }, + "peer-ui": { + "click-to-send-paste-mode": "点击发送 {{descriptor}}", + "click-to-send": "点击以发送文件 或 右键来发送信息", + "connection-hash": "若要验证端到端加密的安全性,请在两个设备上比较此安全编号", + "preparing": "准备中…", + "waiting": "请等待…", + "transferring": "传输中…", + "processing": "处理中…" + } +} From c2a746d69cb16f1e790a9a9031034fc3788dd436 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 29 Aug 2023 02:30:01 +0200 Subject: [PATCH 148/608] fix html attribute translation --- public/scripts/localization.js | 2 +- public_included_ws_fallback/scripts/localization.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index 1619676..8f30f26 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -63,7 +63,7 @@ class Localization { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { - element.attr = Localization.getTranslation(key, attr); + element.setAttribute(attr, Localization.getTranslation(key, attr)); } } diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index 1619676..8f30f26 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -63,7 +63,7 @@ class Localization { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { - element.attr = Localization.getTranslation(key, attr); + element.setAttribute(attr, Localization.getTranslation(key, attr)); } } From abc06fcc21bb73fede96e6cae3487bbf315d3dd6 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Tue, 29 Aug 2023 02:32:54 +0200 Subject: [PATCH 149/608] fix translation fallback for sparely translated languages when complete categories are missing --- public/scripts/localization.js | 22 ++++++++++++------- .../scripts/localization.js | 22 ++++++++++++------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index 8f30f26..a59f30a 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -76,18 +76,24 @@ class Localization { ? Localization.defaultTranslations : Localization.translations; - for (let i=0; i Date: Tue, 29 Aug 2023 02:33:54 +0200 Subject: [PATCH 150/608] enable Norwegian, Russian, and Chinese --- public/scripts/localization.js | 2 +- public_included_ws_fallback/scripts/localization.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index a59f30a..a833993 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -1,7 +1,7 @@ class Localization { constructor() { Localization.defaultLocale = "en"; - Localization.supportedLocales = ["en"]; + Localization.supportedLocales = ["en", "nb", "ru", "zh-CN"]; Localization.translations = {}; Localization.defaultTranslations = {}; diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index a59f30a..a833993 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -1,7 +1,7 @@ class Localization { constructor() { Localization.defaultLocale = "en"; - Localization.supportedLocales = ["en"]; + Localization.supportedLocales = ["en", "nb", "ru", "zh-CN"]; Localization.translations = {}; Localization.defaultTranslations = {}; From d738258869169d9207a3a311b1756c2381479ec7 Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Tue, 29 Aug 2023 09:10:05 -0400 Subject: [PATCH 151/608] Add alternate TURN server option Added note about using an alternate TURN server like OpenRelay. --- docs/host-your-own.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/host-your-own.md b/docs/host-your-own.md index e0e33f7..c81afa1 100644 --- a/docs/host-your-own.md +++ b/docs/host-your-own.md @@ -82,6 +82,7 @@ Set options by using the following flags in the `docker run` command: > You can use `pairdrop/rtc_config_example.json` as a starting point. > > To host your own TURN server you can follow this guide: https://gabrieltanner.org/blog/turn-server/ +> Alternatively, use a free, pre-configured TURN server like [OpenRelay]([url](https://www.metered.ca/tools/openrelay/)) > > Default configuration: > ```json From 17afa18d84f0bffdc3cf7b4b9b9de5dfd0a7461e Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 30 Aug 2023 14:57:40 +0200 Subject: [PATCH 152/608] add translation selector and fix translation of data-attributes --- public/index.html | 43 ++++++++++++--- public/lang/en.json | 7 ++- public/lang/nb.json | 2 +- public/lang/ru.json | 2 +- public/lang/tr.json | 2 +- public/lang/zh-CN.json | 2 +- public/scripts/localization.js | 49 +++++++++++++---- public/scripts/ui.js | 55 +++++++++++++++++++ public/styles.css | 24 +++++--- public_included_ws_fallback/index.html | 43 ++++++++++++--- public_included_ws_fallback/lang/en.json | 2 +- public_included_ws_fallback/lang/nb.json | 2 +- public_included_ws_fallback/lang/ru.json | 2 +- public_included_ws_fallback/lang/tr.json | 2 +- public_included_ws_fallback/lang/zh-CN.json | 2 +- .../scripts/localization.js | 49 +++++++++++++---- public_included_ws_fallback/scripts/ui.js | 55 +++++++++++++++++++ public_included_ws_fallback/styles.css | 37 +++++++++---- 18 files changed, 312 insertions(+), 68 deletions(-) diff --git a/public/index.html b/public/index.html index cd81663..9625637 100644 --- a/public/index.html +++ b/public/index.html @@ -44,6 +44,11 @@
+
+ + + +
@@ -109,7 +114,7 @@
You are known as: -
+
@@ -125,6 +130,25 @@
+ + + + +

Select Language

+
+
+ + + + + +
+
+ +
+
+
+
@@ -147,7 +171,7 @@
Enter key from another device to continue.
-
+
@@ -173,7 +197,7 @@

-
+
@@ -199,7 +223,7 @@
-
+
@@ -224,7 +248,7 @@
-
+
@@ -244,7 +268,7 @@
-
+
@@ -263,7 +287,7 @@
-
+
@@ -392,6 +416,11 @@ + + + + + diff --git a/public/lang/en.json b/public/lang/en.json index ff8294d..de26740 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -1,6 +1,7 @@ { "header": { "about_title": "About PairDrop", + "language-selector_title": "Select Language", "about_aria-label": "Open About PairDrop", "theme-auto_title": "Adapt Theme to System", "theme-light_title": "Always Use Light-Theme", @@ -24,7 +25,7 @@ }, "footer": { "known-as": "You are known as:", - "display-name_placeholder": "Loading…", + "display-name_data-placeholder": "Loading…", "display-name_title": "Edit your device name permanently", "discovery-everyone": "You can be discovered by everyone", "on-this-network": "on this network", @@ -75,7 +76,9 @@ "title-image-plural": "Images", "title-file-plural": "Files", "receive-title": "{{descriptor}} Received", - "download-again": "Download again" + "download-again": "Download again", + "language-selector-title": "Select Language", + "system-language": "System Language" }, "about": { "close-about_aria-label": "Close About PairDrop", diff --git a/public/lang/nb.json b/public/lang/nb.json index b11b664..ee2bd64 100644 --- a/public/lang/nb.json +++ b/public/lang/nb.json @@ -15,7 +15,7 @@ "discovery-everyone": "Du kan oppdages av alle", "and-by": "og av", "webrtc": "hvis WebRTC ikke er tilgjengelig.", - "display-name_placeholder": "Laster inn …", + "display-name_data-placeholder": "Laster inn…", "display-name_title": "Rediger det vedvarende enhetsnavnet ditt", "traffic": "Trafikken", "on-this-network": "på dette nettverket", diff --git a/public/lang/ru.json b/public/lang/ru.json index 8617ec2..1c67504 100644 --- a/public/lang/ru.json +++ b/public/lang/ru.json @@ -24,7 +24,7 @@ }, "footer": { "discovery-everyone": "О вас может узнать каждый", - "display-name_placeholder": "Загрузка…", + "display-name_data-placeholder": "Загрузка…", "routed": "направляется через сервер", "webrtc": ", если WebRTC недоступен.", "traffic": "Трафик", diff --git a/public/lang/tr.json b/public/lang/tr.json index 783e25b..87608f2 100644 --- a/public/lang/tr.json +++ b/public/lang/tr.json @@ -15,7 +15,7 @@ "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" }, "footer": { - "display-name_placeholder": "Yükleniyor…", + "display-name_data-placeholder": "Yükleniyor…", "display-name_title": "Cihazının adını kalıcı olarak düzenle" }, "dialogs": { diff --git a/public/lang/zh-CN.json b/public/lang/zh-CN.json index 4191c7d..d6af684 100644 --- a/public/lang/zh-CN.json +++ b/public/lang/zh-CN.json @@ -26,7 +26,7 @@ "routed": "途径服务器", "webrtc": "如果 WebRTC 不可用。", "known-as": "你的名字是:", - "display-name_placeholder": "加载中…", + "display-name_data-placeholder": "加载中…", "and-by": "和", "display-name_title": "长久修改你的设备名", "discovery-everyone": "你对所有人可见", diff --git a/public/scripts/localization.js b/public/scripts/localization.js index a833993..4510682 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -5,12 +5,19 @@ class Localization { Localization.translations = {}; Localization.defaultTranslations = {}; - const initialLocale = Localization.supportedOrDefault(navigator.languages); + Localization.systemLocale = Localization.supportedOrDefault(navigator.languages); - Localization.setLocale(initialLocale) + let storedLanguageCode = localStorage.getItem("language-code"); + + Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode) + ? storedLanguageCode + : Localization.systemLocale; + + Localization.setTranslation(Localization.initialLocale) .then(_ => { - Localization.translatePage(); - }) + console.log("Initial translation successful."); + Events.fire("translation-loaded"); + }); } static isSupported(locale) { @@ -21,11 +28,21 @@ class Localization { return locales.find(Localization.isSupported) || Localization.defaultLocale; } + static async setTranslation(locale) { + if (!locale) locale = Localization.systemLocale; + + await Localization.setLocale(locale) + await Localization.translatePage(); + + console.log("Page successfully translated", + `System language: ${Localization.systemLocale}`, + `Selected language: ${locale}` + ); + } + static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; - const isFirstTranslation = !Localization.locale - Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale); @@ -34,10 +51,14 @@ class Localization { Localization.locale = newLocale; Localization.translations = newTranslations; + } - if (isFirstTranslation) { - Events.fire("translation-loaded"); - } + static getLocale() { + return Localization.locale; + } + + static isSystemLocale() { + return !localStorage.getItem('language-code'); } static async fetchTranslationsFor(newLocale) { @@ -48,7 +69,7 @@ class Localization { return await response.json(); } - static translatePage() { + static async translatePage() { document .querySelectorAll("[data-i18n-key]") .forEach(element => Localization.translateElement(element)); @@ -63,10 +84,14 @@ class Localization { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { - element.setAttribute(attr, Localization.getTranslation(key, attr)); + if (attr.startsWith("data-")) { + let dataAttr = attr.substring(5); + element.dataset.dataAttr = Localization.getTranslation(key, attr); + } { + element.setAttribute(attr, Localization.getTranslation(key, attr)); + } } } - } static getTranslation(key, attr, data, useDefault=false) { diff --git a/public/scripts/ui.js b/public/scripts/ui.js index f3d08d8..0ab425f 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -45,6 +45,8 @@ class PeersUI { this.$displayName = $('display-name'); + this.$displayName.setAttribute("placeholder", this.$displayName.dataset.placeholder); + 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)); @@ -613,6 +615,58 @@ class Dialog { } } +class LanguageSelectDialog extends Dialog { + + constructor() { + super('language-select-dialog'); + + this.$languageSelectBtn = $('language-selector'); + this.$languageSelectBtn.addEventListener('click', _ => this.show()); + + this.$languageButtons = this.$el.querySelectorAll(".language-buttons button"); + this.$languageButtons.forEach($btn => { + $btn.addEventListener("click", e => this.selectLanguage(e)); + }) + Events.on('keydown', e => this._onKeyDown(e)); + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + this.hide(); + } + } + + show() { + if (Localization.isSystemLocale()) { + this.$languageButtons[0].focus(); + } else { + let locale = Localization.getLocale(); + for (let i=0; i this.hide()); + } +} + class ReceiveDialog extends Dialog { constructor(id) { super(id); @@ -2255,6 +2309,7 @@ class PairDrop { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); + const languageSelectDialog = new LanguageSelectDialog(); const receiveFileDialog = new ReceiveFileDialog(); const receiveRequestDialog = new ReceiveRequestDialog(); const sendTextDialog = new SendTextDialog(); diff --git a/public/styles.css b/public/styles.css index 1375b46..4b23974 100644 --- a/public/styles.css +++ b/public/styles.css @@ -23,6 +23,7 @@ body { -webkit-user-select: none; -moz-user-select: none; user-select: none; + transition: color 300ms; } body { @@ -40,6 +41,10 @@ html { min-height: fill-available; } +.fw { + width: 100%; +} + .row-reverse { display: flex; flex-direction: row-reverse; @@ -591,7 +596,6 @@ footer { align-items: center; padding: 0 0 16px 0; text-align: center; - transition: color 300ms; cursor: default; } @@ -683,7 +687,6 @@ x-dialog x-paper { top: max(50%, 350px); margin-top: -328.5px; width: calc(100vw - 20px); - height: 625px; } #pair-device-dialog ::-moz-selection, @@ -761,7 +764,7 @@ x-dialog a { } x-dialog hr { - margin: 40px -24px 30px -24px; + margin: 20px -24px 20px -24px; border: solid 1.25px var(--border-color); } @@ -868,18 +871,18 @@ x-dialog .row { } /* button row*/ -x-paper > div:last-child { - margin: auto -24px -15px; +x-paper > .button-row { + margin: 25px -24px -15px; border-top: solid 2.5px var(--border-color); height: 50px; } -x-paper > div:last-child > .button { +x-paper > .button-row > .button { height: 100%; width: 100%; } -x-paper > div:last-child > .button:not(:last-child) { +x-paper > .button-row > .button:not(:last-child) { border-left: solid 2.5px var(--border-color); } @@ -1044,6 +1047,11 @@ x-dialog .dialog-subheader { opacity: 0.1; } +.button[selected], +.icon-button[selected] { + opacity: 0.1; +} + #cancel-paste-mode { z-index: 2; margin: 0; @@ -1301,7 +1309,7 @@ x-peers:empty~x-instructions { x-dialog x-paper { padding: 15px; } - x-paper > div:last-child { + x-paper > .button-row { margin: auto -15px -15px; } } diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index a233aab..529bc1a 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -44,6 +44,11 @@ +
+ + + +
@@ -109,7 +114,7 @@
You are known as: -
+
@@ -130,6 +135,25 @@ if WebRTC is not available.
+ + + + +

Select Language

+
+
+ + + + + +
+
+ +
+
+
+
@@ -152,7 +176,7 @@
Enter key from another device to continue.
-
+
@@ -178,7 +202,7 @@

-
+
@@ -204,7 +228,7 @@
-
+
@@ -229,7 +253,7 @@
-
+
@@ -249,7 +273,7 @@
-
+
@@ -268,7 +292,7 @@
-
+
@@ -397,6 +421,11 @@ + + + + + diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index ff8294d..4c88dae 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -24,7 +24,7 @@ }, "footer": { "known-as": "You are known as:", - "display-name_placeholder": "Loading…", + "display-name_data-placeholder": "Loading…", "display-name_title": "Edit your device name permanently", "discovery-everyone": "You can be discovered by everyone", "on-this-network": "on this network", diff --git a/public_included_ws_fallback/lang/nb.json b/public_included_ws_fallback/lang/nb.json index b11b664..ee2bd64 100644 --- a/public_included_ws_fallback/lang/nb.json +++ b/public_included_ws_fallback/lang/nb.json @@ -15,7 +15,7 @@ "discovery-everyone": "Du kan oppdages av alle", "and-by": "og av", "webrtc": "hvis WebRTC ikke er tilgjengelig.", - "display-name_placeholder": "Laster inn …", + "display-name_data-placeholder": "Laster inn…", "display-name_title": "Rediger det vedvarende enhetsnavnet ditt", "traffic": "Trafikken", "on-this-network": "på dette nettverket", diff --git a/public_included_ws_fallback/lang/ru.json b/public_included_ws_fallback/lang/ru.json index 8617ec2..1c67504 100644 --- a/public_included_ws_fallback/lang/ru.json +++ b/public_included_ws_fallback/lang/ru.json @@ -24,7 +24,7 @@ }, "footer": { "discovery-everyone": "О вас может узнать каждый", - "display-name_placeholder": "Загрузка…", + "display-name_data-placeholder": "Загрузка…", "routed": "направляется через сервер", "webrtc": ", если WebRTC недоступен.", "traffic": "Трафик", diff --git a/public_included_ws_fallback/lang/tr.json b/public_included_ws_fallback/lang/tr.json index 783e25b..87608f2 100644 --- a/public_included_ws_fallback/lang/tr.json +++ b/public_included_ws_fallback/lang/tr.json @@ -15,7 +15,7 @@ "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın" }, "footer": { - "display-name_placeholder": "Yükleniyor…", + "display-name_data-placeholder": "Yükleniyor…", "display-name_title": "Cihazının adını kalıcı olarak düzenle" }, "dialogs": { diff --git a/public_included_ws_fallback/lang/zh-CN.json b/public_included_ws_fallback/lang/zh-CN.json index 4191c7d..d6af684 100644 --- a/public_included_ws_fallback/lang/zh-CN.json +++ b/public_included_ws_fallback/lang/zh-CN.json @@ -26,7 +26,7 @@ "routed": "途径服务器", "webrtc": "如果 WebRTC 不可用。", "known-as": "你的名字是:", - "display-name_placeholder": "加载中…", + "display-name_data-placeholder": "加载中…", "and-by": "和", "display-name_title": "长久修改你的设备名", "discovery-everyone": "你对所有人可见", diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index a833993..a447669 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -5,12 +5,19 @@ class Localization { Localization.translations = {}; Localization.defaultTranslations = {}; - const initialLocale = Localization.supportedOrDefault(navigator.languages); + Localization.systemLocale = Localization.supportedOrDefault(navigator.languages); - Localization.setLocale(initialLocale) + let storedLanguageCode = localStorage.getItem("language-code"); + + Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode) + ? storedLanguageCode + : Localization.systemLocale; + + Localization.setTranslation(Localization.initialLocale) .then(_ => { - Localization.translatePage(); - }) + console.log("Initial translation successful."); + Events.fire("translation-loaded"); + }); } static isSupported(locale) { @@ -21,10 +28,20 @@ class Localization { return locales.find(Localization.isSupported) || Localization.defaultLocale; } + static async setTranslation(locale) { + if (!locale) locale = Localization.systemLocale; + + await Localization.setLocale(locale) + await Localization.translatePage(); + + console.log("Page successfully translated", + `System language: ${Localization.systemLocale}`, + `Selected language: ${locale}` + ); + } + static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; - - const isFirstTranslation = !Localization.locale Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); @@ -34,10 +51,14 @@ class Localization { Localization.locale = newLocale; Localization.translations = newTranslations; + } - if (isFirstTranslation) { - Events.fire("translation-loaded"); - } + static getLocale() { + return Localization.locale; + } + + static isSystemLocale() { + return !localStorage.getItem('language-code'); } static async fetchTranslationsFor(newLocale) { @@ -48,7 +69,7 @@ class Localization { return await response.json(); } - static translatePage() { + static async translatePage() { document .querySelectorAll("[data-i18n-key]") .forEach(element => Localization.translateElement(element)); @@ -63,10 +84,14 @@ class Localization { if (attr === "text") { element.innerText = Localization.getTranslation(key); } else { - element.setAttribute(attr, Localization.getTranslation(key, attr)); + if (attr.startsWith("data-")) { + let dataAttr = attr.substring(5); + element.dataset.dataAttr = Localization.getTranslation(key, attr); + } { + element.setAttribute(attr, Localization.getTranslation(key, attr)); + } } } - } static getTranslation(key, attr, data, useDefault=false) { diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index b3afac4..edba81e 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -45,6 +45,8 @@ class PeersUI { this.$displayName = $('display-name'); + this.$displayName.setAttribute("placeholder", this.$displayName.dataset.placeholder); + 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)); @@ -614,6 +616,58 @@ class Dialog { } } +class LanguageSelectDialog extends Dialog { + + constructor() { + super('language-select-dialog'); + + this.$languageSelectBtn = $('language-selector'); + this.$languageSelectBtn.addEventListener('click', _ => this.show()); + + this.$languageButtons = this.$el.querySelectorAll(".language-buttons button"); + this.$languageButtons.forEach($btn => { + $btn.addEventListener("click", e => this.selectLanguage(e)); + }) + Events.on('keydown', e => this._onKeyDown(e)); + } + + _onKeyDown(e) { + if (this.isShown() && e.code === "Escape") { + this.hide(); + } + } + + show() { + if (Localization.isSystemLocale()) { + this.$languageButtons[0].focus(); + } else { + let locale = Localization.getLocale(); + for (let i=0; i this.hide()); + } +} + class ReceiveDialog extends Dialog { constructor(id) { super(id); @@ -2256,6 +2310,7 @@ class PairDrop { const server = new ServerConnection(); const peers = new PeersManager(server); const peersUI = new PeersUI(); + const languageSelectDialog = new LanguageSelectDialog(); const receiveFileDialog = new ReceiveFileDialog(); const receiveRequestDialog = new ReceiveRequestDialog(); const sendTextDialog = new SendTextDialog(); diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index 2e8fbb8..b36dd69 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -24,6 +24,7 @@ body { -webkit-user-select: none; -moz-user-select: none; user-select: none; + transition: color 300ms; } body { @@ -41,6 +42,10 @@ html { min-height: fill-available; } +.fw { + width: 100%; +} + .row-reverse { display: flex; flex-direction: row-reverse; @@ -452,7 +457,7 @@ x-no-peers::before { } x-no-peers[drop-bg]::before { - content: "Release to select recipient"; + content: attr(data-drop-bg); } x-no-peers[drop-bg] * { @@ -652,11 +657,13 @@ footer .font-body2 { #on-this-network { border-bottom: solid 4px var(--primary-color); padding-bottom: 1px; + word-break: keep-all; } #paired-devices { border-bottom: solid 4px var(--paired-device-color); padding-bottom: 1px; + word-break: keep-all; } #display-name { @@ -723,7 +730,6 @@ x-dialog x-paper { top: max(50%, 350px); margin-top: -328.5px; width: calc(100vw - 20px); - height: 625px; } #pair-device-dialog ::-moz-selection, @@ -800,8 +806,12 @@ x-dialog .font-subheading { margin: 16px; } +#pair-instructions { + flex-direction: column; +} + x-dialog hr { - margin: 40px -24px 30px -24px; + margin: 20px -24px 20px -24px; border: solid 1.25px var(--border-color); } @@ -811,7 +821,7 @@ x-dialog hr { /* Edit Paired Devices Dialog */ .paired-devices-wrapper:empty:before { - content: "No paired devices."; + content: attr(data-empty); } .paired-devices-wrapper:empty { @@ -908,18 +918,18 @@ x-dialog .row { } /* button row*/ -x-paper > div:last-child { - margin: auto -24px -15px; +x-paper > .button-row { + margin: 25px -24px -15px; border-top: solid 2.5px var(--border-color); height: 50px; } -x-paper > div:last-child > .button { +x-paper > .button-row > .button { height: 100%; width: 100%; } -x-paper > div:last-child > .button:not(:last-child) { +x-paper > .button-row > .button:not(:last-child) { border-left: solid 2.5px var(--border-color); } @@ -1084,6 +1094,11 @@ x-dialog .dialog-subheader { opacity: 0.1; } +.button[selected], +.icon-button[selected] { + opacity: 0.1; +} + #cancel-paste-mode { z-index: 2; margin: 0; @@ -1314,11 +1329,11 @@ x-instructions:not([drop-peer]):not([drop-bg]):before { } x-instructions[drop-peer]:before { - content: "Release to send to peer"; + content: attr(data-drop-peer); } x-instructions[drop-bg]:not([drop-peer]):before { - content: "Release to select recipient"; + content: attr(data-drop-bg); } x-instructions p { @@ -1358,7 +1373,7 @@ x-peers:empty~x-instructions { x-dialog x-paper { padding: 15px; } - x-paper > div:last-child { + x-paper > .button-row { margin: auto -15px -15px; } } From 1c946fc78ef6014c443c54f4e9432f12eb6007e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 04:32:13 +0000 Subject: [PATCH 153/608] Bump ws from 8.13.0 to 8.14.1 Bumps [ws](https://github.com/websockets/ws) from 8.13.0 to 8.14.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.13.0...8.14.1) --- updated-dependencies: - dependency-name: ws dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ea5f15..b35dc56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "express-rate-limit": "^6.9.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", - "ws": "^8.13.0" + "ws": "^8.14.1" }, "engines": { "node": ">=15" @@ -633,9 +633,9 @@ } }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "engines": { "node": ">=10.0.0" }, @@ -1095,9 +1095,9 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", + "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", "requires": {} } } diff --git a/package.json b/package.json index baca71c..a7df332 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "express-rate-limit": "^6.9.0", "ua-parser-js": "^1.0.35", "unique-names-generator": "^4.3.0", - "ws": "^8.13.0" + "ws": "^8.14.1" }, "engines": { "node": ">=15" From d689fe28e54cc2636c3bd61231e9db51b1c43686 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 18:31:42 +0200 Subject: [PATCH 154/608] add English language names next to native language names to language select dialog --- public/index.html | 6 +++--- public_included_ws_fallback/index.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/index.html b/public/index.html index 9625637..155f511 100644 --- a/public/index.html +++ b/public/index.html @@ -139,9 +139,9 @@
- - - + + +
diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 529bc1a..9a1523b 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -144,9 +144,9 @@
- - - + + +
From 02911804cb696a73e36254b9806e39551c2254cd Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 17:08:57 +0200 Subject: [PATCH 155/608] add default values to Localization.getTranslation function --- public/scripts/localization.js | 4 ++-- public_included_ws_fallback/scripts/localization.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/localization.js b/public/scripts/localization.js index 4510682..879deae 100644 --- a/public/scripts/localization.js +++ b/public/scripts/localization.js @@ -42,7 +42,7 @@ class Localization { static async setLocale(newLocale) { if (newLocale === Localization.locale) return false; - + Localization.defaultTranslations = await Localization.fetchTranslationsFor(Localization.defaultLocale); const newTranslations = await Localization.fetchTranslationsFor(newLocale); @@ -94,7 +94,7 @@ class Localization { } } - static getTranslation(key, attr, data, useDefault=false) { + static getTranslation(key, attr=null, data={}, useDefault=false) { const keys = key.split("."); let translationCandidates = useDefault diff --git a/public_included_ws_fallback/scripts/localization.js b/public_included_ws_fallback/scripts/localization.js index a447669..879deae 100644 --- a/public_included_ws_fallback/scripts/localization.js +++ b/public_included_ws_fallback/scripts/localization.js @@ -94,7 +94,7 @@ class Localization { } } - static getTranslation(key, attr, data, useDefault=false) { + static getTranslation(key, attr=null, data={}, useDefault=false) { const keys = key.split("."); let translationCandidates = useDefault From 6679ef75293be94e0a40175a57e5e8dd97e586a5 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 17:23:47 +0200 Subject: [PATCH 156/608] fix keepAliveTimers not correctly cleared on reconnect --- index.js | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 029a955..176519e 100644 --- a/index.js +++ b/index.js @@ -130,8 +130,10 @@ class PairDropServer { this._wss = new WebSocket.Server({ server }); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); - this._rooms = {}; - this._roomSecrets = {}; + this._rooms = {}; // { roomId: peers[] } + this._roomSecrets = {}; // { pairKey: roomSecret } + + this._keepAliveTimers = {}; console.log('PairDrop is running on port', port); } @@ -139,7 +141,9 @@ class PairDropServer { _onConnection(peer) { peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.onerror = e => console.error(e); + this._keepAlive(peer); + this._send(peer, { type: 'rtc-config', config: rtcConfig @@ -170,7 +174,7 @@ class PairDropServer { this._onDisconnect(sender); break; case 'pong': - sender.lastBeat = Date.now(); + this._keepAliveTimers[sender.id].lastBeat = Date.now(); break; case 'join-ip-room': this._joinRoom(sender); @@ -223,10 +227,15 @@ class PairDropServer { } _disconnect(sender) { - this._leaveRoom(sender, 'ip', '', true); - this._leaveAllSecretRooms(sender, true); this._removeRoomKey(sender.roomKey); sender.roomKey = null; + + this._cancelKeepAlive(sender); + delete this._keepAliveTimers[sender.id]; + + this._leaveRoom(sender, 'ip', '', true); + this._leaveAllSecretRooms(sender, true); + sender.socket.terminate(); } @@ -465,23 +474,29 @@ class PairDropServer { _keepAlive(peer) { this._cancelKeepAlive(peer); - let timeout = 500; - if (!peer.lastBeat) { - peer.lastBeat = Date.now(); + let timeout = 1000; + + if (!this._keepAliveTimers[peer.id]) { + this._keepAliveTimers[peer.id] = { + timer: 0, + lastBeat: Date.now() + }; } - if (Date.now() - peer.lastBeat > 2 * timeout) { + + if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 2 * timeout) { + // Disconnect peer if unresponsive for 10s this._disconnect(peer); return; } this._send(peer, { type: 'ping' }); - peer.timerId = setTimeout(() => this._keepAlive(peer), timeout); + this._keepAliveTimers[peer.id].timer = setTimeout(() => this._keepAlive(peer), timeout); } _cancelKeepAlive(peer) { - if (peer && peer.timerId) { - clearTimeout(peer.timerId); + if (this._keepAliveTimers[peer.id]?.timer) { + clearTimeout(this._keepAliveTimers[peer.id].timer); } } } @@ -506,10 +521,6 @@ class Peer { // set name this._setName(request); - // for keepalive - this.timerId = 0; - this.lastBeat = Date.now(); - this.roomSecrets = []; this.roomKey = null; this.roomKeyRate = 0; From c71bf456e3a71d6539f0ade58c9e0cf5342979c6 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 17:44:49 +0200 Subject: [PATCH 157/608] fix "and 2 other files" div not cleared properly --- public/scripts/ui.js | 24 +++++++++++------------ public_included_ws_fallback/scripts/ui.js | 24 +++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 0ab425f..69cc23a 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -695,20 +695,20 @@ class ReceiveDialog extends Dialog { } _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { - if (files.length > 1) { - let fileOther; - if (files.length === 2) { - fileOther = imagesOnly - ? Localization.getTranslation("dialogs.file-other-description-image") - : Localization.getTranslation("dialogs.file-other-description-file"); - } else { - fileOther = imagesOnly - ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) - : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); - } - this.$fileOther.innerText = fileOther; + let fileOther = ""; + + if (files.length === 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); + } else if (files.length >= 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); } + this.$fileOther.innerText = fileOther; + const fileName = files[0].name; const fileNameSplit = fileName.split('.'); const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index edba81e..c5eb462 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -696,20 +696,20 @@ class ReceiveDialog extends Dialog { } _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) { - if (files.length > 1) { - let fileOther; - if (files.length === 2) { - fileOther = imagesOnly - ? Localization.getTranslation("dialogs.file-other-description-image") - : Localization.getTranslation("dialogs.file-other-description-file"); - } else { - fileOther = imagesOnly - ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) - : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); - } - this.$fileOther.innerText = fileOther; + let fileOther = ""; + + if (files.length === 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image") + : Localization.getTranslation("dialogs.file-other-description-file"); + } else if (files.length >= 2) { + fileOther = imagesOnly + ? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1}) + : Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1}); } + this.$fileOther.innerText = fileOther; + const fileName = files[0].name; const fileNameSplit = fileName.split('.'); const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; From bd7b3c6d28d23004bd2181b112a3c55922e6c220 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 17:50:46 +0200 Subject: [PATCH 158/608] show warning to user if navigator.clipboard.writeText fails --- public/lang/en.json | 1 + public/scripts/ui.js | 11 ++++++++--- public_included_ws_fallback/lang/en.json | 1 + public_included_ws_fallback/scripts/ui.js | 11 ++++++++--- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/public/lang/en.json b/public/lang/en.json index de26740..75a4d60 100644 --- a/public/lang/en.json +++ b/public/lang/en.json @@ -100,6 +100,7 @@ "pairing-key-invalidated": "Key {{key}} invalidated.", "pairing-cleared": "All Devices unpaired.", "copied-to-clipboard": "Copied to clipboard", + "copied-to-clipboard-error": "Copying not possible. Copy manually.", "text-content-incorrect": "Text content is incorrect.", "file-content-incorrect": "File content is incorrect.", "clipboard-content-incorrect": "Clipboard content is incorrect.", diff --git a/public/scripts/ui.js b/public/scripts/ui.js index 69cc23a..4cebb96 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1497,9 +1497,14 @@ class ReceiveTextDialog extends Dialog { async _onCopy() { const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); - await navigator.clipboard.writeText(sanitizedText); - Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); - this.hide(); + navigator.clipboard.writeText(sanitizedText) + .then(_ => { + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); + this.hide(); + }) + .catch(_ => { + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard-error")); + }); } hide() { diff --git a/public_included_ws_fallback/lang/en.json b/public_included_ws_fallback/lang/en.json index 4c88dae..a607219 100644 --- a/public_included_ws_fallback/lang/en.json +++ b/public_included_ws_fallback/lang/en.json @@ -97,6 +97,7 @@ "pairing-key-invalidated": "Key {{key}} invalidated.", "pairing-cleared": "All Devices unpaired.", "copied-to-clipboard": "Copied to clipboard", + "copied-to-clipboard-error": "Copying not possible. Copy manually.", "text-content-incorrect": "Text content is incorrect.", "file-content-incorrect": "File content is incorrect.", "clipboard-content-incorrect": "Clipboard content is incorrect.", diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index c5eb462..460f30f 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1498,9 +1498,14 @@ class ReceiveTextDialog extends Dialog { async _onCopy() { const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' '); - await navigator.clipboard.writeText(sanitizedText); - Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); - this.hide(); + navigator.clipboard.writeText(sanitizedText) + .then(_ => { + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard")); + this.hide(); + }) + .catch(_ => { + Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard-error")); + }); } hide() { From 8d2584fa6933c3209bb7096f18b686e4a60b7e29 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 13 Sep 2023 18:15:01 +0200 Subject: [PATCH 159/608] implement temporary public rooms, tidy up index.js, rework UI dialogs and change colors slightly --- index.js | 283 ++++++++++------ public/index.html | 255 ++++++++++---- public/lang/en.json | 30 +- public/scripts/network.js | 235 +++++++++---- public/scripts/ui.js | 677 ++++++++++++++++++++++++++++---------- public/styles.css | 253 +++++++++----- 6 files changed, 1237 insertions(+), 496 deletions(-) diff --git a/index.js b/index.js index 176519e..1d08d7d 100644 --- a/index.js +++ b/index.js @@ -177,7 +177,7 @@ class PairDropServer { this._keepAliveTimers[sender.id].lastBeat = Date.now(); break; case 'join-ip-room': - this._joinRoom(sender); + this._joinIpRoom(sender); break; case 'room-secrets': this._onRoomSecrets(sender, message); @@ -196,9 +196,15 @@ class PairDropServer { break; case 'regenerate-room-secret': this._onRegenerateRoomSecret(sender, message); - break - case 'resend-peers': - this._notifyPeers(sender); + break; + case 'create-public-room': + this._onCreatePublicRoom(sender); + break; + case 'join-public-room': + this._onJoinPublicRoom(sender, message); + break; + case 'leave-public-room': + this._onLeavePublicRoom(sender); break; case 'signal': default: @@ -207,7 +213,9 @@ class PairDropServer { } _signalAndRelay(sender, message) { - const room = message.roomType === 'ip' ? sender.ip : message.roomSecret; + const room = message.roomType === 'ip' + ? sender.ip + : message.roomId; // relay message to recipient if (message.to && Peer.isValidUuid(message.to) && this._rooms[room]) { @@ -227,14 +235,15 @@ class PairDropServer { } _disconnect(sender) { - this._removeRoomKey(sender.roomKey); - sender.roomKey = null; + this._removePairKey(sender.pairKey); + sender.pairKey = null; this._cancelKeepAlive(sender); delete this._keepAliveTimers[sender.id]; - this._leaveRoom(sender, 'ip', '', true); + this._leaveIpRoom(sender, true); this._leaveAllSecretRooms(sender, true); + this._leavePublicRoom(sender, true); sender.socket.terminate(); } @@ -264,7 +273,7 @@ class PairDropServer { for (const peerId in room) { const peer = room[peerId]; - this._leaveRoom(peer, 'secret', roomSecret); + this._leaveSecretRoom(peer, roomSecret, true); this._send(peer, { type: 'secret-room-deleted', @@ -275,34 +284,35 @@ class PairDropServer { _onPairDeviceInitiate(sender) { let roomSecret = randomizer.getRandomString(256); - let roomKey = this._createRoomKey(sender, roomSecret); - if (sender.roomKey) this._removeRoomKey(sender.roomKey); - sender.roomKey = roomKey; + let pairKey = this._createPairKey(sender, roomSecret); + + if (sender.pairKey) { + this._removePairKey(sender.pairKey); + } + sender.pairKey = pairKey; + this._send(sender, { type: 'pair-device-initiated', roomSecret: roomSecret, - roomKey: roomKey + pairKey: pairKey }); - this._joinRoom(sender, 'secret', roomSecret); + this._joinSecretRoom(sender, roomSecret); } _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' }); + if (sender.rateLimitReached()) { + this._send(sender, { type: '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) { + if (!this._roomSecrets[message.pairKey] || sender.id === this._roomSecrets[message.pairKey].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); + const roomSecret = this._roomSecrets[message.pairKey].roomSecret; + const creator = this._roomSecrets[message.pairKey].creator; + this._removePairKey(message.pairKey); this._send(sender, { type: 'pair-device-joined', roomSecret: roomSecret, @@ -313,22 +323,53 @@ class PairDropServer { roomSecret: roomSecret, peerId: sender.id }); - this._joinRoom(sender, 'secret', roomSecret); - this._removeRoomKey(sender.roomKey); + this._joinSecretRoom(sender, roomSecret); + this._removePairKey(sender.pairKey); } _onPairDeviceCancel(sender) { - const roomKey = sender.roomKey + const pairKey = sender.pairKey - if (!roomKey) return; + if (!pairKey) return; - this._removeRoomKey(roomKey); + this._removePairKey(pairKey); this._send(sender, { type: 'pair-device-canceled', - roomKey: roomKey, + pairKey: pairKey, }); } + _onCreatePublicRoom(sender) { + let publicRoomId = randomizer.getRandomString(5, true).toLowerCase(); + + this._send(sender, { + type: 'public-room-created', + roomId: publicRoomId + }); + + this._joinPublicRoom(sender, publicRoomId); + } + + _onJoinPublicRoom(sender, message) { + if (sender.rateLimitReached()) { + this._send(sender, { type: 'join-key-rate-limit' }); + return; + } + + if (!this._rooms[message.publicRoomId] && !message.createIfInvalid) { + this._send(sender, { type: 'public-room-id-invalid', publicRoomId: message.publicRoomId }); + return; + } + + this._leavePublicRoom(sender); + this._joinPublicRoom(sender, message.publicRoomId); + } + + _onLeavePublicRoom(sender) { + this._leavePublicRoom(sender, true); + this._send(sender, { type: 'public-room-left' }); + } + _onRegenerateRoomSecret(sender, message) { const oldRoomSecret = message.roomSecret; const newRoomSecret = randomizer.getRandomString(256); @@ -346,122 +387,158 @@ class PairDropServer { delete this._rooms[oldRoomSecret]; } - _createRoomKey(creator, roomSecret) { - let roomKey; + _createPairKey(creator, roomSecret) { + let pairKey; do { // get randomInt until keyRoom not occupied - roomKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s - } while (roomKey in this._roomSecrets) + pairKey = crypto.randomInt(1000000, 1999999).toString().substring(1); // include numbers with leading 0s + } while (pairKey in this._roomSecrets) - this._roomSecrets[roomKey] = { + this._roomSecrets[pairKey] = { roomSecret: roomSecret, creator: creator } - return roomKey; + return pairKey; } - _removeRoomKey(roomKey) { + _removePairKey(roomKey) { if (roomKey in this._roomSecrets) { this._roomSecrets[roomKey].creator.roomKey = null delete this._roomSecrets[roomKey]; } } - _joinRoom(peer, roomType = 'ip', roomSecret = '') { - const room = roomType === 'ip' ? peer.ip : roomSecret; + _joinIpRoom(peer) { + this._joinRoom(peer, 'ip', peer.ip); + } - if (this._rooms[room] && this._rooms[room][peer.id]) { + _joinSecretRoom(peer, roomSecret) { + this._joinRoom(peer, 'secret', roomSecret); + + // add secret to peer + peer.addRoomSecret(roomSecret); + } + + _joinPublicRoom(peer, publicRoomId) { + // prevent joining of 2 public rooms simultaneously + this._leavePublicRoom(peer); + + this._joinRoom(peer, 'public-id', publicRoomId); + + peer.publicRoomId = publicRoomId; + } + + _joinRoom(peer, roomType, roomId) { + // roomType: 'ip', 'secret' or 'public-id' + if (this._rooms[roomId] && this._rooms[roomId][peer.id]) { // ensures that otherPeers never receive `peer-left` after `peer-joined` on reconnect. - this._leaveRoom(peer, roomType, roomSecret); + this._leaveRoom(peer, roomType, roomId); } // if room doesn't exist, create it - if (!this._rooms[room]) { - this._rooms[room] = {}; + if (!this._rooms[roomId]) { + this._rooms[roomId] = {}; } - this._notifyPeers(peer, roomType, roomSecret); + this._notifyPeers(peer, roomType, roomId); // add peer to room - this._rooms[room][peer.id] = peer; - // add secret to peer - if (roomType === 'secret') { - peer.addRoomSecret(roomSecret); - } + this._rooms[roomId][peer.id] = peer; } - _leaveRoom(peer, roomType = 'ip', roomSecret = '', disconnect = false) { - const room = roomType === 'ip' ? peer.ip : roomSecret; - if (!this._rooms[room] || !this._rooms[room][peer.id]) return; - this._cancelKeepAlive(this._rooms[room][peer.id]); + _leaveIpRoom(peer, disconnect = false) { + this._leaveRoom(peer, 'ip', peer.ip, disconnect); + } - // delete the peer - delete this._rooms[room][peer.id]; + _leaveSecretRoom(peer, roomSecret, disconnect = false) { + this._leaveRoom(peer, 'secret', roomSecret, disconnect) - //if room is empty, delete the room - if (!Object.keys(this._rooms[room]).length) { - delete this._rooms[room]; - } else { - // notify all other peers - for (const otherPeerId in this._rooms[room]) { - const otherPeer = this._rooms[room][otherPeerId]; - this._send(otherPeer, { - type: 'peer-left', - peerId: peer.id, - roomType: roomType, - roomSecret: roomSecret, - disconnect: disconnect - }); - } - } //remove secret from peer - if (roomType === 'secret') { - peer.removeRoomSecret(roomSecret); + peer.removeRoomSecret(roomSecret); + } + + _leavePublicRoom(peer, disconnect = false) { + if (!peer.publicRoomId) return; + + this._leaveRoom(peer, 'public-id', peer.publicRoomId, disconnect); + + peer.publicRoomId = null; + } + + _leaveRoom(peer, roomType, roomId, disconnect = false) { + if (!this._rooms[roomId] || !this._rooms[roomId][peer.id]) return; + + // remove peer from room + delete this._rooms[roomId][peer.id]; + + // delete room if empty and abort + if (!Object.keys(this._rooms[roomId]).length) { + delete this._rooms[roomId]; + return; + } + + // notify all other peers that remain in room that peer left + for (const otherPeerId in this._rooms[roomId]) { + const otherPeer = this._rooms[roomId][otherPeerId]; + + let msg = { + type: 'peer-left', + peerId: peer.id, + roomType: roomType, + roomId: roomId, + disconnect: disconnect + }; + + this._send(otherPeer, msg); } } - _notifyPeers(peer, roomType = 'ip', roomSecret = '') { - const room = roomType === 'ip' ? peer.ip : roomSecret; - if (!this._rooms[room]) return; + _notifyPeers(peer, roomType, roomId) { + if (!this._rooms[roomId]) return; - // notify all other peers - for (const otherPeerId in this._rooms[room]) { + // notify all other peers that peer joined + for (const otherPeerId in this._rooms[roomId]) { if (otherPeerId === peer.id) continue; - const otherPeer = this._rooms[room][otherPeerId]; - this._send(otherPeer, { + const otherPeer = this._rooms[roomId][otherPeerId]; + + let msg = { type: 'peer-joined', peer: peer.getInfo(), roomType: roomType, - roomSecret: roomSecret - }); + roomId: roomId + }; + + this._send(otherPeer, msg); } - // notify peer about the other peers + // notify peer about peers already in the room const otherPeers = []; - for (const otherPeerId in this._rooms[room]) { + for (const otherPeerId in this._rooms[roomId]) { if (otherPeerId === peer.id) continue; - otherPeers.push(this._rooms[room][otherPeerId].getInfo()); + otherPeers.push(this._rooms[roomId][otherPeerId].getInfo()); } - this._send(peer, { + let msg = { type: 'peers', peers: otherPeers, roomType: roomType, - roomSecret: roomSecret - }); + roomId: roomId + }; + + this._send(peer, msg); } _joinSecretRooms(peer, roomSecrets) { for (let i=0; i 2 * timeout) { + if (Date.now() - this._keepAliveTimers[peer.id].lastBeat > 5 * timeout) { // Disconnect peer if unresponsive for 10s this._disconnect(peer); return; @@ -521,9 +598,22 @@ class Peer { // set name this._setName(request); + this.requestRate = 0; + this.roomSecrets = []; this.roomKey = null; - this.roomKeyRate = 0; + + this.publicRoomId = null; + } + + rateLimitReached() { + // rate limit implementation: max 10 attempts every 10s + if (this.requestRate >= 10) { + return true; + } + this.requestRate += 1; + setTimeout(_ => this.requestRate -= 1, 10000); + return false; } _setIP(request) { @@ -699,8 +789,15 @@ const hasher = (() => { })() const randomizer = (() => { + let charCodeLettersOnly = r => 65 <= r && r <= 90; + let charCodeAllPrintableChars = r => r === 45 || 47 <= r && r <= 57 || 64 <= r && r <= 90 || 97 <= r && r <= 122; + return { - getRandomString(length) { + getRandomString(length, lettersOnly = false) { + const charCodeCondition = lettersOnly + ? charCodeLettersOnly + : charCodeAllPrintableChars; + let string = ""; while (string.length < length) { let arr = new Uint16Array(length); @@ -711,7 +808,7 @@ const randomizer = (() => { }) 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; + return charCodeCondition(r); }); string += String.fromCharCode.apply(String, arr); } diff --git a/public/index.html b/public/index.html index 155f511..0ea2fd1 100644 --- a/public/index.html +++ b/public/index.html @@ -78,7 +78,7 @@
-