diff --git a/README.md b/README.md index 111e4f0..63a807f 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)) diff --git a/index.js b/index.js index 02fe123..9e7fd71 100644 --- a/index.js +++ b/index.js @@ -477,7 +477,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; @@ -549,15 +549,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 3a4dff2..f0aec21 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 @@ -332,6 +336,10 @@ + + + + diff --git a/public/scripts/network.js b/public/scripts/network.js index 2bf52c8..6dcf24b 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); @@ -117,37 +118,18 @@ 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(); @@ -157,6 +139,10 @@ class ServerConnection { } } + _disconnect() { + this.send({ type: 'disconnect' }); + } + _onDisconnect() { console.log('WS: server disconnected'); Events.fire('notify-user', 'Connecting..'); @@ -358,6 +344,9 @@ class Peer { case 'text': this._onTextReceived(messageJSON); break; + case 'display-name-changed': + this._onDisplayNameChanged(messageJSON); + break; } } @@ -495,6 +484,12 @@ class Peer { Events.fire('text-received', { text: escaped, peerId: this._peerId }); this.sendJSON({ type: 'message-transfer-complete' }); } + + _onDisplayNameChanged(message) { + if (!message.displayName) return; + console.debug(message) + Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); + } } class RTCPeer extends Peer { @@ -568,14 +563,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()}); } _onMessage(message) { @@ -615,13 +610,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() { @@ -635,9 +638,11 @@ class RTCPeer extends Peer { console.log('RTC: state changed:', this._conn.connectionState); switch (this._conn.connectionState) { case 'disconnected': + Events.fire('peer-disconnected', this._peerId); this._onError('rtc connection disconnected'); break; case 'failed': + Events.fire('peer-disconnected', this._peerId); this._onError('rtc connection failed'); break; } @@ -696,8 +701,11 @@ class PeersManager { Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) Events.on('send-text', e => this._onSendText(e.detail)); Events.on('peer-left', e => this._onPeerLeft(e.detail)); + Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); + Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); + Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); } _onMessage(message) { @@ -721,10 +729,6 @@ class PeersManager { }) } - sendTo(peerId, message) { - this.peers[peerId].send(message); - } - _onRespondToFileTransferRequest(detail) { this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); } @@ -756,6 +760,10 @@ class PeersManager { } } + _onPeerConnected(peerId) { + this._notifyPeerDisplayNameChanged(peerId); + } + _onPeerDisconnected(peerId) { const peer = this.peers[peerId]; delete this.peers[peerId]; @@ -773,6 +781,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 cc0a476..e0c90b0 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -9,8 +9,8 @@ window.pasteMode.activated = false; // set display name Events.on('display-name', e => { const me = e.detail.message; - const $displayName = $('display-name') - $displayName.textContent = 'You are known as ' + me.displayName; + const $displayName = $('display-name'); + $displayName.setAttribute('placeholder', me.displayName); $displayName.title = me.deviceName; }); @@ -43,6 +43,82 @@ class PeersUI { Events.on('peer-added', _ => this.evaluateOverflowing()); Events.on('bg-resize', _ => this.evaluateOverflowing()); + + this.$displayName = $('display-name'); + + this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e)); + this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e)); + this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText)); + + Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail)); + Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName)); + + // Load saved display name on page load + this._getSavedDisplayName().then(displayName => { + console.log("Retrieved edited display name:", displayName) + if (displayName) Events.fire('self-display-name-changed', displayName); + }); + } + + _insertDisplayName(displayName) { + this.$displayName.textContent = displayName; + } + + _onKeyDownDisplayName(e) { + if (e.key === "Enter" || e.key === "Escape") { + e.preventDefault(); + e.target.blur(); + } + } + + _onKeyUpDisplayName(e) { + // fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty + if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = ''; + } + + async _saveDisplayName(newDisplayName) { + newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') + const savedDisplayName = await this._getSavedDisplayName(); + if (newDisplayName === savedDisplayName) return; + + if (newDisplayName) { + PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => { + Events.fire('notify-user', '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').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: ''}); + }); + } + } + + _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); + console.debug(peerIdNode) + console.debug(displayName) + if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName; } _onKeyDown(e) { @@ -544,6 +620,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; @@ -1723,6 +1800,23 @@ class PersistentStorage { } } +class Broadcast { + constructor() { + this.bc = new BroadcastChannel('pairdrop'); + this.bc.addEventListener('message', e => this._onMessage(e)); + Events.on('broadcast-send', e => this._broadcastMessage(e.detail)); + } + + _broadcastMessage(message) { + this.bc.postMessage(message); + } + + _onMessage(e) { + console.log('Broadcast message received:', e.data) + Events.fire(e.data.type, e.data.detail); + } +} + class PairDrop { constructor() { Events.on('load', _ => { @@ -1742,6 +1836,7 @@ class PairDrop { const webShareTargetUI = new WebShareTargetUI(); const webFileHandlersUI = new WebFileHandlersUI(); const noSleepUI = new NoSleepUI(); + const broadCast = new Broadcast(); }); } } diff --git a/public/styles.css b/public/styles.css index f998e83..8f68114 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; } @@ -533,6 +534,7 @@ footer { padding: 0 0 16px 0; text-align: center; transition: color 300ms; + cursor: default; } footer .logo { @@ -557,6 +559,35 @@ footer .font-body2 { padding-bottom: 1px; } +#display-name { + display: inline-block; + text-align: left; + border: none; + outline: none; + max-width: 18em; + text-overflow: ellipsis; + white-space: nowrap; + 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 { + width: 1rem; + height: 1rem; + margin-left: -1rem; + margin-bottom: -2px; + position: relative; + z-index: -1; +} + /* Dialog */ x-dialog x-background { @@ -995,11 +1026,11 @@ button::-moz-focus-inner { x-toast { position: absolute; min-height: 48px; - bottom: 24px; + top: 50px; width: 100%; max-width: 344px; - background-color: #323232; - color: rgba(255, 255, 255, 0.95); + background-color: rgb(var(--text-color)); + color: rgb(var(--bg-color)); align-items: center; box-sizing: border-box; padding: 8px 24px; @@ -1013,7 +1044,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 7591cf1..a75fe69 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 @@ -335,6 +339,10 @@ + + + + diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index 6f40723..58e0019 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); @@ -113,6 +114,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; @@ -127,37 +129,18 @@ 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(); @@ -167,6 +150,10 @@ class ServerConnection { } } + _disconnect() { + this.send({ type: 'disconnect' }); + } + _onDisconnect() { console.log('WS: server disconnected'); Events.fire('notify-user', 'Connecting..'); @@ -368,6 +355,9 @@ class Peer { case 'text': this._onTextReceived(messageJSON); break; + case 'display-name-changed': + this._onDisplayNameChanged(messageJSON); + break; } } @@ -505,6 +495,11 @@ class Peer { Events.fire('text-received', { text: escaped, peerId: this._peerId }); this.sendJSON({ type: 'message-transfer-complete' }); } + + _onDisplayNameChanged(message) { + if (!message.displayName) return; + Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); + } } class RTCPeer extends Peer { @@ -578,14 +573,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()}); } _onMessage(message) { @@ -625,13 +620,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() { @@ -645,9 +648,11 @@ class RTCPeer extends Peer { console.log('RTC: state changed:', this._conn.connectionState); switch (this._conn.connectionState) { case 'disconnected': + Events.fire('peer-disconnected', this._peerId); this._onError('rtc connection disconnected'); break; case 'failed': + Events.fire('peer-disconnected', this._peerId); this._onError('rtc connection failed'); break; } @@ -701,6 +706,7 @@ class WSPeer extends Peer { super(serverConnection, peerId, roomType, roomSecret); this.rtcSupported = false; if (!peerId) return; // we will listen for a caller + this._isCaller = true; this._sendSignal(); } @@ -712,6 +718,7 @@ class WSPeer extends Peer { } sendJSON(message) { + console.debug(message) message.to = this._peerId; message.roomType = this._roomType; message.roomSecret = this._roomSecret; @@ -723,9 +730,9 @@ class WSPeer extends Peer { } onServerMessage(message) { + this._peerId = message.sender.id; Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()}) if (message.connected) return; - this._peerId = message.sender.id; this._sendSignal(true); } @@ -746,8 +753,11 @@ class PeersManager { Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail)) Events.on('send-text', e => this._onSendText(e.detail)); Events.on('peer-left', e => this._onPeerLeft(e.detail)); + Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); + Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); + Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-relay', e => this._onWsRelay(e.detail)); } @@ -787,10 +797,6 @@ class PeersManager { }) } - sendTo(peerId, message) { - this.peers[peerId].send(message); - } - _onRespondToFileTransferRequest(detail) { this.peers[detail.to]._respondToFileTransferRequest(detail.accepted); } @@ -825,6 +831,10 @@ class PeersManager { } } + _onPeerConnected(peerId) { + this._notifyPeerDisplayNameChanged(peerId); + } + _onWsDisconnected() { for (const peerId in this.peers) { console.debug(this.peers[peerId].rtcSupported); @@ -851,6 +861,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 82b0eab..0699043 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -9,8 +9,8 @@ window.pasteMode.activated = false; // set display name Events.on('display-name', e => { const me = e.detail.message; - const $displayName = $('display-name') - $displayName.textContent = 'You are known as ' + me.displayName; + const $displayName = $('display-name'); + $displayName.setAttribute('placeholder', me.displayName); $displayName.title = me.deviceName; }); @@ -43,6 +43,81 @@ class PeersUI { Events.on('peer-added', _ => this.evaluateOverflowing()); Events.on('bg-resize', _ => this.evaluateOverflowing()); + + this.$displayName = $('display-name'); + + this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e)); + this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e)); + this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText)); + + Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail)); + Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName)); + + // Load saved display name on page load + this._getSavedDisplayName().then(displayName => { + console.log("Retrieved edited display name:", displayName) + if (displayName) Events.fire('self-display-name-changed', displayName); + }); + } + + _insertDisplayName(displayName) { + this.$displayName.textContent = displayName; + } + + _onKeyDownDisplayName(e) { + if (e.key === "Enter" || e.key === "Escape") { + e.preventDefault(); + e.target.blur(); + } + } + + _onKeyUpDisplayName(e) { + // fix for Firefox inserting a linebreak into div on edit which prevents the placeholder from showing automatically when it is empty + if (/^(\n|\r|\r\n)$/.test(e.target.innerText)) e.target.innerText = ''; + } + + async _saveDisplayName(newDisplayName) { + newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') + 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.'); + }).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').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: ''}); + }); + } + } + + _getSavedDisplayName() { + return new Promise((resolve) => { + PersistentStorage.get('editedDisplayName') + .then(displayName => resolve(displayName ?? "")) + .catch(_ => resolve(localStorage.getItem('editedDisplayName') ?? "")) + }); + } + + _changePeerDisplayName(peerId, displayName) { + this.peers[peerId].name.displayName = displayName; + const peerIdNode = $(peerId); + if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName; } _onKeyDown(e) { @@ -545,6 +620,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; @@ -1724,6 +1800,23 @@ class PersistentStorage { } } +class Broadcast { + constructor() { + this.bc = new BroadcastChannel('pairdrop'); + this.bc.addEventListener('message', e => this._onMessage(e)); + Events.on('broadcast-send', e => this._broadcastMessage(e.detail)); + } + + _broadcastMessage(message) { + this.bc.postMessage(message); + } + + _onMessage(e) { + console.log('Broadcast message received:', e.data) + Events.fire(e.data.type, e.data.detail); + } +} + class PairDrop { constructor() { Events.on('load', _ => { @@ -1743,6 +1836,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 e9eda38..16300e8 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; } @@ -559,6 +560,7 @@ footer { align-items: center; text-align: center; transition: color 300ms; + cursor: default; } footer .logo { @@ -583,6 +585,35 @@ footer .font-body2 { padding-bottom: 1px; } +#display-name { + display: inline-block; + text-align: left; + border: none; + outline: none; + max-width: 18em; + text-overflow: ellipsis; + white-space: nowrap; + 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 { + width: 1rem; + height: 1rem; + margin-left: -1rem; + margin-bottom: -2px; + position: relative; + z-index: -1; +} + /* Dialog */ x-dialog x-background { @@ -1021,11 +1052,11 @@ button::-moz-focus-inner { x-toast { position: absolute; min-height: 48px; - bottom: 24px; + top: 50px; width: 100%; max-width: 344px; - background-color: #323232; - color: rgba(255, 255, 255, 0.95); + background-color: rgb(var(--text-color)); + color: rgb(var(--bg-color)); align-items: center; box-sizing: border-box; padding: 8px 24px; @@ -1039,7 +1070,7 @@ x-toast { x-toast:not([show]):not(:hover) { opacity: 0; - transform: translateY(100px); + transform: translateY(-100px); }