diff --git a/index.js b/index.js index 766d9bd..514ed98 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const process = require('process') const crypto = require('crypto') const {spawn} = require('child_process') +const WebSocket = require('ws'); // Handle SIGINT process.on('SIGINT', () => { @@ -99,7 +100,6 @@ const { uniqueNamesGenerator, animals, colors } = require('unique-names-generato class PairDropServer { constructor() { - const WebSocket = require('ws'); this._wss = new WebSocket.Server({ server }); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); @@ -110,10 +110,10 @@ class PairDropServer { } _onConnection(peer) { - this._joinRoom(peer); peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.onerror = e => console.error(e); this._keepAlive(peer); + this._joinRoom(peer); // send displayName this._send(peer, { @@ -317,6 +317,10 @@ class PairDropServer { _joinRoom(peer, roomType = 'ip', roomSecret = '') { const room = roomType === 'ip' ? peer.ip : roomSecret; + if (this._rooms[room] && this._rooms[room][peer.id]) { + this._leaveRoom(peer, roomType, roomSecret); + } + // if room doesn't exist, create it if (!this._rooms[room]) { this._rooms[room] = {}; @@ -341,10 +345,6 @@ class PairDropServer { // delete the peer delete this._rooms[room][peer.id]; - if (roomType === 'ip') { - peer.socket.terminate(); - } - //if room is empty, delete the room if (!Object.keys(this._rooms[room]).length) { delete this._rooms[room]; diff --git a/package-lock.json b/package-lock.json index c9ddb2a..c5060cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pairdrop", - "version": "1.1.3", + "version": "1.2.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pairdrop", - "version": "1.1.3", + "version": "1.2.2", "license": "ISC", "dependencies": { "express": "^4.18.2", diff --git a/package.json b/package.json index 30444eb..d2baecf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pairdrop", - "version": "1.1.3", + "version": "1.2.2", "description": "", "main": "index.js", "scripts": { diff --git a/public/index.html b/public/index.html index 6c54479..ad659da 100644 --- a/public/index.html +++ b/public/index.html @@ -69,7 +69,7 @@ - +
@@ -110,18 +110,17 @@
Input this key on another device
or scan the QR-Code.

- - - - - - + + + + + +
Enter key from another device to continue.
-
+
-
- Cancel +
@@ -134,9 +133,9 @@

Unpair Devices

Are you sure to unpair all devices?
-
+
- Cancel +
@@ -146,25 +145,23 @@ -

PairDrop

-
+

+
would like to share
-
- - +
+ +
-
- +
+
-
-
+
-
@@ -174,13 +171,23 @@ -

-
-
+

+
+
+ + has sent +
+
+ + +
+
+
+
-
- -
+
+ +
@@ -191,16 +198,16 @@
-

PairDrop

-
+

Send Message

+
Send a Message to
+
-
+
-
- Cancel +
@@ -210,16 +217,15 @@ -

PairDrop - Message Received

-
+

Message Received

+
- sent a message: + has sent:
-
+
-
diff --git a/public/scripts/network.js b/public/scripts/network.js index 76d9e0b..ce28ff5 100644 --- a/public/scripts/network.js +++ b/public/scripts/network.js @@ -37,6 +37,7 @@ class ServerConnection { _onOpen() { console.log('WS: server connected'); Events.fire('ws-connected'); + if (this._isReconnect) Events.fire('notify-user', 'Connected.'); } _sendRoomSecrets(roomSecrets) { @@ -126,6 +127,8 @@ class ServerConnection { this._socket.onclose = null; this._socket.close(); this._socket = null; + Events.fire('ws-disconnected'); + this._isReconnect = true; } } @@ -135,10 +138,11 @@ class ServerConnection { _onDisconnect() { console.log('WS: server disconnected'); - Events.fire('notify-user', 'No server connection. Retry in 5s...'); + Events.fire('notify-user', 'Connecting..'); clearTimeout(this._reconnectTimer); this._reconnectTimer = setTimeout(_ => this._connect(), 5000); Events.fire('ws-disconnected'); + this._isReconnect = true; } _onVisibilityChange() { @@ -304,25 +308,25 @@ class Peer { this._onChunkReceived(message); return; } - message = JSON.parse(message); - switch (message.type) { + const messageJSON = JSON.parse(message); + switch (messageJSON.type) { case 'request': - this._onFilesTransferRequest(message); + this._onFilesTransferRequest(messageJSON); break; case 'header': - this._onFilesHeader(message); + this._onFilesHeader(messageJSON); break; case 'partition': - this._onReceivedPartitionEnd(message); + this._onReceivedPartitionEnd(messageJSON); break; case 'partition-received': this._sendNextPartition(); break; case 'progress': - this._onDownloadProgress(message.progress); + this._onDownloadProgress(messageJSON.progress); break; case 'files-transfer-response': - this._onFileTransferRequestResponded(message); + this._onFileTransferRequestResponded(messageJSON); break; case 'file-transfer-complete': this._onFileTransferCompleted(); @@ -331,10 +335,10 @@ class Peer { this._onMessageTransferCompleted(); break; case 'text': - this._onTextReceived(message); + this._onTextReceived(messageJSON); break; case 'display-name-changed': - this._onDisplayNameChanged(message); + this._onDisplayNameChanged(messageJSON); break; } } @@ -428,7 +432,7 @@ class Peer { if (!this._requestAccepted.header.length) { this._busy = false; Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); - Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted}); + Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize}); this._filesReceived = []; this._requestAccepted = null; } @@ -476,6 +480,7 @@ class Peer { _onDisplayNameChanged(message) { if (!message.displayName) return; + console.debug(message) Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); } } @@ -484,17 +489,11 @@ class RTCPeer extends Peer { constructor(serverConnection, peerId, roomType, roomSecret) { super(serverConnection, peerId, roomType, roomSecret); + this.rtcSupported = true; if (!peerId) return; // we will listen for a caller 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); @@ -567,6 +566,13 @@ class RTCPeer extends Peer { Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()}); } + _onMessage(message) { + if (typeof message === 'string') { + console.log('RTC:', JSON.parse(message)); + } + super._onMessage(message); + } + getConnectionHash() { const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n"); diff --git a/public/scripts/ui.js b/public/scripts/ui.js index a45ace6..e0c90b0 100644 --- a/public/scripts/ui.js +++ b/public/scripts/ui.js @@ -1,6 +1,5 @@ const $ = query => document.getElementById(query); const $$ = query => document.body.querySelector(query); -const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase()); window.isProductionEnvironment = !window.location.host.startsWith('localhost'); window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; window.android = /android/i.test(navigator.userAgent); @@ -28,7 +27,7 @@ class PeersUI { Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); this.peers = {}; - this.$cancelPasteModeBtn = $('cancel-paste-mode-btn'); + this.$cancelPasteModeBtn = $('cancel-paste-mode'); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); Events.on('dragover', e => this._onDragOver(e)); @@ -79,7 +78,6 @@ class PeersUI { async _saveDisplayName(newDisplayName) { newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') - console.debug(newDisplayName) const savedDisplayName = await this._getSavedDisplayName(); if (newDisplayName === savedDisplayName) return; @@ -118,6 +116,8 @@ class PeersUI { _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; } @@ -548,10 +548,14 @@ class Dialog { class ReceiveDialog extends Dialog { constructor(id) { super(id); - - this.$fileDescriptionNode = this.$el.querySelector('.file-description'); - this.$fileSizeNode = this.$el.querySelector('.file-size'); - this.$previewBox = this.$el.querySelector('.file-preview') + this.$fileDescription = this.$el.querySelector('.file-description'); + this.$displayName = this.$el.querySelector('.display-name'); + this.$fileStem = this.$el.querySelector('.file-stem'); + this.$fileExtension = this.$el.querySelector('.file-extension'); + this.$fileOther = this.$el.querySelector('.file-other'); + this.$fileSize = this.$el.querySelector('.file-size'); + this.$previewBox = this.$el.querySelector('.file-preview'); + this.$receiveTitle = this.$el.querySelector('h2:first-of-type'); } _formatFileSize(bytes) { @@ -567,6 +571,26 @@ class ReceiveDialog extends Dialog { return bytes + ' Bytes'; } } + + _parseFileData(displayName, files, imagesOnly, totalSize) { + if (files.length > 1) { + let fileOtherText = ` and ${files.length - 1} other `; + if (files.length === 2) { + fileOtherText += imagesOnly ? 'image' : 'file'; + } else { + fileOtherText += imagesOnly ? 'images' : 'files'; + } + this.$fileOther.innerText = fileOtherText; + } + + const fileName = files[0].name; + const fileNameSplit = fileName.split('.'); + const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; + this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); + this.$fileExtension.innerText = fileExtension; + this.$displayName.innerText = displayName; + this.$fileSize.innerText = this._formatFileSize(totalSize); + } } class ReceiveFileDialog extends ReceiveDialog { @@ -574,24 +598,25 @@ class ReceiveFileDialog extends ReceiveDialog { constructor() { super('receive-file-dialog'); - this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download'); - this.$receiveTitleNode = this.$el.querySelector('#receive-title') + this.$downloadBtn = this.$el.querySelector('#download-btn'); + this.$shareBtn = this.$el.querySelector('#share-btn'); - Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request)); + Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize)); this._filesQueue = []; } - _onFilesReceived(sender, files, request) { - this._nextFiles(sender, files, request); + _onFilesReceived(sender, files, imagesOnly, totalSize) { + const displayName = $(sender).ui._displayName() + this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); + this._nextFiles(); window.blop.play(); } - _nextFiles(sender, nextFiles, nextRequest) { - if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest}); + _nextFiles() { if (this._busy) return; this._busy = true; - const {peerId, files, request} = this._filesQueue.shift(); - this._displayFiles(peerId, files, request); + const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift(); + this._displayFiles(peer, displayName, files, imagesOnly, totalSize); } _dequeueFile() { @@ -623,7 +648,6 @@ class ReceiveFileDialog extends ReceiveDialog { let element = document.createElement(previewElement[mime]); element.src = URL.createObjectURL(file); element.controls = true; - element.classList.add('element-preview'); element.onload = _ => { this.$previewBox.appendChild(element); resolve(true) @@ -634,30 +658,32 @@ class ReceiveFileDialog extends ReceiveDialog { }); } - async _displayFiles(peerId, files, request) { - if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback); - - let url; - let title; - let filenameDownload; - - let descriptor = request.imagesOnly ? "Image" : "File"; - - let size = this._formatFileSize(request.totalSize); - let description = files[0].name; - - let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); + async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) { + this._parseFileData(displayName, files, imagesOnly, totalSize); + let descriptor, url, filenameDownload; if (files.length === 1) { - url = URL.createObjectURL(files[0]) - title = `PairDrop - ${descriptor} Received` - filenameDownload = files[0].name; + descriptor = imagesOnly ? 'Image' : 'File'; } else { - title = `PairDrop - ${files.length} ${descriptor}s Received` - description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`; - if(files.length>2) description += "s"; + descriptor = imagesOnly ? 'Images' : 'Files'; + } + this.$receiveTitle.innerText = `${descriptor} Received`; - if(!shareInsteadOfDownload) { + const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); + if (canShare) { + this.$shareBtn.removeAttribute('hidden'); + this.$shareBtn.onclick = _ => { + navigator.share({files: files}) + .catch(err => { + console.error(err); + }); + } + } + + let downloadZipped = false; + if (files.length > 1) { + downloadZipped = true; + try { let bytesCompleted = 0; zipper.createNewZipWriter(); for (let i=0; i { Events.fire('set-progress', { peerId: peerId, - progress: (bytesCompleted + progress) / request.totalSize, + progress: (bytesCompleted + progress) / totalSize, status: 'process' }) } @@ -685,47 +711,58 @@ class ReceiveFileDialog extends ReceiveDialog { let minutes = now.getMinutes().toString(); minutes = minutes.length < 2 ? "0" + minutes : minutes; filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`; + } catch (e) { + console.error(e); + downloadZipped = false; } } - this.$receiveTitleNode.textContent = title; - this.$fileDescriptionNode.textContent = description; - this.$fileSizeNode.textContent = size; - - if (shareInsteadOfDownload) { - this.$shareOrDownloadBtn.innerText = "Share"; - this.continue = _ => { - navigator.share({files: files}) - .catch(err => console.error(err)); + this.$downloadBtn.innerText = "Download"; + this.$downloadBtn.onclick = _ => { + if (downloadZipped) { + let tmpZipBtn = document.createElement("a"); + tmpZipBtn.download = filenameDownload; + tmpZipBtn.href = url; + tmpZipBtn.click(); + } else { + this._downloadFilesIndividually(files); } - this.continueCallback = _ => this.continue(); - } else { - this.$shareOrDownloadBtn.innerText = "Download again"; - this.continue = _ => { - let tmpBtn = document.createElement("a"); - tmpBtn.download = filenameDownload; - tmpBtn.href = url; - tmpBtn.click(); - }; - this.continueCallback = _ => { - this.continue(); - this.hide(); - }; - } - this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback); + + if (!canShare) { + this.$downloadBtn.innerText = "Download again"; + } + Events.fire('notify-user', `${descriptor} downloaded successfully`); + this.$downloadBtn.style.pointerEvents = "none"; + setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); + }; this.createPreviewElement(files[0]).finally(_ => { - document.title = `PairDrop - ${files.length} Files received`; + document.title = files.length === 1 + ? 'File received - PairDrop' + : `${files.length} Files received - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); - this.show(); Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) - this.continue(); + this.show(); + + if (canShare) { + this.$shareBtn.click(); + } else { + this.$downloadBtn.click(); + } }).catch(r => console.error(r)); } + _downloadFilesIndividually(files) { + let tmpBtn = document.createElement("a"); + for (let i=0; i this._respondToFileTransferRequest(true)); @@ -773,32 +805,18 @@ class ReceiveRequestDialog extends ReceiveDialog { _showRequestDialog(request, peerId) { this.correspondingPeerId = peerId; - this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); - - const fileName = request.header[0].name; - const fileNameSplit = fileName.split('.'); - const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; - this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length); - this.$fileExtensionNode.innerText = fileExtension - - if (request.header.length >= 2) { - let fileOtherText = ` and ${request.header.length - 1} other `; - fileOtherText += request.imagesOnly ? 'image' : 'file'; - if (request.header.length > 2) fileOtherText += "s"; - this.$fileOtherNode.innerText = fileOtherText; - } - - this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize); + const displayName = $(peerId).ui._displayName(); + this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize); if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { let element = document.createElement('img'); element.src = request.thumbnailDataUrl; - element.classList.add('element-preview'); - this.$previewBox.appendChild(element) } - document.title = 'PairDrop - File Transfer Requested'; + this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` + + document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); this.show(); } @@ -833,7 +851,7 @@ class PairDeviceDialog extends Dialog { this.$clearSecretsBtn = $('clear-pair-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); let createJoinForm = this.$el.querySelector('form'); - createJoinForm.addEventListener('submit', _ => this._onSubmit()); + createJoinForm.addEventListener('submit', e => this._onSubmit(e)); this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel()) this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e))); @@ -912,7 +930,7 @@ class PairDeviceDialog extends Dialog { }) this.$submitBtn.removeAttribute("disabled"); if (document.activeElement === this.$inputRoomKeyChars[5]) { - this._onSubmit(); + this._pairDeviceJoin(this.inputRoomKey); } } } @@ -962,7 +980,8 @@ class PairDeviceDialog extends Dialog { return url.href; } - _onSubmit() { + _onSubmit(e) { + e.preventDefault(); this._pairDeviceJoin(this.inputRoomKey); } @@ -1049,14 +1068,19 @@ class ClearDevicesDialog extends Dialog { super('clear-devices-dialog'); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); let clearDevicesForm = this.$el.querySelector('form'); - clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); + clearDevicesForm.addEventListener('submit', e => this._onSubmit(e)); } _onClearPairDevices() { this.show(); } - _onSubmit() { + _onSubmit(e) { + e.preventDefault(); + this._clearRoomSecrets(); + } + + _clearRoomSecrets() { Events.fire('clear-room-secrets'); this.hide(); } @@ -1067,10 +1091,10 @@ 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('#send-text-dialog .display-name'); + this.$peerDisplayName = this.$el.querySelector('.display-name'); this.$form = this.$el.querySelector('form'); this.$submit = this.$el.querySelector('button[type="submit"]'); - this.$form.addEventListener('submit', _ => this._send()); + this.$form.addEventListener('submit', e => this._onSubmit(e)); this.$text.addEventListener('input', e => this._onChange(e)); Events.on("keydown", e => this._onKeyDown(e)); } @@ -1112,6 +1136,11 @@ class SendTextDialog extends Dialog { sel.addRange(range); } + _onSubmit(e) { + e.preventDefault(); + this._send(); + } + _send() { Events.fire('send-text', { to: this.correspondingPeerId, @@ -1135,7 +1164,7 @@ class ReceiveTextDialog extends Dialog { Events.on("keydown", e => this._onKeyDown(e)); - this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-dialog .display-name'); + this.$displayNameNode = this.$el.querySelector('.display-name'); this._receiveTextQueue = []; } @@ -1153,6 +1182,7 @@ class ReceiveTextDialog extends Dialog { _onText(text, peerId) { window.blop.play(); this._receiveTextQueue.push({text: text, peerId: peerId}); + this._setDocumentTitleMessages(); if (this.$el.attributes["show"]) return; this._dequeueRequests(); } @@ -1164,23 +1194,35 @@ class ReceiveTextDialog extends Dialog { } _showReceiveTextDialog(text, peerId) { - this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); + this.$displayNameNode.innerText = $(peerId).ui._displayName(); - if (isURL(text)) { - const $a = document.createElement('a'); - $a.href = text; - $a.target = '_blank'; - $a.textContent = text; - this.$text.innerHTML = ''; - this.$text.appendChild($a); - } else { - this.$text.textContent = text; + this.$text.innerText = text; + this.$text.classList.remove('text-center'); + + // Beautify text if text is short + if (text.length < 2000) { + // replace urls with actual links + this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => { + return `${url}`; + }); + + if (!/\s/.test(text)) { + this.$text.classList.add('text-center'); + } } - document.title = 'PairDrop - Message Received'; + + this._setDocumentTitleMessages(); + document.changeFavicon("images/favicon-96x96-notification.png"); this.show(); } + _setDocumentTitleMessages() { + document.title = !this._receiveTextQueue.length + ? 'Message Received - PairDrop' + : `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`; + } + async _onCopy() { await navigator.clipboard.writeText(this.$text.textContent); Events.fire('notify-user', 'Copied to clipboard'); @@ -1253,7 +1295,7 @@ class Base64ZipDialog extends Dialog { } _setPasteBtnToProcessing() { - this.$pasteBtn.pointerEvents = "none"; + this.$pasteBtn.style.pointerEvents = "none"; this.$pasteBtn.innerText = "Processing..."; } @@ -1398,7 +1440,7 @@ class Notifications { _messageNotification(message, peerId) { if (document.visibilityState !== 'visible') { const peerDisplayName = $(peerId).ui._displayName(); - if (isURL(message)) { + if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); this._bind(notification, _ => window.open(message, '_blank', null, true)); } else { @@ -1459,7 +1501,7 @@ class NetworkStatusUI { constructor() { Events.on('offline', _ => this._showOfflineMessage()); Events.on('online', _ => this._showOnlineMessage()); - Events.on('ws-connected', _ => this._showOnlineMessage()); + Events.on('ws-connected', _ => this._onWsConnected()); Events.on('ws-disconnected', _ => this._onWsDisconnected()); if (!navigator.onLine) this._showOfflineMessage(); } @@ -1470,17 +1512,16 @@ class NetworkStatusUI { } _showOnlineMessage() { - window.animateBackground(true); - if (!this.firstConnect) { - this.firstConnect = true; - return; - } Events.fire('notify-user', 'You are back online'); + window.animateBackground(true); + } + + _onWsConnected() { + window.animateBackground(true); } _onWsDisconnected() { window.animateBackground(false); - if (!this.firstConnect) this.firstConnect = true; } } @@ -1836,8 +1877,8 @@ Events.on('load', () => { let x0, y0, w, h, dw, offset; function init() { - w = window.innerWidth; - h = window.innerHeight; + w = document.documentElement.clientWidth; + h = document.documentElement.clientHeight; c.width = w; c.height = h; offset = $$('footer').offsetHeight - 32; diff --git a/public/service-worker.js b/public/service-worker.js index 1f1adc6..b76115f 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.1.3'; +const cacheVersion = 'v1.2.2'; const cacheTitle = `pairdrop-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public/styles.css b/public/styles.css index 2303116..bc022e6 100644 --- a/public/styles.css +++ b/public/styles.css @@ -587,7 +587,7 @@ x-dialog x-background { z-index: 10; transition: opacity 300ms; will-change: opacity; - padding: 35px; + padding: 15px; overflow: overlay; } @@ -598,19 +598,20 @@ x-dialog x-paper { padding: 16px 24px; width: 100%; max-width: 400px; + overflow: hidden; box-sizing: border-box; transition: transform 300ms; will-change: transform; } #pair-device-dialog x-paper { - position: absolute; - top: max(50%, 350px); - height: 650px; - margin-top: -325px; display: flex; flex-direction: column; - justify-content: space-between; + position: absolute; + top: max(50%, 350px); + margin-top: -328.5px; + width: calc(100vw - 20px); + height: 625px; } x-dialog:not([show]) { @@ -625,12 +626,6 @@ x-dialog:not([show]) x-background { opacity: 0; } -x-dialog .row-reverse>.button { - margin-top: 0; - margin-bottom: -16px; - width: 50%; - height: 50px; -} x-dialog a { color: var(--primary-color); @@ -669,7 +664,7 @@ x-dialog .font-subheading { } #key-input-container>input:nth-of-type(4) { - margin-left: 18px; + margin-left: 5%; } #room-key { @@ -681,16 +676,11 @@ x-dialog .font-subheading { } #room-key-qr-code { - padding: inherit; - margin: auto; - width: 150px; - height: 150px; + margin: 16px; } #pair-device-dialog hr { - margin-top: 40px; - margin-bottom: 40px; - width: 100%; + margin: 40px -24px; } #pair-device-dialog x-background { @@ -704,29 +694,24 @@ x-dialog .row { margin-bottom: 8px; } -x-dialog h2 { - margin-top: 1rem; -} - -#receive-request-dialog h2, -#receive-file-dialog h2 { - margin-bottom: 0.5rem; -} - -x-dialog .row-reverse { - margin: 40px -24px 0; +/* button row*/ +x-paper > div:last-child { + margin: auto -24px -15px; border-top: solid 2.5px var(--border-color); + height: 50px; } -.separator { - border: solid 1.25px var(--border-color); - margin-bottom: -16px; +x-paper > div:last-child > .button { + height: 100%; + width: 50%; +} + +x-paper > div:last-child > .button:not(:last-child) { + border-left: solid 2.5px var(--border-color); } .file-description { - word-break: break-word; - width: 80%; - margin: auto; + margin-bottom: 25px; } .file-description .row { @@ -738,26 +723,26 @@ x-dialog .row-reverse { word-break: normal; } -#file-name { +.file-name { font-style: italic; + max-width: 100%; } -#file-stem { - max-width: 80%; +.file-stem { overflow: hidden; text-overflow: ellipsis; - word-break: break-all; - max-height: 20px; -} - -.file-size{ - margin-bottom: 30px; + white-space: nowrap; } /* Send Text Dialog */ +x-dialog .dialog-subheader { + margin-bottom: 25px; +} + #text-input { - min-height: 120px; + min-height: 200px; + margin: 14px auto; } /* Receive Text Dialog */ @@ -765,14 +750,14 @@ x-dialog .row-reverse { #receive-text-dialog #text { width: 100%; word-break: break-all; - max-height: 300px; + max-height: calc(100vh - 393px); overflow-x: hidden; overflow-y: auto; -webkit-user-select: all; -moz-user-select: all; user-select: all; white-space: pre-wrap; - margin-top:36px; + padding: 15px 0; } #receive-text-dialog #text a { @@ -791,11 +776,7 @@ x-dialog .row-reverse { .row-separator { border-bottom: solid 2.5px var(--border-color); - margin: auto -25px; -} - -#receive-text-description-container { - margin-bottom: 25px; + margin: auto -24px; } #base64-paste-btn { @@ -823,7 +804,6 @@ x-dialog .row-reverse { padding: 2px 16px 0; box-sizing: border-box; min-height: 36px; - min-width: 100px; font-size: 14px; line-height: 24px; font-weight: 700; @@ -834,6 +814,7 @@ x-dialog .row-reverse { user-select: none; background: inherit; color: var(--primary-color); + overflow: hidden; } .button[disabled] { @@ -871,7 +852,7 @@ x-dialog .row-reverse { opacity: 0.1; } -#cancel-paste-mode-btn { +#cancel-paste-mode { z-index: 2; margin: 0; padding: 0; @@ -898,7 +879,6 @@ button::-moz-focus-inner { /* Icon Button */ - .icon-button { width: 40px; height: 40px; @@ -908,10 +888,7 @@ button::-moz-focus-inner { border-radius: 50%; } - - /* Text Input */ - .textarea { box-sizing: border-box; border: none; @@ -925,9 +902,8 @@ button::-moz-focus-inner { display: block; overflow: auto; resize: none; - min-height: 40px; line-height: 16px; - max-height: 300px; + max-height: calc(100vh - 254px); white-space: pre; } @@ -1117,6 +1093,14 @@ x-peers:empty~x-instructions { } /* Responsive Styles */ +@media screen and (max-width: 360px) { + x-dialog x-paper { + padding: 15px; + } + x-paper > div:last-child { + margin: auto -15px -15px; + } +} @media screen and (min-height: 800px) { footer { @@ -1189,7 +1173,9 @@ x-dialog x-paper { display: none; } -.element-preview { +.file-preview > img, +.file-preview > audio, +.file-preview > video { max-width: 100%; max-height: 40vh; margin: auto; diff --git a/public_included_ws_fallback/index.html b/public_included_ws_fallback/index.html index 3610ca9..a75fe69 100644 --- a/public_included_ws_fallback/index.html +++ b/public_included_ws_fallback/index.html @@ -69,7 +69,7 @@ - +
@@ -113,18 +113,17 @@
Input this key on another device
or scan the QR-Code.

- - - - - - + + + + + +
Enter key from another device to continue.
-
+
-
- Cancel +
@@ -137,9 +136,9 @@

Unpair Devices

Are you sure to unpair all devices?
-
+
- Cancel +
@@ -149,25 +148,23 @@ -

PairDrop

-
+

+
would like to share
-
- - +
+ +
-
- +
+
-
-
+
-
@@ -177,13 +174,23 @@ -

-
-
+

+
+
+ + has sent +
+
+ + +
+
+
+
-
- -
+
+ +
@@ -194,16 +201,16 @@ -

PairDrop

-
+

Send Message

+
Send a Message to
+
-
+
-
- Cancel +
@@ -213,16 +220,15 @@ -

PairDrop - Message Received

-
+

Message Received

+
- sent a message: + has sent:
-
+
-
diff --git a/public_included_ws_fallback/scripts/network.js b/public_included_ws_fallback/scripts/network.js index bf277d3..85199d4 100644 --- a/public_included_ws_fallback/scripts/network.js +++ b/public_included_ws_fallback/scripts/network.js @@ -35,6 +35,7 @@ class ServerConnection { _onOpen() { console.log('WS: server connected'); Events.fire('ws-connected'); + if (this._isReconnect) Events.fire('notify-user', 'Connected.'); } _sendRoomSecrets(roomSecrets) { @@ -137,6 +138,8 @@ class ServerConnection { this._socket.onclose = null; this._socket.close(); this._socket = null; + Events.fire('ws-disconnected'); + this._isReconnect = true; } } @@ -146,10 +149,11 @@ class ServerConnection { _onDisconnect() { console.log('WS: server disconnected'); - Events.fire('notify-user', 'No server connection. Retry in 5s...'); + Events.fire('notify-user', 'Connecting..'); clearTimeout(this._reconnectTimer); this._reconnectTimer = setTimeout(_ => this._connect(), 5000); Events.fire('ws-disconnected'); + this._isReconnect = true; } _onVisibilityChange() { @@ -315,25 +319,25 @@ class Peer { this._onChunkReceived(message); return; } - message = JSON.parse(message); - switch (message.type) { + const messageJSON = JSON.parse(message); + switch (messageJSON.type) { case 'request': - this._onFilesTransferRequest(message); + this._onFilesTransferRequest(messageJSON); break; case 'header': - this._onFilesHeader(message); + this._onFilesHeader(messageJSON); break; case 'partition': - this._onReceivedPartitionEnd(message); + this._onReceivedPartitionEnd(messageJSON); break; case 'partition-received': this._sendNextPartition(); break; case 'progress': - this._onDownloadProgress(message.progress); + this._onDownloadProgress(messageJSON.progress); break; case 'files-transfer-response': - this._onFileTransferRequestResponded(message); + this._onFileTransferRequestResponded(messageJSON); break; case 'file-transfer-complete': this._onFileTransferCompleted(); @@ -342,10 +346,10 @@ class Peer { this._onMessageTransferCompleted(); break; case 'text': - this._onTextReceived(message); + this._onTextReceived(messageJSON); break; case 'display-name-changed': - this._onDisplayNameChanged(message); + this._onDisplayNameChanged(messageJSON); break; } } @@ -439,7 +443,7 @@ class Peer { if (!this._requestAccepted.header.length) { this._busy = false; Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); - Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted}); + Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize}); this._filesReceived = []; this._requestAccepted = null; } @@ -495,17 +499,11 @@ class RTCPeer extends Peer { constructor(serverConnection, peerId, roomType, roomSecret) { super(serverConnection, peerId, roomType, roomSecret); + this.rtcSupported = true; if (!peerId) return; // we will listen for a caller 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); @@ -578,6 +576,13 @@ class RTCPeer extends Peer { Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()}); } + _onMessage(message) { + if (typeof message === 'string') { + console.log('RTC:', JSON.parse(message)); + } + super._onMessage(message); + } + getConnectionHash() { const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n"); @@ -692,6 +697,7 @@ class WSPeer extends Peer { constructor(serverConnection, peerId, roomType, roomSecret) { super(serverConnection, peerId, roomType, roomSecret); + this.rtcSupported = false; if (!peerId) return; // we will listen for a caller this._isCaller = true; this._sendSignal(); @@ -712,15 +718,15 @@ class WSPeer extends Peer { this._server.send(message); } - _sendSignal() { - this.sendJSON({type: 'signal'}); + _sendSignal(connected = false) { + this.sendJSON({type: 'signal', connected: connected}); } onServerMessage(message) { this._peerId = message.sender.id; Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()}) - if (this._isCaller) return; - this._sendSignal(); + if (message.connected) return; + this._sendSignal(true); } getConnectionHash() { @@ -745,6 +751,7 @@ class PeersManager { Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); + Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-relay', e => this._onWsRelay(e.detail)); } @@ -764,7 +771,7 @@ class PeersManager { _onWsRelay(message) { const messageJSON = JSON.parse(message) if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk); - this.peers[messageJSON.sender.id]._onMessage(message, false) + this.peers[messageJSON.sender.id]._onMessage(message) } _onPeers(msg) { @@ -808,9 +815,9 @@ class PeersManager { } _onPeerLeft(msg) { - if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) { - console.log('WSPeer left:', msg.peerId) - Events.fire('peer-disconnected', msg.peerId) + if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) { + console.log('WSPeer left:', msg.peerId); + Events.fire('peer-disconnected', msg.peerId); } else if (msg.disconnect === true) { // if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately Events.fire('peer-disconnected', msg.peerId); @@ -821,6 +828,15 @@ class PeersManager { this._notifyPeerDisplayNameChanged(peerId); } + _onWsDisconnected() { + for (const peerId in this.peers) { + console.debug(this.peers[peerId].rtcSupported); + if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) { + Events.fire('peer-disconnected', peerId); + } + } + } + _onPeerDisconnected(peerId) { const peer = this.peers[peerId]; delete this.peers[peerId]; diff --git a/public_included_ws_fallback/scripts/ui.js b/public_included_ws_fallback/scripts/ui.js index f8040bf..0699043 100644 --- a/public_included_ws_fallback/scripts/ui.js +++ b/public_included_ws_fallback/scripts/ui.js @@ -1,6 +1,5 @@ const $ = query => document.getElementById(query); const $$ = query => document.body.querySelector(query); -const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase()); window.isProductionEnvironment = !window.location.host.startsWith('localhost'); window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; window.android = /android/i.test(navigator.userAgent); @@ -28,7 +27,7 @@ class PeersUI { Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text)); this.peers = {}; - this.$cancelPasteModeBtn = $('cancel-paste-mode-btn'); + this.$cancelPasteModeBtn = $('cancel-paste-mode'); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); Events.on('dragover', e => this._onDragOver(e)); @@ -549,10 +548,14 @@ class Dialog { class ReceiveDialog extends Dialog { constructor(id) { super(id); - - this.$fileDescriptionNode = this.$el.querySelector('.file-description'); - this.$fileSizeNode = this.$el.querySelector('.file-size'); - this.$previewBox = this.$el.querySelector('.file-preview') + this.$fileDescription = this.$el.querySelector('.file-description'); + this.$displayName = this.$el.querySelector('.display-name'); + this.$fileStem = this.$el.querySelector('.file-stem'); + this.$fileExtension = this.$el.querySelector('.file-extension'); + this.$fileOther = this.$el.querySelector('.file-other'); + this.$fileSize = this.$el.querySelector('.file-size'); + this.$previewBox = this.$el.querySelector('.file-preview'); + this.$receiveTitle = this.$el.querySelector('h2:first-of-type'); } _formatFileSize(bytes) { @@ -568,6 +571,26 @@ class ReceiveDialog extends Dialog { return bytes + ' Bytes'; } } + + _parseFileData(displayName, files, imagesOnly, totalSize) { + if (files.length > 1) { + let fileOtherText = ` and ${files.length - 1} other `; + if (files.length === 2) { + fileOtherText += imagesOnly ? 'image' : 'file'; + } else { + fileOtherText += imagesOnly ? 'images' : 'files'; + } + this.$fileOther.innerText = fileOtherText; + } + + const fileName = files[0].name; + const fileNameSplit = fileName.split('.'); + const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; + this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); + this.$fileExtension.innerText = fileExtension; + this.$displayName.innerText = displayName; + this.$fileSize.innerText = this._formatFileSize(totalSize); + } } class ReceiveFileDialog extends ReceiveDialog { @@ -575,24 +598,25 @@ class ReceiveFileDialog extends ReceiveDialog { constructor() { super('receive-file-dialog'); - this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download'); - this.$receiveTitleNode = this.$el.querySelector('#receive-title') + this.$downloadBtn = this.$el.querySelector('#download-btn'); + this.$shareBtn = this.$el.querySelector('#share-btn'); - Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request)); + Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize)); this._filesQueue = []; } - _onFilesReceived(sender, files, request) { - this._nextFiles(sender, files, request); + _onFilesReceived(sender, files, imagesOnly, totalSize) { + const displayName = $(sender).ui._displayName() + this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); + this._nextFiles(); window.blop.play(); } - _nextFiles(sender, nextFiles, nextRequest) { - if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest}); + _nextFiles() { if (this._busy) return; this._busy = true; - const {peerId, files, request} = this._filesQueue.shift(); - this._displayFiles(peerId, files, request); + const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift(); + this._displayFiles(peer, displayName, files, imagesOnly, totalSize); } _dequeueFile() { @@ -624,7 +648,6 @@ class ReceiveFileDialog extends ReceiveDialog { let element = document.createElement(previewElement[mime]); element.src = URL.createObjectURL(file); element.controls = true; - element.classList.add('element-preview'); element.onload = _ => { this.$previewBox.appendChild(element); resolve(true) @@ -635,30 +658,32 @@ class ReceiveFileDialog extends ReceiveDialog { }); } - async _displayFiles(peerId, files, request) { - if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback); - - let url; - let title; - let filenameDownload; - - let descriptor = request.imagesOnly ? "Image" : "File"; - - let size = this._formatFileSize(request.totalSize); - let description = files[0].name; - - let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); + async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) { + this._parseFileData(displayName, files, imagesOnly, totalSize); + let descriptor, url, filenameDownload; if (files.length === 1) { - url = URL.createObjectURL(files[0]) - title = `PairDrop - ${descriptor} Received` - filenameDownload = files[0].name; + descriptor = imagesOnly ? 'Image' : 'File'; } else { - title = `PairDrop - ${files.length} ${descriptor}s Received` - description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`; - if(files.length>2) description += "s"; + descriptor = imagesOnly ? 'Images' : 'Files'; + } + this.$receiveTitle.innerText = `${descriptor} Received`; - if(!shareInsteadOfDownload) { + const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files}); + if (canShare) { + this.$shareBtn.removeAttribute('hidden'); + this.$shareBtn.onclick = _ => { + navigator.share({files: files}) + .catch(err => { + console.error(err); + }); + } + } + + let downloadZipped = false; + if (files.length > 1) { + downloadZipped = true; + try { let bytesCompleted = 0; zipper.createNewZipWriter(); for (let i=0; i { Events.fire('set-progress', { peerId: peerId, - progress: (bytesCompleted + progress) / request.totalSize, + progress: (bytesCompleted + progress) / totalSize, status: 'process' }) } @@ -686,47 +711,58 @@ class ReceiveFileDialog extends ReceiveDialog { let minutes = now.getMinutes().toString(); minutes = minutes.length < 2 ? "0" + minutes : minutes; filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`; + } catch (e) { + console.error(e); + downloadZipped = false; } } - this.$receiveTitleNode.textContent = title; - this.$fileDescriptionNode.textContent = description; - this.$fileSizeNode.textContent = size; - - if (shareInsteadOfDownload) { - this.$shareOrDownloadBtn.innerText = "Share"; - this.continue = _ => { - navigator.share({files: files}) - .catch(err => console.error(err)); + this.$downloadBtn.innerText = "Download"; + this.$downloadBtn.onclick = _ => { + if (downloadZipped) { + let tmpZipBtn = document.createElement("a"); + tmpZipBtn.download = filenameDownload; + tmpZipBtn.href = url; + tmpZipBtn.click(); + } else { + this._downloadFilesIndividually(files); } - this.continueCallback = _ => this.continue(); - } else { - this.$shareOrDownloadBtn.innerText = "Download again"; - this.continue = _ => { - let tmpBtn = document.createElement("a"); - tmpBtn.download = filenameDownload; - tmpBtn.href = url; - tmpBtn.click(); - }; - this.continueCallback = _ => { - this.continue(); - this.hide(); - }; - } - this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback); + + if (!canShare) { + this.$downloadBtn.innerText = "Download again"; + } + Events.fire('notify-user', `${descriptor} downloaded successfully`); + this.$downloadBtn.style.pointerEvents = "none"; + setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000); + }; this.createPreviewElement(files[0]).finally(_ => { - document.title = `PairDrop - ${files.length} Files received`; + document.title = files.length === 1 + ? 'File received - PairDrop' + : `${files.length} Files received - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); - this.show(); Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) - this.continue(); + this.show(); + + if (canShare) { + this.$shareBtn.click(); + } else { + this.$downloadBtn.click(); + } }).catch(r => console.error(r)); } + _downloadFilesIndividually(files) { + let tmpBtn = document.createElement("a"); + for (let i=0; i this._respondToFileTransferRequest(true)); @@ -774,32 +805,18 @@ class ReceiveRequestDialog extends ReceiveDialog { _showRequestDialog(request, peerId) { this.correspondingPeerId = peerId; - this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); - - const fileName = request.header[0].name; - const fileNameSplit = fileName.split('.'); - const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1]; - this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length); - this.$fileExtensionNode.innerText = fileExtension - - if (request.header.length >= 2) { - let fileOtherText = ` and ${request.header.length - 1} other `; - fileOtherText += request.imagesOnly ? 'image' : 'file'; - if (request.header.length > 2) fileOtherText += "s"; - this.$fileOtherNode.innerText = fileOtherText; - } - - this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize); + const displayName = $(peerId).ui._displayName(); + this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize); if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { let element = document.createElement('img'); element.src = request.thumbnailDataUrl; - element.classList.add('element-preview'); - this.$previewBox.appendChild(element) } - document.title = 'PairDrop - File Transfer Requested'; + this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request` + + document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`; document.changeFavicon("images/favicon-96x96-notification.png"); this.show(); } @@ -834,7 +851,7 @@ class PairDeviceDialog extends Dialog { this.$clearSecretsBtn = $('clear-pair-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); let createJoinForm = this.$el.querySelector('form'); - createJoinForm.addEventListener('submit', _ => this._onSubmit()); + createJoinForm.addEventListener('submit', e => this._onSubmit(e)); this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel()) this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e))); @@ -913,7 +930,7 @@ class PairDeviceDialog extends Dialog { }) this.$submitBtn.removeAttribute("disabled"); if (document.activeElement === this.$inputRoomKeyChars[5]) { - this._onSubmit(); + this._pairDeviceJoin(this.inputRoomKey); } } } @@ -963,7 +980,8 @@ class PairDeviceDialog extends Dialog { return url.href; } - _onSubmit() { + _onSubmit(e) { + e.preventDefault(); this._pairDeviceJoin(this.inputRoomKey); } @@ -1050,14 +1068,19 @@ class ClearDevicesDialog extends Dialog { super('clear-devices-dialog'); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); let clearDevicesForm = this.$el.querySelector('form'); - clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); + clearDevicesForm.addEventListener('submit', e => this._onSubmit(e)); } _onClearPairDevices() { this.show(); } - _onSubmit() { + _onSubmit(e) { + e.preventDefault(); + this._clearRoomSecrets(); + } + + _clearRoomSecrets() { Events.fire('clear-room-secrets'); this.hide(); } @@ -1068,10 +1091,10 @@ 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('#send-text-dialog .display-name'); + this.$peerDisplayName = this.$el.querySelector('.display-name'); this.$form = this.$el.querySelector('form'); this.$submit = this.$el.querySelector('button[type="submit"]'); - this.$form.addEventListener('submit', _ => this._send()); + this.$form.addEventListener('submit', e => this._onSubmit(e)); this.$text.addEventListener('input', e => this._onChange(e)); Events.on("keydown", e => this._onKeyDown(e)); } @@ -1113,6 +1136,11 @@ class SendTextDialog extends Dialog { sel.addRange(range); } + _onSubmit(e) { + e.preventDefault(); + this._send(); + } + _send() { Events.fire('send-text', { to: this.correspondingPeerId, @@ -1136,7 +1164,7 @@ class ReceiveTextDialog extends Dialog { Events.on("keydown", e => this._onKeyDown(e)); - this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-dialog .display-name'); + this.$displayNameNode = this.$el.querySelector('.display-name'); this._receiveTextQueue = []; } @@ -1154,6 +1182,7 @@ class ReceiveTextDialog extends Dialog { _onText(text, peerId) { window.blop.play(); this._receiveTextQueue.push({text: text, peerId: peerId}); + this._setDocumentTitleMessages(); if (this.$el.attributes["show"]) return; this._dequeueRequests(); } @@ -1165,23 +1194,35 @@ class ReceiveTextDialog extends Dialog { } _showReceiveTextDialog(text, peerId) { - this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); + this.$displayNameNode.innerText = $(peerId).ui._displayName(); - if (isURL(text)) { - const $a = document.createElement('a'); - $a.href = text; - $a.target = '_blank'; - $a.textContent = text; - this.$text.innerHTML = ''; - this.$text.appendChild($a); - } else { - this.$text.textContent = text; + this.$text.innerText = text; + this.$text.classList.remove('text-center'); + + // Beautify text if text is short + if (text.length < 2000) { + // replace urls with actual links + this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => { + return `${url}`; + }); + + if (!/\s/.test(text)) { + this.$text.classList.add('text-center'); + } } - document.title = 'PairDrop - Message Received'; + + this._setDocumentTitleMessages(); + document.changeFavicon("images/favicon-96x96-notification.png"); this.show(); } + _setDocumentTitleMessages() { + document.title = !this._receiveTextQueue.length + ? 'Message Received - PairDrop' + : `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`; + } + async _onCopy() { await navigator.clipboard.writeText(this.$text.textContent); Events.fire('notify-user', 'Copied to clipboard'); @@ -1254,7 +1295,7 @@ class Base64ZipDialog extends Dialog { } _setPasteBtnToProcessing() { - this.$pasteBtn.pointerEvents = "none"; + this.$pasteBtn.style.pointerEvents = "none"; this.$pasteBtn.innerText = "Processing..."; } @@ -1399,7 +1440,7 @@ class Notifications { _messageNotification(message, peerId) { if (document.visibilityState !== 'visible') { const peerDisplayName = $(peerId).ui._displayName(); - if (isURL(message)) { + if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) { const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message); this._bind(notification, _ => window.open(message, '_blank', null, true)); } else { @@ -1460,7 +1501,7 @@ class NetworkStatusUI { constructor() { Events.on('offline', _ => this._showOfflineMessage()); Events.on('online', _ => this._showOnlineMessage()); - Events.on('ws-connected', _ => this._showOnlineMessage()); + Events.on('ws-connected', _ => this._onWsConnected()); Events.on('ws-disconnected', _ => this._onWsDisconnected()); if (!navigator.onLine) this._showOfflineMessage(); } @@ -1471,17 +1512,16 @@ class NetworkStatusUI { } _showOnlineMessage() { - window.animateBackground(true); - if (!this.firstConnect) { - this.firstConnect = true; - return; - } Events.fire('notify-user', 'You are back online'); + window.animateBackground(true); + } + + _onWsConnected() { + window.animateBackground(true); } _onWsDisconnected() { window.animateBackground(false); - if (!this.firstConnect) this.firstConnect = true; } } @@ -1837,8 +1877,8 @@ Events.on('load', () => { let x0, y0, w, h, dw, offset; function init() { - w = window.innerWidth; - h = window.innerHeight; + w = document.documentElement.clientWidth; + h = document.documentElement.clientHeight; c.width = w; c.height = h; offset = $$('footer').offsetHeight - 32; diff --git a/public_included_ws_fallback/service-worker.js b/public_included_ws_fallback/service-worker.js index 9e968ec..c384efd 100644 --- a/public_included_ws_fallback/service-worker.js +++ b/public_included_ws_fallback/service-worker.js @@ -1,4 +1,4 @@ -const cacheVersion = 'v1.1.3'; +const cacheVersion = 'v1.2.2'; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const urlsToCache = [ 'index.html', diff --git a/public_included_ws_fallback/styles.css b/public_included_ws_fallback/styles.css index de2d6f9..4b3cc83 100644 --- a/public_included_ws_fallback/styles.css +++ b/public_included_ws_fallback/styles.css @@ -613,7 +613,7 @@ x-dialog x-background { z-index: 10; transition: opacity 300ms; will-change: opacity; - padding: 35px; + padding: 15px; overflow: overlay; } @@ -624,19 +624,20 @@ x-dialog x-paper { padding: 16px 24px; width: 100%; max-width: 400px; + overflow: hidden; box-sizing: border-box; transition: transform 300ms; will-change: transform; } #pair-device-dialog x-paper { - position: absolute; - top: max(50%, 350px); - height: 650px; - margin-top: -325px; display: flex; flex-direction: column; - justify-content: space-between; + position: absolute; + top: max(50%, 350px); + margin-top: -328.5px; + width: calc(100vw - 20px); + height: 625px; } x-dialog:not([show]) { @@ -651,12 +652,6 @@ x-dialog:not([show]) x-background { opacity: 0; } -x-dialog .row-reverse>.button { - margin-top: 0; - margin-bottom: -16px; - width: 50%; - height: 50px; -} x-dialog a { color: var(--primary-color); @@ -695,7 +690,7 @@ x-dialog .font-subheading { } #key-input-container>input:nth-of-type(4) { - margin-left: 18px; + margin-left: 5%; } #room-key { @@ -707,16 +702,11 @@ x-dialog .font-subheading { } #room-key-qr-code { - padding: inherit; - margin: auto; - width: 150px; - height: 150px; + margin: 16px; } #pair-device-dialog hr { - margin-top: 40px; - margin-bottom: 40px; - width: 100%; + margin: 40px -24px; } #pair-device-dialog x-background { @@ -730,29 +720,24 @@ x-dialog .row { margin-bottom: 8px; } -x-dialog h2 { - margin-top: 1rem; -} - -#receive-request-dialog h2, -#receive-file-dialog h2 { - margin-bottom: 0.5rem; -} - -x-dialog .row-reverse { - margin: 40px -24px 0; +/* button row*/ +x-paper > div:last-child { + margin: auto -24px -15px; border-top: solid 2.5px var(--border-color); + height: 50px; } -.separator { - border: solid 1.25px var(--border-color); - margin-bottom: -16px; +x-paper > div:last-child > .button { + height: 100%; + width: 50%; +} + +x-paper > div:last-child > .button:not(:last-child) { + border-left: solid 2.5px var(--border-color); } .file-description { - word-break: break-word; - width: 80%; - margin: auto; + margin-bottom: 25px; } .file-description .row { @@ -764,26 +749,26 @@ x-dialog .row-reverse { word-break: normal; } -#file-name { +.file-name { font-style: italic; + max-width: 100%; } -#file-stem { - max-width: 80%; +.file-stem { overflow: hidden; text-overflow: ellipsis; - word-break: break-all; - max-height: 20px; -} - -.file-size{ - margin-bottom: 30px; + white-space: nowrap; } /* Send Text Dialog */ +x-dialog .dialog-subheader { + margin-bottom: 25px; +} + #text-input { - min-height: 120px; + min-height: 200px; + margin: 14px auto; } /* Receive Text Dialog */ @@ -791,14 +776,14 @@ x-dialog .row-reverse { #receive-text-dialog #text { width: 100%; word-break: break-all; - max-height: 300px; + max-height: calc(100vh - 393px); overflow-x: hidden; overflow-y: auto; -webkit-user-select: all; -moz-user-select: all; user-select: all; white-space: pre-wrap; - margin-top:36px; + padding: 15px 0; } #receive-text-dialog #text a { @@ -817,11 +802,7 @@ x-dialog .row-reverse { .row-separator { border-bottom: solid 2.5px var(--border-color); - margin: auto -25px; -} - -#receive-text-description-container { - margin-bottom: 25px; + margin: auto -24px; } #base64-paste-btn { @@ -849,7 +830,6 @@ x-dialog .row-reverse { padding: 2px 16px 0; box-sizing: border-box; min-height: 36px; - min-width: 100px; font-size: 14px; line-height: 24px; font-weight: 700; @@ -860,6 +840,7 @@ x-dialog .row-reverse { user-select: none; background: inherit; color: var(--primary-color); + overflow: hidden; } .button[disabled] { @@ -897,7 +878,7 @@ x-dialog .row-reverse { opacity: 0.1; } -#cancel-paste-mode-btn { +#cancel-paste-mode { z-index: 2; margin: 0; padding: 0; @@ -924,7 +905,6 @@ button::-moz-focus-inner { /* Icon Button */ - .icon-button { width: 40px; height: 40px; @@ -934,10 +914,7 @@ button::-moz-focus-inner { border-radius: 50%; } - - /* Text Input */ - .textarea { box-sizing: border-box; border: none; @@ -951,9 +928,8 @@ button::-moz-focus-inner { display: block; overflow: auto; resize: none; - min-height: 40px; line-height: 16px; - max-height: 300px; + max-height: calc(100vh - 254px); white-space: pre; } @@ -1143,6 +1119,14 @@ x-peers:empty~x-instructions { } /* Responsive Styles */ +@media screen and (max-width: 360px) { + x-dialog x-paper { + padding: 15px; + } + x-paper > div:last-child { + margin: auto -15px -15px; + } +} @media screen and (min-height: 800px) { #websocket-fallback { @@ -1215,7 +1199,9 @@ x-dialog x-paper { display: none; } -.element-preview { +.file-preview > img, +.file-preview > audio, +.file-preview > video { max-width: 100%; max-height: 40vh; margin: auto;