From d56ee874376e611d57fd560e6b33525ff85a11c3 Mon Sep 17 00:00:00 2001 From: schlagmichdoch Date: Wed, 1 Mar 2023 21:35:00 +0100 Subject: [PATCH] - 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); }