From d56ee874376e611d57fd560e6b33525ff85a11c3 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 1 Mar 2023 21:35:00 +0100 Subject: [PATCH 1/6] - 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 2/6] 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 3/6] 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 451173caac7f8e469de1d964b580b861caba0de9 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Fri, 3 Mar 2023 19:10:24 +0100 Subject: [PATCH 4/6] 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 e37f9bd9fb8d6bcce8e1d4c979d767e1f9f05016 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 4 Mar 2023 15:44:42 +0100 Subject: [PATCH 5/6] 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 96ed0e53b1103eaba66db63509b0d824446e4214 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Sat, 4 Mar 2023 20:50:52 +0100 Subject: [PATCH 6/6] 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 {