merge master into branch

This commit is contained in:
schlagmichdoch 2023-03-03 17:40:10 +01:00
commit 002b31a113
13 changed files with 585 additions and 498 deletions

View file

@ -1,6 +1,7 @@
const process = require('process') const process = require('process')
const crypto = require('crypto') const crypto = require('crypto')
const {spawn} = require('child_process') const {spawn} = require('child_process')
const WebSocket = require('ws');
// Handle SIGINT // Handle SIGINT
process.on('SIGINT', () => { process.on('SIGINT', () => {
@ -99,7 +100,6 @@ const { uniqueNamesGenerator, animals, colors } = require('unique-names-generato
class PairDropServer { class PairDropServer {
constructor() { constructor() {
const WebSocket = require('ws');
this._wss = new WebSocket.Server({ server }); this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request))); this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
@ -110,10 +110,10 @@ class PairDropServer {
} }
_onConnection(peer) { _onConnection(peer) {
this._joinRoom(peer);
peer.socket.on('message', message => this._onMessage(peer, message)); peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e); peer.socket.onerror = e => console.error(e);
this._keepAlive(peer); this._keepAlive(peer);
this._joinRoom(peer);
// send displayName // send displayName
this._send(peer, { this._send(peer, {
@ -317,6 +317,10 @@ class PairDropServer {
_joinRoom(peer, roomType = 'ip', roomSecret = '') { _joinRoom(peer, roomType = 'ip', roomSecret = '') {
const room = roomType === 'ip' ? peer.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 room doesn't exist, create it
if (!this._rooms[room]) { if (!this._rooms[room]) {
this._rooms[room] = {}; this._rooms[room] = {};
@ -341,10 +345,6 @@ class PairDropServer {
// delete the peer // delete the peer
delete this._rooms[room][peer.id]; delete this._rooms[room][peer.id];
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room //if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) { if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room]; delete this._rooms[room];

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.1.3", "version": "1.2.2",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pairdrop", "name": "pairdrop",
"version": "1.1.3", "version": "1.2.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.1.3", "version": "1.2.2",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View file

@ -69,7 +69,7 @@
<use xlink:href="#clear-pair-devices-icon" /> <use xlink:href="#clear-pair-devices-icon" />
</svg> </svg>
</a> </a>
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a> <a id="cancel-paste-mode" class="button" hidden>Done</a>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center"> <div id="center">
@ -110,18 +110,17 @@
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div> <div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr> <hr>
<div id="key-input-container"> <div id="key-input-container">
<input id="char0" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input id="char1" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char2" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char3" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char4" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char5" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div> <div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" disabled>Pair</button>
<div class="separator"></div> <button class="button" close>Cancel</button>
<a class="button" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -134,9 +133,9 @@
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Unpair Devices</h2> <h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div> <div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button> <button class="button" type="submit">Unpair Devices</button>
<a class="button" close>Cancel</a> <button class="button" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -146,25 +145,23 @@
<x-dialog id="receive-request-dialog"> <x-dialog id="receive-request-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">PairDrop</h2> <h2 class="center"></h2>
<div class="text-center file-description"> <div class="center column file-description">
<div> <div>
<span class="display-name"></span> <span class="display-name"></span>
<span>would like to share</span> <span>would like to share</span>
</div> </div>
<div id="file-name" class="row" > <div class="row file-name" >
<span id="file-stem"></span> <span class="file-stem"></span>
<span id="file-extension"></span> <span class="file-extension"></span>
</div> </div>
<div class="row"> <div class="row file-other">
<span id="file-other"></span>
</div> </div>
<div class="row font-body2 file-size"></div>
</div> </div>
<div class="font-body2 text-center file-size"></div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<div class="separator"></div>
<button id="decline-request" class="button" title="ESCAPE">Decline</button> <button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div> </div>
</x-paper> </x-paper>
@ -174,13 +171,23 @@
<x-dialog id="receive-file-dialog"> <x-dialog id="receive-file-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 id="receive-title" class="center"></h2> <h2 class="center"></h2>
<div class="text-center file-description"></div> <div class="center column file-description">
<div class="font-body2 text-center file-size"></div> <div>
<span class="display-name"></span>
<span>has sent</span>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<a id="share-or-download" class="button" autofocus></a> <button id="share-btn" class="button" autofocus hidden>Share</button>
<div class="separator"></div> <button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button> <button class="button" close>Close</button>
</div> </div>
</x-paper> </x-paper>
@ -191,16 +198,16 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">PairDrop</h2> <h2 class="text-center">Send Message</h2>
<div class="text-center"> <div class="dialog-subheader text-center">
<span>Send a Message to</span> <span>Send a Message to</span>
<span class="display-name"></span> <span class="display-name"></span>
</div> </div>
<div class="row-separator"></div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button> <button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<div class="separator"></div> <button class="button" title="ESCAPE" close>Cancel</button>
<a class="button" title="ESCAPE" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -210,16 +217,15 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2>PairDrop - Message Received</h2> <h2 class="text-center">Message Received</h2>
<div id="receive-text-description-container"> <div class="text-center dialog-subheader">
<span class="display-name"></span> <span class="display-name"></span>
<span>sent a message:</span> <span>has sent:</span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="row-reverse"> <div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button id="close" class="button" title="ESCAPE">Close</button> <button id="close" class="button" title="ESCAPE">Close</button>
</div> </div>
</x-paper> </x-paper>

View file

@ -37,6 +37,7 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
} }
_sendRoomSecrets(roomSecrets) { _sendRoomSecrets(roomSecrets) {
@ -126,6 +127,8 @@ class ServerConnection {
this._socket.onclose = null; this._socket.onclose = null;
this._socket.close(); this._socket.close();
this._socket = null; this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
} }
} }
@ -135,10 +138,11 @@ class ServerConnection {
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...'); Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 5000); this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
_onVisibilityChange() { _onVisibilityChange() {
@ -304,25 +308,25 @@ class Peer {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
message = JSON.parse(message); const messageJSON = JSON.parse(message);
switch (message.type) { switch (messageJSON.type) {
case 'request': case 'request':
this._onFilesTransferRequest(message); this._onFilesTransferRequest(messageJSON);
break; break;
case 'header': case 'header':
this._onFilesHeader(message); this._onFilesHeader(messageJSON);
break; break;
case 'partition': case 'partition':
this._onReceivedPartitionEnd(message); this._onReceivedPartitionEnd(messageJSON);
break; break;
case 'partition-received': case 'partition-received':
this._sendNextPartition(); this._sendNextPartition();
break; break;
case 'progress': case 'progress':
this._onDownloadProgress(message.progress); this._onDownloadProgress(messageJSON.progress);
break; break;
case 'files-transfer-response': case 'files-transfer-response':
this._onFileTransferRequestResponded(message); this._onFileTransferRequestResponded(messageJSON);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted();
@ -331,10 +335,10 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(message); this._onTextReceived(messageJSON);
break; break;
case 'display-name-changed': case 'display-name-changed':
this._onDisplayNameChanged(message); this._onDisplayNameChanged(messageJSON);
break; break;
} }
} }
@ -428,7 +432,7 @@ class Peer {
if (!this._requestAccepted.header.length) { if (!this._requestAccepted.header.length) {
this._busy = false; this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); 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._filesReceived = [];
this._requestAccepted = null; this._requestAccepted = null;
} }
@ -476,6 +480,7 @@ class Peer {
_onDisplayNameChanged(message) { _onDisplayNameChanged(message) {
if (!message.displayName) return; if (!message.displayName) return;
console.debug(message)
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName}); 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) { constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller if (!peerId) return; // we will listen for a caller
this._connect(peerId, true); this._connect(peerId, true);
} }
_onMessage(message) {
if (typeof message !== 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
}
_connect(peerId, isCaller) { _connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(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()}); 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() { getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n"); const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");

View file

@ -1,6 +1,5 @@
const $ = query => document.getElementById(query); const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(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.isProductionEnvironment = !window.location.host.startsWith('localhost');
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent); 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)); Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {}; this.peers = {};
this.$cancelPasteModeBtn = $('cancel-paste-mode-btn'); this.$cancelPasteModeBtn = $('cancel-paste-mode');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e)); Events.on('dragover', e => this._onDragOver(e));
@ -79,7 +78,6 @@ class PeersUI {
async _saveDisplayName(newDisplayName) { async _saveDisplayName(newDisplayName) {
newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '') newDisplayName = newDisplayName.replace(/(\n|\r|\r\n)/, '')
console.debug(newDisplayName)
const savedDisplayName = await this._getSavedDisplayName(); const savedDisplayName = await this._getSavedDisplayName();
if (newDisplayName === savedDisplayName) return; if (newDisplayName === savedDisplayName) return;
@ -118,6 +116,8 @@ class PeersUI {
_changePeerDisplayName(peerId, displayName) { _changePeerDisplayName(peerId, displayName) {
this.peers[peerId].name.displayName = displayName; this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId); const peerIdNode = $(peerId);
console.debug(peerIdNode)
console.debug(displayName)
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName; if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
} }
@ -548,10 +548,14 @@ class Dialog {
class ReceiveDialog extends Dialog { class ReceiveDialog extends Dialog {
constructor(id) { constructor(id) {
super(id); super(id);
this.$fileDescription = this.$el.querySelector('.file-description');
this.$fileDescriptionNode = this.$el.querySelector('.file-description'); this.$displayName = this.$el.querySelector('.display-name');
this.$fileSizeNode = this.$el.querySelector('.file-size'); this.$fileStem = this.$el.querySelector('.file-stem');
this.$previewBox = this.$el.querySelector('.file-preview') 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) { _formatFileSize(bytes) {
@ -567,6 +571,26 @@ class ReceiveDialog extends Dialog {
return bytes + ' Bytes'; 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 { class ReceiveFileDialog extends ReceiveDialog {
@ -574,24 +598,25 @@ class ReceiveFileDialog extends ReceiveDialog {
constructor() { constructor() {
super('receive-file-dialog'); super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download'); this.$downloadBtn = this.$el.querySelector('#download-btn');
this.$receiveTitleNode = this.$el.querySelector('#receive-title') 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 = []; this._filesQueue = [];
} }
_onFilesReceived(sender, files, request) { _onFilesReceived(sender, files, imagesOnly, totalSize) {
this._nextFiles(sender, files, request); const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play(); window.blop.play();
} }
_nextFiles(sender, nextFiles, nextRequest) { _nextFiles() {
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
if (this._busy) return; if (this._busy) return;
this._busy = true; this._busy = true;
const {peerId, files, request} = this._filesQueue.shift(); const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peerId, files, request); this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
} }
_dequeueFile() { _dequeueFile() {
@ -623,7 +648,6 @@ class ReceiveFileDialog extends ReceiveDialog {
let element = document.createElement(previewElement[mime]); let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file); element.src = URL.createObjectURL(file);
element.controls = true; element.controls = true;
element.classList.add('element-preview');
element.onload = _ => { element.onload = _ => {
this.$previewBox.appendChild(element); this.$previewBox.appendChild(element);
resolve(true) resolve(true)
@ -634,30 +658,32 @@ class ReceiveFileDialog extends ReceiveDialog {
}); });
} }
async _displayFiles(peerId, files, request) { async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback); this._parseFileData(displayName, files, imagesOnly, totalSize);
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});
let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
url = URL.createObjectURL(files[0]) descriptor = imagesOnly ? 'Image' : 'File';
title = `PairDrop - ${descriptor} Received`
filenameDownload = files[0].name;
} else { } else {
title = `PairDrop - ${files.length} ${descriptor}s Received` descriptor = imagesOnly ? 'Images' : 'Files';
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`; }
if(files.length>2) description += "s"; 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; let bytesCompleted = 0;
zipper.createNewZipWriter(); zipper.createNewZipWriter();
for (let i=0; i<files.length; i++) { for (let i=0; i<files.length; i++) {
@ -665,7 +691,7 @@ class ReceiveFileDialog extends ReceiveDialog {
onprogress: (progress) => { onprogress: (progress) => {
Events.fire('set-progress', { Events.fire('set-progress', {
peerId: peerId, peerId: peerId,
progress: (bytesCompleted + progress) / request.totalSize, progress: (bytesCompleted + progress) / totalSize,
status: 'process' status: 'process'
}) })
} }
@ -685,47 +711,58 @@ class ReceiveFileDialog extends ReceiveDialog {
let minutes = now.getMinutes().toString(); let minutes = now.getMinutes().toString();
minutes = minutes.length < 2 ? "0" + minutes : minutes; minutes = minutes.length < 2 ? "0" + minutes : minutes;
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`; filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
} catch (e) {
console.error(e);
downloadZipped = false;
} }
} }
this.$receiveTitleNode.textContent = title; this.$downloadBtn.innerText = "Download";
this.$fileDescriptionNode.textContent = description; this.$downloadBtn.onclick = _ => {
this.$fileSizeNode.textContent = size; if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
if (shareInsteadOfDownload) { tmpZipBtn.download = filenameDownload;
this.$shareOrDownloadBtn.innerText = "Share"; tmpZipBtn.href = url;
this.continue = _ => { tmpZipBtn.click();
navigator.share({files: files}) } else {
.catch(err => console.error(err)); this._downloadFilesIndividually(files);
} }
this.continueCallback = _ => this.continue();
} else { if (!canShare) {
this.$shareOrDownloadBtn.innerText = "Download again"; this.$downloadBtn.innerText = "Download again";
this.continue = _ => { }
let tmpBtn = document.createElement("a"); Events.fire('notify-user', `${descriptor} downloaded successfully`);
tmpBtn.download = filenameDownload; this.$downloadBtn.style.pointerEvents = "none";
tmpBtn.href = url; setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
tmpBtn.click(); };
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
this.createPreviewElement(files[0]).finally(_ => { 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"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) 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)); }).catch(r => console.error(r));
} }
_downloadFilesIndividually(files) {
let tmpBtn = document.createElement("a");
for (let i=0; i<files.length; i++) {
tmpBtn.download = files[i].name;
tmpBtn.href = URL.createObjectURL(files[i]);
tmpBtn.click();
}
}
hide() { hide() {
this.$shareOrDownloadBtn.removeAttribute('href'); this.$shareBtn.setAttribute('hidden', '');
this.$shareOrDownloadBtn.removeAttribute('download');
this.$previewBox.innerHTML = ''; this.$previewBox.innerHTML = '';
super.hide(); super.hide();
this._dequeueFile(); this._dequeueFile();
@ -737,11 +774,6 @@ class ReceiveRequestDialog extends ReceiveDialog {
constructor() { constructor() {
super('receive-request-dialog'); super('receive-request-dialog');
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');
this.$acceptRequestBtn = this.$el.querySelector('#accept-request'); this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
this.$declineRequestBtn = this.$el.querySelector('#decline-request'); this.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true)); this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
@ -773,32 +805,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
_showRequestDialog(request, peerId) { _showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
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);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img'); let element = document.createElement('img');
element.src = request.thumbnailDataUrl; element.src = request.thumbnailDataUrl;
element.classList.add('element-preview');
this.$previewBox.appendChild(element) 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"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show(); this.show();
} }
@ -833,7 +851,7 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn = $('clear-pair-devices'); this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form'); 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.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e))); this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
@ -912,7 +930,7 @@ class PairDeviceDialog extends Dialog {
}) })
this.$submitBtn.removeAttribute("disabled"); this.$submitBtn.removeAttribute("disabled");
if (document.activeElement === this.$inputRoomKeyChars[5]) { if (document.activeElement === this.$inputRoomKeyChars[5]) {
this._onSubmit(); this._pairDeviceJoin(this.inputRoomKey);
} }
} }
} }
@ -962,7 +980,8 @@ class PairDeviceDialog extends Dialog {
return url.href; return url.href;
} }
_onSubmit() { _onSubmit(e) {
e.preventDefault();
this._pairDeviceJoin(this.inputRoomKey); this._pairDeviceJoin(this.inputRoomKey);
} }
@ -1049,14 +1068,19 @@ class ClearDevicesDialog extends Dialog {
super('clear-devices-dialog'); super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form'); let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
} }
_onClearPairDevices() { _onClearPairDevices() {
this.show(); this.show();
} }
_onSubmit() { _onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets'); Events.fire('clear-room-secrets');
this.hide(); this.hide();
} }
@ -1067,10 +1091,10 @@ class SendTextDialog extends Dialog {
super('send-text-dialog'); super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input'); 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.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]'); 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)); this.$text.addEventListener('input', e => this._onChange(e));
Events.on("keydown", e => this._onKeyDown(e)); Events.on("keydown", e => this._onKeyDown(e));
} }
@ -1112,6 +1136,11 @@ class SendTextDialog extends Dialog {
sel.addRange(range); sel.addRange(range);
} }
_onSubmit(e) {
e.preventDefault();
this._send();
}
_send() { _send() {
Events.fire('send-text', { Events.fire('send-text', {
to: this.correspondingPeerId, to: this.correspondingPeerId,
@ -1135,7 +1164,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e)); 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 = []; this._receiveTextQueue = [];
} }
@ -1153,6 +1182,7 @@ class ReceiveTextDialog extends Dialog {
_onText(text, peerId) { _onText(text, peerId) {
window.blop.play(); window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId}); this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return; if (this.$el.attributes["show"]) return;
this._dequeueRequests(); this._dequeueRequests();
} }
@ -1164,23 +1194,35 @@ class ReceiveTextDialog extends Dialog {
} }
_showReceiveTextDialog(text, peerId) { _showReceiveTextDialog(text, peerId) {
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); this.$displayNameNode.innerText = $(peerId).ui._displayName();
if (isURL(text)) { this.$text.innerText = text;
const $a = document.createElement('a'); this.$text.classList.remove('text-center');
$a.href = text;
$a.target = '_blank'; // Beautify text if text is short
$a.textContent = text; if (text.length < 2000) {
this.$text.innerHTML = ''; // replace urls with actual links
this.$text.appendChild($a); this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
} else { return `<a href="${url}" target="_blank">${url}</a>`;
this.$text.textContent = text; });
if (!/\s/.test(text)) {
this.$text.classList.add('text-center');
}
} }
document.title = 'PairDrop - Message Received';
this._setDocumentTitleMessages();
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show(); this.show();
} }
_setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop'
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
}
async _onCopy() { async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent); await navigator.clipboard.writeText(this.$text.textContent);
Events.fire('notify-user', 'Copied to clipboard'); Events.fire('notify-user', 'Copied to clipboard');
@ -1253,7 +1295,7 @@ class Base64ZipDialog extends Dialog {
} }
_setPasteBtnToProcessing() { _setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none"; this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = "Processing...";
} }
@ -1398,7 +1440,7 @@ class Notifications {
_messageNotification(message, peerId) { _messageNotification(message, peerId) {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName(); 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); const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
this._bind(notification, _ => window.open(message, '_blank', null, true)); this._bind(notification, _ => window.open(message, '_blank', null, true));
} else { } else {
@ -1459,7 +1501,7 @@ class NetworkStatusUI {
constructor() { constructor() {
Events.on('offline', _ => this._showOfflineMessage()); Events.on('offline', _ => this._showOfflineMessage());
Events.on('online', _ => this._showOnlineMessage()); Events.on('online', _ => this._showOnlineMessage());
Events.on('ws-connected', _ => this._showOnlineMessage()); Events.on('ws-connected', _ => this._onWsConnected());
Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-disconnected', _ => this._onWsDisconnected());
if (!navigator.onLine) this._showOfflineMessage(); if (!navigator.onLine) this._showOfflineMessage();
} }
@ -1470,17 +1512,16 @@ class NetworkStatusUI {
} }
_showOnlineMessage() { _showOnlineMessage() {
window.animateBackground(true);
if (!this.firstConnect) {
this.firstConnect = true;
return;
}
Events.fire('notify-user', 'You are back online'); Events.fire('notify-user', 'You are back online');
window.animateBackground(true);
}
_onWsConnected() {
window.animateBackground(true);
} }
_onWsDisconnected() { _onWsDisconnected() {
window.animateBackground(false); window.animateBackground(false);
if (!this.firstConnect) this.firstConnect = true;
} }
} }
@ -1836,8 +1877,8 @@ Events.on('load', () => {
let x0, y0, w, h, dw, offset; let x0, y0, w, h, dw, offset;
function init() { function init() {
w = window.innerWidth; w = document.documentElement.clientWidth;
h = window.innerHeight; h = document.documentElement.clientHeight;
c.width = w; c.width = w;
c.height = h; c.height = h;
offset = $$('footer').offsetHeight - 32; offset = $$('footer').offsetHeight - 32;

View file

@ -1,4 +1,4 @@
const cacheVersion = 'v1.1.3'; const cacheVersion = 'v1.2.2';
const cacheTitle = `pairdrop-cache-${cacheVersion}`; const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',

View file

@ -587,7 +587,7 @@ x-dialog x-background {
z-index: 10; z-index: 10;
transition: opacity 300ms; transition: opacity 300ms;
will-change: opacity; will-change: opacity;
padding: 35px; padding: 15px;
overflow: overlay; overflow: overlay;
} }
@ -598,19 +598,20 @@ x-dialog x-paper {
padding: 16px 24px; padding: 16px 24px;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
transition: transform 300ms; transition: transform 300ms;
will-change: transform; will-change: transform;
} }
#pair-device-dialog x-paper { #pair-device-dialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
display: flex; display: flex;
flex-direction: column; 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]) { x-dialog:not([show]) {
@ -625,12 +626,6 @@ x-dialog:not([show]) x-background {
opacity: 0; opacity: 0;
} }
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a { x-dialog a {
color: var(--primary-color); color: var(--primary-color);
@ -669,7 +664,7 @@ x-dialog .font-subheading {
} }
#key-input-container>input:nth-of-type(4) { #key-input-container>input:nth-of-type(4) {
margin-left: 18px; margin-left: 5%;
} }
#room-key { #room-key {
@ -681,16 +676,11 @@ x-dialog .font-subheading {
} }
#room-key-qr-code { #room-key-qr-code {
padding: inherit; margin: 16px;
margin: auto;
width: 150px;
height: 150px;
} }
#pair-device-dialog hr { #pair-device-dialog hr {
margin-top: 40px; margin: 40px -24px;
margin-bottom: 40px;
width: 100%;
} }
#pair-device-dialog x-background { #pair-device-dialog x-background {
@ -704,29 +694,24 @@ x-dialog .row {
margin-bottom: 8px; margin-bottom: 8px;
} }
x-dialog h2 { /* button row*/
margin-top: 1rem; x-paper > div:last-child {
} margin: auto -24px -15px;
#receive-request-dialog h2,
#receive-file-dialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px 0;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
height: 50px;
} }
.separator { x-paper > div:last-child > .button {
border: solid 1.25px var(--border-color); height: 100%;
margin-bottom: -16px; width: 50%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
} }
.file-description { .file-description {
word-break: break-word; margin-bottom: 25px;
width: 80%;
margin: auto;
} }
.file-description .row { .file-description .row {
@ -738,26 +723,26 @@ x-dialog .row-reverse {
word-break: normal; word-break: normal;
} }
#file-name { .file-name {
font-style: italic; font-style: italic;
max-width: 100%;
} }
#file-stem { .file-stem {
max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-all; white-space: nowrap;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
} }
/* Send Text Dialog */ /* Send Text Dialog */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input { #text-input {
min-height: 120px; min-height: 200px;
margin: 14px auto;
} }
/* Receive Text Dialog */ /* Receive Text Dialog */
@ -765,14 +750,14 @@ x-dialog .row-reverse {
#receive-text-dialog #text { #receive-text-dialog #text {
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: calc(100vh - 393px);
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-user-select: all; -webkit-user-select: all;
-moz-user-select: all; -moz-user-select: all;
user-select: all; user-select: all;
white-space: pre-wrap; white-space: pre-wrap;
margin-top:36px; padding: 15px 0;
} }
#receive-text-dialog #text a { #receive-text-dialog #text a {
@ -791,11 +776,7 @@ x-dialog .row-reverse {
.row-separator { .row-separator {
border-bottom: solid 2.5px var(--border-color); border-bottom: solid 2.5px var(--border-color);
margin: auto -25px; margin: auto -24px;
}
#receive-text-description-container {
margin-bottom: 25px;
} }
#base64-paste-btn { #base64-paste-btn {
@ -823,7 +804,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0; padding: 2px 16px 0;
box-sizing: border-box; box-sizing: border-box;
min-height: 36px; min-height: 36px;
min-width: 100px;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
font-weight: 700; font-weight: 700;
@ -834,6 +814,7 @@ x-dialog .row-reverse {
user-select: none; user-select: none;
background: inherit; background: inherit;
color: var(--primary-color); color: var(--primary-color);
overflow: hidden;
} }
.button[disabled] { .button[disabled] {
@ -871,7 +852,7 @@ x-dialog .row-reverse {
opacity: 0.1; opacity: 0.1;
} }
#cancel-paste-mode-btn { #cancel-paste-mode {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -898,7 +879,6 @@ button::-moz-focus-inner {
/* Icon Button */ /* Icon Button */
.icon-button { .icon-button {
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -908,10 +888,7 @@ button::-moz-focus-inner {
border-radius: 50%; border-radius: 50%;
} }
/* Text Input */ /* Text Input */
.textarea { .textarea {
box-sizing: border-box; box-sizing: border-box;
border: none; border: none;
@ -925,9 +902,8 @@ button::-moz-focus-inner {
display: block; display: block;
overflow: auto; overflow: auto;
resize: none; resize: none;
min-height: 40px;
line-height: 16px; line-height: 16px;
max-height: 300px; max-height: calc(100vh - 254px);
white-space: pre; white-space: pre;
} }
@ -1117,6 +1093,14 @@ x-peers:empty~x-instructions {
} }
/* Responsive Styles */ /* 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) { @media screen and (min-height: 800px) {
footer { footer {
@ -1189,7 +1173,9 @@ x-dialog x-paper {
display: none; display: none;
} }
.element-preview { .file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%; max-width: 100%;
max-height: 40vh; max-height: 40vh;
margin: auto; margin: auto;

View file

@ -69,7 +69,7 @@
<use xlink:href="#clear-pair-devices-icon" /> <use xlink:href="#clear-pair-devices-icon" />
</svg> </svg>
</a> </a>
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a> <a id="cancel-paste-mode" class="button" hidden>Done</a>
</header> </header>
<!-- Center --> <!-- Center -->
<div id="center"> <div id="center">
@ -113,18 +113,17 @@
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div> <div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr> <hr>
<div id="key-input-container"> <div id="key-input-container">
<input id="char0" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" autofocus contenteditable placeholder="" disabled>
<input id="char1" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char2" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char3" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char4" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
<input id="char5" type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled> <input type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</div> </div>
<div class="font-subheading center text-center">Enter key from another device to continue.</div> <div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button> <button class="button" type="submit" disabled>Pair</button>
<div class="separator"></div> <button class="button" close>Cancel</button>
<a class="button" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -137,9 +136,9 @@
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">Unpair Devices</h2> <h2 class="center">Unpair Devices</h2>
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div> <div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button> <button class="button" type="submit">Unpair Devices</button>
<a class="button" close>Cancel</a> <button class="button" close>Cancel</button>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -149,25 +148,23 @@
<x-dialog id="receive-request-dialog"> <x-dialog id="receive-request-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="center">PairDrop</h2> <h2 class="center"></h2>
<div class="text-center file-description"> <div class="center column file-description">
<div> <div>
<span class="display-name"></span> <span class="display-name"></span>
<span>would like to share</span> <span>would like to share</span>
</div> </div>
<div id="file-name" class="row" > <div class="row file-name" >
<span id="file-stem"></span> <span class="file-stem"></span>
<span id="file-extension"></span> <span class="file-extension"></span>
</div> </div>
<div class="row"> <div class="row file-other">
<span id="file-other"></span>
</div> </div>
<div class="row font-body2 file-size"></div>
</div> </div>
<div class="font-body2 text-center file-size"></div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<button id="accept-request" class="button" title="ENTER" autofocus>Accept</button> <button id="accept-request" class="button" title="ENTER" autofocus>Accept</button>
<div class="separator"></div>
<button id="decline-request" class="button" title="ESCAPE">Decline</button> <button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div> </div>
</x-paper> </x-paper>
@ -177,13 +174,23 @@
<x-dialog id="receive-file-dialog"> <x-dialog id="receive-file-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 id="receive-title" class="center"></h2> <h2 class="center"></h2>
<div class="text-center file-description"></div> <div class="center column file-description">
<div class="font-body2 text-center file-size"></div> <div>
<span class="display-name"></span>
<span>has sent</span>
</div>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row file-other"></div>
<div class="row font-body2 file-size"></div>
</div>
<div class="center file-preview"></div> <div class="center file-preview"></div>
<div class="row-reverse space-between"> <div class="center row-reverse">
<a id="share-or-download" class="button" autofocus></a> <button id="share-btn" class="button" autofocus hidden>Share</button>
<div class="separator"></div> <button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button> <button class="button" close>Close</button>
</div> </div>
</x-paper> </x-paper>
@ -194,16 +201,16 @@
<form action="#"> <form action="#">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2 class="text-center">PairDrop</h2> <h2 class="text-center">Send Message</h2>
<div class="text-center"> <div class="dialog-subheader text-center">
<span>Send a Message to</span> <span>Send a Message to</span>
<span class="display-name"></span> <span class="display-name"></span>
</div> </div>
<div class="row-separator"></div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div> <div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="row-reverse"> <div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button> <button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<div class="separator"></div> <button class="button" title="ESCAPE" close>Cancel</button>
<a class="button" title="ESCAPE" close>Cancel</a>
</div> </div>
</x-paper> </x-paper>
</x-background> </x-background>
@ -213,16 +220,15 @@
<x-dialog id="receive-text-dialog"> <x-dialog id="receive-text-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<h2>PairDrop - Message Received</h2> <h2 class="text-center">Message Received</h2>
<div id="receive-text-description-container"> <div class="text-center dialog-subheader">
<span class="display-name"></span> <span class="display-name"></span>
<span>sent a message:</span> <span>has sent:</span>
</div> </div>
<div class="row-separator"></div> <div class="row-separator"></div>
<div id="text"></div> <div id="text"></div>
<div class="row-reverse"> <div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button> <button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button id="close" class="button" title="ESCAPE">Close</button> <button id="close" class="button" title="ESCAPE">Close</button>
</div> </div>
</x-paper> </x-paper>

View file

@ -35,6 +35,7 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
} }
_sendRoomSecrets(roomSecrets) { _sendRoomSecrets(roomSecrets) {
@ -137,6 +138,8 @@ class ServerConnection {
this._socket.onclose = null; this._socket.onclose = null;
this._socket.close(); this._socket.close();
this._socket = null; this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
} }
} }
@ -146,10 +149,11 @@ class ServerConnection {
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...'); Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 5000); this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
Events.fire('ws-disconnected'); Events.fire('ws-disconnected');
this._isReconnect = true;
} }
_onVisibilityChange() { _onVisibilityChange() {
@ -315,25 +319,25 @@ class Peer {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
message = JSON.parse(message); const messageJSON = JSON.parse(message);
switch (message.type) { switch (messageJSON.type) {
case 'request': case 'request':
this._onFilesTransferRequest(message); this._onFilesTransferRequest(messageJSON);
break; break;
case 'header': case 'header':
this._onFilesHeader(message); this._onFilesHeader(messageJSON);
break; break;
case 'partition': case 'partition':
this._onReceivedPartitionEnd(message); this._onReceivedPartitionEnd(messageJSON);
break; break;
case 'partition-received': case 'partition-received':
this._sendNextPartition(); this._sendNextPartition();
break; break;
case 'progress': case 'progress':
this._onDownloadProgress(message.progress); this._onDownloadProgress(messageJSON.progress);
break; break;
case 'files-transfer-response': case 'files-transfer-response':
this._onFileTransferRequestResponded(message); this._onFileTransferRequestResponded(messageJSON);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted();
@ -342,10 +346,10 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(message); this._onTextReceived(messageJSON);
break; break;
case 'display-name-changed': case 'display-name-changed':
this._onDisplayNameChanged(message); this._onDisplayNameChanged(messageJSON);
break; break;
} }
} }
@ -439,7 +443,7 @@ class Peer {
if (!this._requestAccepted.header.length) { if (!this._requestAccepted.header.length) {
this._busy = false; this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'}); 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._filesReceived = [];
this._requestAccepted = null; this._requestAccepted = null;
} }
@ -495,17 +499,11 @@ class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) { constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller if (!peerId) return; // we will listen for a caller
this._connect(peerId, true); this._connect(peerId, true);
} }
_onMessage(message) {
if (typeof message !== 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
}
_connect(peerId, isCaller) { _connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(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()}); 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() { getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n"); const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.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) { constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret); super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = false;
if (!peerId) return; // we will listen for a caller if (!peerId) return; // we will listen for a caller
this._isCaller = true; this._isCaller = true;
this._sendSignal(); this._sendSignal();
@ -712,15 +718,15 @@ class WSPeer extends Peer {
this._server.send(message); this._server.send(message);
} }
_sendSignal() { _sendSignal(connected = false) {
this.sendJSON({type: 'signal'}); this.sendJSON({type: 'signal', connected: connected});
} }
onServerMessage(message) { onServerMessage(message) {
this._peerId = message.sender.id; this._peerId = message.sender.id;
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()}) Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (this._isCaller) return; if (message.connected) return;
this._sendSignal(); this._sendSignal(true);
} }
getConnectionHash() { getConnectionHash() {
@ -745,6 +751,7 @@ class PeersManager {
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(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('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); 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)); Events.on('ws-relay', e => this._onWsRelay(e.detail));
} }
@ -764,7 +771,7 @@ class PeersManager {
_onWsRelay(message) { _onWsRelay(message) {
const messageJSON = JSON.parse(message) const messageJSON = JSON.parse(message)
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk); 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) { _onPeers(msg) {
@ -808,9 +815,9 @@ class PeersManager {
} }
_onPeerLeft(msg) { _onPeerLeft(msg) {
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) { if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', msg.peerId) console.log('WSPeer left:', msg.peerId);
Events.fire('peer-disconnected', msg.peerId) Events.fire('peer-disconnected', msg.peerId);
} else if (msg.disconnect === true) { } else if (msg.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately // if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId); Events.fire('peer-disconnected', msg.peerId);
@ -821,6 +828,15 @@ class PeersManager {
this._notifyPeerDisplayNameChanged(peerId); 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) { _onPeerDisconnected(peerId) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
delete this.peers[peerId]; delete this.peers[peerId];

View file

@ -1,6 +1,5 @@
const $ = query => document.getElementById(query); const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(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.isProductionEnvironment = !window.location.host.startsWith('localhost');
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent); 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)); Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {}; this.peers = {};
this.$cancelPasteModeBtn = $('cancel-paste-mode-btn'); this.$cancelPasteModeBtn = $('cancel-paste-mode');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode()); this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e)); Events.on('dragover', e => this._onDragOver(e));
@ -549,10 +548,14 @@ class Dialog {
class ReceiveDialog extends Dialog { class ReceiveDialog extends Dialog {
constructor(id) { constructor(id) {
super(id); super(id);
this.$fileDescription = this.$el.querySelector('.file-description');
this.$fileDescriptionNode = this.$el.querySelector('.file-description'); this.$displayName = this.$el.querySelector('.display-name');
this.$fileSizeNode = this.$el.querySelector('.file-size'); this.$fileStem = this.$el.querySelector('.file-stem');
this.$previewBox = this.$el.querySelector('.file-preview') 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) { _formatFileSize(bytes) {
@ -568,6 +571,26 @@ class ReceiveDialog extends Dialog {
return bytes + ' Bytes'; 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 { class ReceiveFileDialog extends ReceiveDialog {
@ -575,24 +598,25 @@ class ReceiveFileDialog extends ReceiveDialog {
constructor() { constructor() {
super('receive-file-dialog'); super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download'); this.$downloadBtn = this.$el.querySelector('#download-btn');
this.$receiveTitleNode = this.$el.querySelector('#receive-title') 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 = []; this._filesQueue = [];
} }
_onFilesReceived(sender, files, request) { _onFilesReceived(sender, files, imagesOnly, totalSize) {
this._nextFiles(sender, files, request); const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play(); window.blop.play();
} }
_nextFiles(sender, nextFiles, nextRequest) { _nextFiles() {
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
if (this._busy) return; if (this._busy) return;
this._busy = true; this._busy = true;
const {peerId, files, request} = this._filesQueue.shift(); const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peerId, files, request); this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
} }
_dequeueFile() { _dequeueFile() {
@ -624,7 +648,6 @@ class ReceiveFileDialog extends ReceiveDialog {
let element = document.createElement(previewElement[mime]); let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file); element.src = URL.createObjectURL(file);
element.controls = true; element.controls = true;
element.classList.add('element-preview');
element.onload = _ => { element.onload = _ => {
this.$previewBox.appendChild(element); this.$previewBox.appendChild(element);
resolve(true) resolve(true)
@ -635,30 +658,32 @@ class ReceiveFileDialog extends ReceiveDialog {
}); });
} }
async _displayFiles(peerId, files, request) { async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback); this._parseFileData(displayName, files, imagesOnly, totalSize);
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});
let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
url = URL.createObjectURL(files[0]) descriptor = imagesOnly ? 'Image' : 'File';
title = `PairDrop - ${descriptor} Received`
filenameDownload = files[0].name;
} else { } else {
title = `PairDrop - ${files.length} ${descriptor}s Received` descriptor = imagesOnly ? 'Images' : 'Files';
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`; }
if(files.length>2) description += "s"; 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; let bytesCompleted = 0;
zipper.createNewZipWriter(); zipper.createNewZipWriter();
for (let i=0; i<files.length; i++) { for (let i=0; i<files.length; i++) {
@ -666,7 +691,7 @@ class ReceiveFileDialog extends ReceiveDialog {
onprogress: (progress) => { onprogress: (progress) => {
Events.fire('set-progress', { Events.fire('set-progress', {
peerId: peerId, peerId: peerId,
progress: (bytesCompleted + progress) / request.totalSize, progress: (bytesCompleted + progress) / totalSize,
status: 'process' status: 'process'
}) })
} }
@ -686,47 +711,58 @@ class ReceiveFileDialog extends ReceiveDialog {
let minutes = now.getMinutes().toString(); let minutes = now.getMinutes().toString();
minutes = minutes.length < 2 ? "0" + minutes : minutes; minutes = minutes.length < 2 ? "0" + minutes : minutes;
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`; filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
} catch (e) {
console.error(e);
downloadZipped = false;
} }
} }
this.$receiveTitleNode.textContent = title; this.$downloadBtn.innerText = "Download";
this.$fileDescriptionNode.textContent = description; this.$downloadBtn.onclick = _ => {
this.$fileSizeNode.textContent = size; if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
if (shareInsteadOfDownload) { tmpZipBtn.download = filenameDownload;
this.$shareOrDownloadBtn.innerText = "Share"; tmpZipBtn.href = url;
this.continue = _ => { tmpZipBtn.click();
navigator.share({files: files}) } else {
.catch(err => console.error(err)); this._downloadFilesIndividually(files);
} }
this.continueCallback = _ => this.continue();
} else { if (!canShare) {
this.$shareOrDownloadBtn.innerText = "Download again"; this.$downloadBtn.innerText = "Download again";
this.continue = _ => { }
let tmpBtn = document.createElement("a"); Events.fire('notify-user', `${descriptor} downloaded successfully`);
tmpBtn.download = filenameDownload; this.$downloadBtn.style.pointerEvents = "none";
tmpBtn.href = url; setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
tmpBtn.click(); };
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
this.createPreviewElement(files[0]).finally(_ => { 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"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'}) 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)); }).catch(r => console.error(r));
} }
_downloadFilesIndividually(files) {
let tmpBtn = document.createElement("a");
for (let i=0; i<files.length; i++) {
tmpBtn.download = files[i].name;
tmpBtn.href = URL.createObjectURL(files[i]);
tmpBtn.click();
}
}
hide() { hide() {
this.$shareOrDownloadBtn.removeAttribute('href'); this.$shareBtn.setAttribute('hidden', '');
this.$shareOrDownloadBtn.removeAttribute('download');
this.$previewBox.innerHTML = ''; this.$previewBox.innerHTML = '';
super.hide(); super.hide();
this._dequeueFile(); this._dequeueFile();
@ -738,11 +774,6 @@ class ReceiveRequestDialog extends ReceiveDialog {
constructor() { constructor() {
super('receive-request-dialog'); super('receive-request-dialog');
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');
this.$acceptRequestBtn = this.$el.querySelector('#accept-request'); this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
this.$declineRequestBtn = this.$el.querySelector('#decline-request'); this.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true)); this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
@ -774,32 +805,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
_showRequestDialog(request, peerId) { _showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
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);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") { if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img'); let element = document.createElement('img');
element.src = request.thumbnailDataUrl; element.src = request.thumbnailDataUrl;
element.classList.add('element-preview');
this.$previewBox.appendChild(element) 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"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show(); this.show();
} }
@ -834,7 +851,7 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn = $('clear-pair-devices'); this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices'); this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form'); 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.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e))); this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
@ -913,7 +930,7 @@ class PairDeviceDialog extends Dialog {
}) })
this.$submitBtn.removeAttribute("disabled"); this.$submitBtn.removeAttribute("disabled");
if (document.activeElement === this.$inputRoomKeyChars[5]) { if (document.activeElement === this.$inputRoomKeyChars[5]) {
this._onSubmit(); this._pairDeviceJoin(this.inputRoomKey);
} }
} }
} }
@ -963,7 +980,8 @@ class PairDeviceDialog extends Dialog {
return url.href; return url.href;
} }
_onSubmit() { _onSubmit(e) {
e.preventDefault();
this._pairDeviceJoin(this.inputRoomKey); this._pairDeviceJoin(this.inputRoomKey);
} }
@ -1050,14 +1068,19 @@ class ClearDevicesDialog extends Dialog {
super('clear-devices-dialog'); super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices()); $('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form'); let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit()); clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
} }
_onClearPairDevices() { _onClearPairDevices() {
this.show(); this.show();
} }
_onSubmit() { _onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets'); Events.fire('clear-room-secrets');
this.hide(); this.hide();
} }
@ -1068,10 +1091,10 @@ class SendTextDialog extends Dialog {
super('send-text-dialog'); super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName)); Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input'); 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.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]'); 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)); this.$text.addEventListener('input', e => this._onChange(e));
Events.on("keydown", e => this._onKeyDown(e)); Events.on("keydown", e => this._onKeyDown(e));
} }
@ -1113,6 +1136,11 @@ class SendTextDialog extends Dialog {
sel.addRange(range); sel.addRange(range);
} }
_onSubmit(e) {
e.preventDefault();
this._send();
}
_send() { _send() {
Events.fire('send-text', { Events.fire('send-text', {
to: this.correspondingPeerId, to: this.correspondingPeerId,
@ -1136,7 +1164,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e)); 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 = []; this._receiveTextQueue = [];
} }
@ -1154,6 +1182,7 @@ class ReceiveTextDialog extends Dialog {
_onText(text, peerId) { _onText(text, peerId) {
window.blop.play(); window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId}); this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return; if (this.$el.attributes["show"]) return;
this._dequeueRequests(); this._dequeueRequests();
} }
@ -1165,23 +1194,35 @@ class ReceiveTextDialog extends Dialog {
} }
_showReceiveTextDialog(text, peerId) { _showReceiveTextDialog(text, peerId) {
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName(); this.$displayNameNode.innerText = $(peerId).ui._displayName();
if (isURL(text)) { this.$text.innerText = text;
const $a = document.createElement('a'); this.$text.classList.remove('text-center');
$a.href = text;
$a.target = '_blank'; // Beautify text if text is short
$a.textContent = text; if (text.length < 2000) {
this.$text.innerHTML = ''; // replace urls with actual links
this.$text.appendChild($a); this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
} else { return `<a href="${url}" target="_blank">${url}</a>`;
this.$text.textContent = text; });
if (!/\s/.test(text)) {
this.$text.classList.add('text-center');
}
} }
document.title = 'PairDrop - Message Received';
this._setDocumentTitleMessages();
document.changeFavicon("images/favicon-96x96-notification.png"); document.changeFavicon("images/favicon-96x96-notification.png");
this.show(); this.show();
} }
_setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop'
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
}
async _onCopy() { async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent); await navigator.clipboard.writeText(this.$text.textContent);
Events.fire('notify-user', 'Copied to clipboard'); Events.fire('notify-user', 'Copied to clipboard');
@ -1254,7 +1295,7 @@ class Base64ZipDialog extends Dialog {
} }
_setPasteBtnToProcessing() { _setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none"; this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = "Processing...";
} }
@ -1399,7 +1440,7 @@ class Notifications {
_messageNotification(message, peerId) { _messageNotification(message, peerId) {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName(); 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); const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
this._bind(notification, _ => window.open(message, '_blank', null, true)); this._bind(notification, _ => window.open(message, '_blank', null, true));
} else { } else {
@ -1460,7 +1501,7 @@ class NetworkStatusUI {
constructor() { constructor() {
Events.on('offline', _ => this._showOfflineMessage()); Events.on('offline', _ => this._showOfflineMessage());
Events.on('online', _ => this._showOnlineMessage()); Events.on('online', _ => this._showOnlineMessage());
Events.on('ws-connected', _ => this._showOnlineMessage()); Events.on('ws-connected', _ => this._onWsConnected());
Events.on('ws-disconnected', _ => this._onWsDisconnected()); Events.on('ws-disconnected', _ => this._onWsDisconnected());
if (!navigator.onLine) this._showOfflineMessage(); if (!navigator.onLine) this._showOfflineMessage();
} }
@ -1471,17 +1512,16 @@ class NetworkStatusUI {
} }
_showOnlineMessage() { _showOnlineMessage() {
window.animateBackground(true);
if (!this.firstConnect) {
this.firstConnect = true;
return;
}
Events.fire('notify-user', 'You are back online'); Events.fire('notify-user', 'You are back online');
window.animateBackground(true);
}
_onWsConnected() {
window.animateBackground(true);
} }
_onWsDisconnected() { _onWsDisconnected() {
window.animateBackground(false); window.animateBackground(false);
if (!this.firstConnect) this.firstConnect = true;
} }
} }
@ -1837,8 +1877,8 @@ Events.on('load', () => {
let x0, y0, w, h, dw, offset; let x0, y0, w, h, dw, offset;
function init() { function init() {
w = window.innerWidth; w = document.documentElement.clientWidth;
h = window.innerHeight; h = document.documentElement.clientHeight;
c.width = w; c.width = w;
c.height = h; c.height = h;
offset = $$('footer').offsetHeight - 32; offset = $$('footer').offsetHeight - 32;

View file

@ -1,4 +1,4 @@
const cacheVersion = 'v1.1.3'; const cacheVersion = 'v1.2.2';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',

View file

@ -613,7 +613,7 @@ x-dialog x-background {
z-index: 10; z-index: 10;
transition: opacity 300ms; transition: opacity 300ms;
will-change: opacity; will-change: opacity;
padding: 35px; padding: 15px;
overflow: overlay; overflow: overlay;
} }
@ -624,19 +624,20 @@ x-dialog x-paper {
padding: 16px 24px; padding: 16px 24px;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
transition: transform 300ms; transition: transform 300ms;
will-change: transform; will-change: transform;
} }
#pair-device-dialog x-paper { #pair-device-dialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
display: flex; display: flex;
flex-direction: column; 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]) { x-dialog:not([show]) {
@ -651,12 +652,6 @@ x-dialog:not([show]) x-background {
opacity: 0; opacity: 0;
} }
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a { x-dialog a {
color: var(--primary-color); color: var(--primary-color);
@ -695,7 +690,7 @@ x-dialog .font-subheading {
} }
#key-input-container>input:nth-of-type(4) { #key-input-container>input:nth-of-type(4) {
margin-left: 18px; margin-left: 5%;
} }
#room-key { #room-key {
@ -707,16 +702,11 @@ x-dialog .font-subheading {
} }
#room-key-qr-code { #room-key-qr-code {
padding: inherit; margin: 16px;
margin: auto;
width: 150px;
height: 150px;
} }
#pair-device-dialog hr { #pair-device-dialog hr {
margin-top: 40px; margin: 40px -24px;
margin-bottom: 40px;
width: 100%;
} }
#pair-device-dialog x-background { #pair-device-dialog x-background {
@ -730,29 +720,24 @@ x-dialog .row {
margin-bottom: 8px; margin-bottom: 8px;
} }
x-dialog h2 { /* button row*/
margin-top: 1rem; x-paper > div:last-child {
} margin: auto -24px -15px;
#receive-request-dialog h2,
#receive-file-dialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px 0;
border-top: solid 2.5px var(--border-color); border-top: solid 2.5px var(--border-color);
height: 50px;
} }
.separator { x-paper > div:last-child > .button {
border: solid 1.25px var(--border-color); height: 100%;
margin-bottom: -16px; width: 50%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
} }
.file-description { .file-description {
word-break: break-word; margin-bottom: 25px;
width: 80%;
margin: auto;
} }
.file-description .row { .file-description .row {
@ -764,26 +749,26 @@ x-dialog .row-reverse {
word-break: normal; word-break: normal;
} }
#file-name { .file-name {
font-style: italic; font-style: italic;
max-width: 100%;
} }
#file-stem { .file-stem {
max-width: 80%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
word-break: break-all; white-space: nowrap;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
} }
/* Send Text Dialog */ /* Send Text Dialog */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input { #text-input {
min-height: 120px; min-height: 200px;
margin: 14px auto;
} }
/* Receive Text Dialog */ /* Receive Text Dialog */
@ -791,14 +776,14 @@ x-dialog .row-reverse {
#receive-text-dialog #text { #receive-text-dialog #text {
width: 100%; width: 100%;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: calc(100vh - 393px);
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-user-select: all; -webkit-user-select: all;
-moz-user-select: all; -moz-user-select: all;
user-select: all; user-select: all;
white-space: pre-wrap; white-space: pre-wrap;
margin-top:36px; padding: 15px 0;
} }
#receive-text-dialog #text a { #receive-text-dialog #text a {
@ -817,11 +802,7 @@ x-dialog .row-reverse {
.row-separator { .row-separator {
border-bottom: solid 2.5px var(--border-color); border-bottom: solid 2.5px var(--border-color);
margin: auto -25px; margin: auto -24px;
}
#receive-text-description-container {
margin-bottom: 25px;
} }
#base64-paste-btn { #base64-paste-btn {
@ -849,7 +830,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0; padding: 2px 16px 0;
box-sizing: border-box; box-sizing: border-box;
min-height: 36px; min-height: 36px;
min-width: 100px;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
font-weight: 700; font-weight: 700;
@ -860,6 +840,7 @@ x-dialog .row-reverse {
user-select: none; user-select: none;
background: inherit; background: inherit;
color: var(--primary-color); color: var(--primary-color);
overflow: hidden;
} }
.button[disabled] { .button[disabled] {
@ -897,7 +878,7 @@ x-dialog .row-reverse {
opacity: 0.1; opacity: 0.1;
} }
#cancel-paste-mode-btn { #cancel-paste-mode {
z-index: 2; z-index: 2;
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -924,7 +905,6 @@ button::-moz-focus-inner {
/* Icon Button */ /* Icon Button */
.icon-button { .icon-button {
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -934,10 +914,7 @@ button::-moz-focus-inner {
border-radius: 50%; border-radius: 50%;
} }
/* Text Input */ /* Text Input */
.textarea { .textarea {
box-sizing: border-box; box-sizing: border-box;
border: none; border: none;
@ -951,9 +928,8 @@ button::-moz-focus-inner {
display: block; display: block;
overflow: auto; overflow: auto;
resize: none; resize: none;
min-height: 40px;
line-height: 16px; line-height: 16px;
max-height: 300px; max-height: calc(100vh - 254px);
white-space: pre; white-space: pre;
} }
@ -1143,6 +1119,14 @@ x-peers:empty~x-instructions {
} }
/* Responsive Styles */ /* 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) { @media screen and (min-height: 800px) {
#websocket-fallback { #websocket-fallback {
@ -1215,7 +1199,9 @@ x-dialog x-paper {
display: none; display: none;
} }
.element-preview { .file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%; max-width: 100%;
max-height: 40vh; max-height: 40vh;
margin: auto; margin: auto;