merge master into branch

This commit is contained in:
schlagmichdoch 2023-03-03 18:01:24 +01:00
commit c0a4224a59
13 changed files with 587 additions and 493 deletions

View file

@ -1,6 +1,7 @@
const process = require('process')
const crypto = require('crypto')
const {spawn} = require('child_process')
const WebSocket = require('ws');
const fs = require('fs');
// Handle SIGINT
@ -119,7 +120,6 @@ const { uniqueNamesGenerator, animals, colors } = require('unique-names-generato
class PairDropServer {
constructor() {
const WebSocket = require('ws');
this._wss = new WebSocket.Server({ server });
this._wss.on('connection', (socket, request) => this._onConnection(new Peer(socket, request)));
@ -130,15 +130,14 @@ class PairDropServer {
}
_onConnection(peer) {
peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e);
this._keepAlive(peer);
this._send(peer, {
type: 'rtc-config',
config: rtcConfig
});
this._joinRoom(peer);
peer.socket.on('message', message => this._onMessage(peer, message));
peer.socket.onerror = e => console.error(e);
this._keepAlive(peer);
// send displayName
this._send(peer, {
@ -342,6 +341,10 @@ class PairDropServer {
_joinRoom(peer, roomType = 'ip', roomSecret = '') {
const room = roomType === 'ip' ? peer.ip : roomSecret;
if (this._rooms[room] && this._rooms[room][peer.id]) {
this._leaveRoom(peer, roomType, roomSecret);
}
// if room doesn't exist, create it
if (!this._rooms[room]) {
this._rooms[room] = {};
@ -366,10 +369,6 @@ class PairDropServer {
// delete the peer
delete this._rooms[room][peer.id];
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room];

4
package-lock.json generated
View file

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

View file

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

View file

@ -69,7 +69,7 @@
<use xlink:href="#clear-pair-devices-icon" />
</svg>
</a>
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a>
<a id="cancel-paste-mode" class="button" hidden>Done</a>
</header>
<!-- Center -->
<div id="center">
@ -106,18 +106,17 @@
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr>
<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 id="char1" 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 id="char3" 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 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" autofocus contenteditable placeholder="" disabled>
<input 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 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 type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</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>
<div class="separator"></div>
<a class="button" close>Cancel</a>
<button class="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@ -130,9 +129,9 @@
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<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>
<a class="button" close>Cancel</a>
<button class="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@ -142,25 +141,23 @@
<x-dialog id="receive-request-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center">PairDrop</h2>
<div class="text-center file-description">
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span id="requesting-peer-display-name"></span>
<span class="display-name"></span>
<span>would like to share</span>
</div>
<div id="file-name" class="row" >
<span id="file-stem"></span>
<span id="file-extension"></span>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row">
<span id="file-other"></span>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="font-body2 text-center file-size"></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>
<div class="separator"></div>
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div>
</x-paper>
@ -170,13 +167,23 @@
<x-dialog id="receive-file-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 id="receive-title" class="center"></h2>
<div class="text-center file-description"></div>
<div class="font-body2 text-center file-size"></div>
<h2 class="center"></h2>
<div class="center column file-description">
<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="row-reverse space-between">
<a id="share-or-download" class="button" autofocus></a>
<div class="separator"></div>
<div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button>
</div>
</x-paper>
@ -187,16 +194,16 @@
<form action="#">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="text-center">PairDrop</h2>
<div class="text-center">
<h2 class="text-center">Send Message</h2>
<div class="dialog-subheader text-center">
<span>Send a Message to</span>
<span id="text-send-peer-display-name"></span>
<span class="display-name"></span>
</div>
<div class="row-separator"></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>
<div class="separator"></div>
<a class="button" title="ESCAPE" close>Cancel</a>
<button class="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
</x-background>
@ -206,16 +213,15 @@
<x-dialog id="receive-text-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2>PairDrop - Message Received</h2>
<div id="receive-text-description-container">
<span id="receive-text-peer-display-name"></span>
<span>sent the following message:</span>
<h2 class="text-center">Message Received</h2>
<div class="text-center dialog-subheader">
<span class="display-name"></span>
<span>has sent:</span>
</div>
<div class="row-separator"></div>
<div id="text"></div>
<div class="row-reverse">
<div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button id="close" class="button" title="ESCAPE">Close</button>
</div>
</x-paper>

View file

@ -36,6 +36,7 @@ class ServerConnection {
_onOpen() {
console.log('WS: server connected');
Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
}
_sendRoomSecrets(roomSecrets) {
@ -152,15 +153,17 @@ class ServerConnection {
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
}
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...');
Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onVisibilityChange() {
@ -326,26 +329,25 @@ class Peer {
this._onChunkReceived(message);
return;
}
message = JSON.parse(message);
console.log('RTC:', message);
switch (message.type) {
const messageJSON = JSON.parse(message);
switch (messageJSON.type) {
case 'request':
this._onFilesTransferRequest(message);
this._onFilesTransferRequest(messageJSON);
break;
case 'header':
this._onFilesHeader(message);
this._onFilesHeader(messageJSON);
break;
case 'partition':
this._onReceivedPartitionEnd(message);
this._onReceivedPartitionEnd(messageJSON);
break;
case 'partition-received':
this._sendNextPartition();
break;
case 'progress':
this._onDownloadProgress(message.progress);
this._onDownloadProgress(messageJSON.progress);
break;
case 'files-transfer-response':
this._onFileTransferRequestResponded(message);
this._onFileTransferRequestResponded(messageJSON);
break;
case 'file-transfer-complete':
this._onFileTransferCompleted();
@ -354,7 +356,7 @@ class Peer {
this._onMessageTransferCompleted();
break;
case 'text':
this._onTextReceived(message);
this._onTextReceived(messageJSON);
break;
}
}
@ -448,7 +450,7 @@ class Peer {
if (!this._requestAccepted.header.length) {
this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = [];
this._requestAccepted = null;
}
@ -499,6 +501,7 @@ class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
}
@ -575,6 +578,13 @@ class RTCPeer extends Peer {
this._channel = channel;
}
_onMessage(message) {
if (typeof message === 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
}
getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");

View file

@ -1,6 +1,5 @@
const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(query);
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent);
@ -28,7 +27,7 @@ class PeersUI {
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {};
this.$cancelPasteModeBtn = $('cancel-paste-mode-btn');
this.$cancelPasteModeBtn = $('cancel-paste-mode');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e));
@ -473,10 +472,14 @@ class Dialog {
class ReceiveDialog extends Dialog {
constructor(id) {
super(id);
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
this.$fileSizeNode = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview')
this.$fileDescription = this.$el.querySelector('.file-description');
this.$displayName = this.$el.querySelector('.display-name');
this.$fileStem = this.$el.querySelector('.file-stem');
this.$fileExtension = this.$el.querySelector('.file-extension');
this.$fileOther = this.$el.querySelector('.file-other');
this.$fileSize = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview');
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
}
_formatFileSize(bytes) {
@ -492,6 +495,26 @@ class ReceiveDialog extends Dialog {
return bytes + ' Bytes';
}
}
_parseFileData(displayName, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
fileOtherText += imagesOnly ? 'image' : 'file';
} else {
fileOtherText += imagesOnly ? 'images' : 'files';
}
this.$fileOther.innerText = fileOtherText;
}
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
}
class ReceiveFileDialog extends ReceiveDialog {
@ -499,24 +522,25 @@ class ReceiveFileDialog extends ReceiveDialog {
constructor() {
super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download');
this.$receiveTitleNode = this.$el.querySelector('#receive-title')
this.$downloadBtn = this.$el.querySelector('#download-btn');
this.$shareBtn = this.$el.querySelector('#share-btn');
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request));
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
this._filesQueue = [];
}
_onFilesReceived(sender, files, request) {
this._nextFiles(sender, files, request);
_onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play();
}
_nextFiles(sender, nextFiles, nextRequest) {
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
_nextFiles() {
if (this._busy) return;
this._busy = true;
const {peerId, files, request} = this._filesQueue.shift();
this._displayFiles(peerId, files, request);
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
}
_dequeueFile() {
@ -547,7 +571,6 @@ class ReceiveFileDialog extends ReceiveDialog {
let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file);
element.controls = true;
element.classList.add('element-preview');
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true)
@ -558,30 +581,32 @@ class ReceiveFileDialog extends ReceiveDialog {
});
}
async _displayFiles(peerId, files, request) {
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
let url;
let title;
let filenameDownload;
let descriptor = request.imagesOnly ? "Image" : "File";
let size = this._formatFileSize(request.totalSize);
let description = files[0].name;
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload;
if (files.length === 1) {
url = URL.createObjectURL(files[0])
title = `PairDrop - ${descriptor} Received`
filenameDownload = files[0].name;
descriptor = imagesOnly ? 'Image' : 'File';
} else {
title = `PairDrop - ${files.length} ${descriptor}s Received`
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
if(files.length>2) description += "s";
descriptor = imagesOnly ? 'Images' : 'Files';
}
this.$receiveTitle.innerText = `${descriptor} Received`;
if(!shareInsteadOfDownload) {
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
if (canShare) {
this.$shareBtn.removeAttribute('hidden');
this.$shareBtn.onclick = _ => {
navigator.share({files: files})
.catch(err => {
console.error(err);
});
}
}
let downloadZipped = false;
if (files.length > 1) {
downloadZipped = true;
try {
let bytesCompleted = 0;
zipper.createNewZipWriter();
for (let i=0; i<files.length; i++) {
@ -589,7 +614,7 @@ class ReceiveFileDialog extends ReceiveDialog {
onprogress: (progress) => {
Events.fire('set-progress', {
peerId: peerId,
progress: (bytesCompleted + progress) / request.totalSize,
progress: (bytesCompleted + progress) / totalSize,
status: 'process'
})
}
@ -609,47 +634,58 @@ class ReceiveFileDialog extends ReceiveDialog {
let minutes = now.getMinutes().toString();
minutes = minutes.length < 2 ? "0" + minutes : minutes;
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
} catch (e) {
console.error(e);
downloadZipped = false;
}
}
this.$receiveTitleNode.textContent = title;
this.$fileDescriptionNode.textContent = description;
this.$fileSizeNode.textContent = size;
if (shareInsteadOfDownload) {
this.$shareOrDownloadBtn.innerText = "Share";
this.continue = _ => {
navigator.share({files: files})
.catch(err => console.error(err));
this.$downloadBtn.innerText = "Download";
this.$downloadBtn.onclick = _ => {
if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
tmpZipBtn.download = filenameDownload;
tmpZipBtn.href = url;
tmpZipBtn.click();
} else {
this._downloadFilesIndividually(files);
}
this.continueCallback = _ => this.continue();
} else {
this.$shareOrDownloadBtn.innerText = "Download again";
this.continue = _ => {
let tmpBtn = document.createElement("a");
tmpBtn.download = filenameDownload;
tmpBtn.href = url;
tmpBtn.click();
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
if (!canShare) {
this.$downloadBtn.innerText = "Download again";
}
Events.fire('notify-user', `${descriptor} downloaded successfully`);
this.$downloadBtn.style.pointerEvents = "none";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => {
document.title = `PairDrop - ${files.length} Files received`;
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.continue();
this.show();
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r));
}
_downloadFilesIndividually(files) {
let tmpBtn = document.createElement("a");
for (let i=0; i<files.length; i++) {
tmpBtn.download = files[i].name;
tmpBtn.href = URL.createObjectURL(files[i]);
tmpBtn.click();
}
}
hide() {
this.$shareOrDownloadBtn.removeAttribute('href');
this.$shareOrDownloadBtn.removeAttribute('download');
this.$shareBtn.setAttribute('hidden', '');
this.$previewBox.innerHTML = '';
super.hide();
this._dequeueFile();
@ -661,11 +697,6 @@ class ReceiveRequestDialog extends ReceiveDialog {
constructor() {
super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-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.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
@ -697,32 +728,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
_showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId;
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
const fileName = request.header[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtensionNode.innerText = fileExtension
if (request.header.length >= 2) {
let fileOtherText = ` and ${request.header.length - 1} other `;
fileOtherText += request.imagesOnly ? 'image' : 'file';
if (request.header.length > 2) fileOtherText += "s";
this.$fileOtherNode.innerText = fileOtherText;
}
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img');
element.src = request.thumbnailDataUrl;
element.classList.add('element-preview');
this.$previewBox.appendChild(element)
}
document.title = 'PairDrop - File Transfer Requested';
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
}
@ -757,7 +774,7 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form');
createJoinForm.addEventListener('submit', _ => this._onSubmit());
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
@ -836,7 +853,7 @@ class PairDeviceDialog extends Dialog {
})
this.$submitBtn.removeAttribute("disabled");
if (document.activeElement === this.$inputRoomKeyChars[5]) {
this._onSubmit();
this._pairDeviceJoin(this.inputRoomKey);
}
}
}
@ -886,7 +903,8 @@ class PairDeviceDialog extends Dialog {
return url.href;
}
_onSubmit() {
_onSubmit(e) {
e.preventDefault();
this._pairDeviceJoin(this.inputRoomKey);
}
@ -973,14 +991,19 @@ class ClearDevicesDialog extends Dialog {
super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
}
_onClearPairDevices() {
this.show();
}
_onSubmit() {
_onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets');
this.hide();
}
@ -991,10 +1014,10 @@ class SendTextDialog extends Dialog {
super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name');
this.$peerDisplayName = this.$el.querySelector('.display-name');
this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send());
this.$form.addEventListener('submit', e => this._onSubmit(e));
this.$text.addEventListener('input', e => this._onChange(e));
Events.on("keydown", e => this._onKeyDown(e));
}
@ -1036,6 +1059,11 @@ class SendTextDialog extends Dialog {
sel.addRange(range);
}
_onSubmit(e) {
e.preventDefault();
this._send();
}
_send() {
Events.fire('send-text', {
to: this.correspondingPeerId,
@ -1059,7 +1087,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name');
this.$displayNameNode = this.$el.querySelector('.display-name');
this._receiveTextQueue = [];
}
@ -1077,6 +1105,7 @@ class ReceiveTextDialog extends Dialog {
_onText(text, peerId) {
window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return;
this._dequeueRequests();
}
@ -1088,23 +1117,35 @@ class ReceiveTextDialog extends Dialog {
}
_showReceiveTextDialog(text, peerId) {
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
this.$displayNameNode.innerText = $(peerId).ui._displayName();
if (isURL(text)) {
const $a = document.createElement('a');
$a.href = text;
$a.target = '_blank';
$a.textContent = text;
this.$text.innerHTML = '';
this.$text.appendChild($a);
} else {
this.$text.textContent = text;
this.$text.innerText = text;
this.$text.classList.remove('text-center');
// Beautify text if text is short
if (text.length < 2000) {
// replace urls with actual links
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
return `<a href="${url}" target="_blank">${url}</a>`;
});
if (!/\s/.test(text)) {
this.$text.classList.add('text-center');
}
}
document.title = 'PairDrop - Message Received';
this._setDocumentTitleMessages();
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
}
_setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop'
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
}
async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent);
Events.fire('notify-user', 'Copied to clipboard');
@ -1177,7 +1218,7 @@ class Base64ZipDialog extends Dialog {
}
_setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none";
this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
}
@ -1322,7 +1363,7 @@ class Notifications {
_messageNotification(message, peerId) {
if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName();
if (isURL(message)) {
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
this._bind(notification, _ => window.open(message, '_blank', null, true));
} else {
@ -1383,7 +1424,7 @@ class NetworkStatusUI {
constructor() {
Events.on('offline', _ => this._showOfflineMessage());
Events.on('online', _ => this._showOnlineMessage());
Events.on('ws-connected', _ => this._showOnlineMessage());
Events.on('ws-connected', _ => this._onWsConnected());
Events.on('ws-disconnected', _ => this._onWsDisconnected());
if (!navigator.onLine) this._showOfflineMessage();
}
@ -1394,17 +1435,16 @@ class NetworkStatusUI {
}
_showOnlineMessage() {
window.animateBackground(true);
if (!this.firstConnect) {
this.firstConnect = true;
return;
}
Events.fire('notify-user', 'You are back online');
window.animateBackground(true);
}
_onWsConnected() {
window.animateBackground(true);
}
_onWsDisconnected() {
window.animateBackground(false);
if (!this.firstConnect) this.firstConnect = true;
}
}
@ -1742,8 +1782,8 @@ Events.on('load', () => {
let x0, y0, w, h, dw, offset;
function init() {
w = window.innerWidth;
h = window.innerHeight;
w = document.documentElement.clientWidth;
h = document.documentElement.clientHeight;
c.width = w;
c.height = h;
offset = $$('footer').offsetHeight - 32;

View file

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

View file

@ -564,7 +564,7 @@ x-dialog x-background {
z-index: 10;
transition: opacity 300ms;
will-change: opacity;
padding: 35px;
padding: 15px;
overflow: overlay;
}
@ -575,19 +575,20 @@ x-dialog x-paper {
padding: 16px 24px;
width: 100%;
max-width: 400px;
overflow: hidden;
box-sizing: border-box;
transition: transform 300ms;
will-change: transform;
}
#pair-device-dialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
width: calc(100vw - 20px);
height: 625px;
}
x-dialog:not([show]) {
@ -602,12 +603,6 @@ x-dialog:not([show]) x-background {
opacity: 0;
}
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a {
color: var(--primary-color);
@ -646,7 +641,7 @@ x-dialog .font-subheading {
}
#key-input-container>input:nth-of-type(4) {
margin-left: 18px;
margin-left: 5%;
}
#room-key {
@ -658,16 +653,11 @@ x-dialog .font-subheading {
}
#room-key-qr-code {
padding: inherit;
margin: auto;
width: 150px;
height: 150px;
margin: 16px;
}
#pair-device-dialog hr {
margin-top: 40px;
margin-bottom: 40px;
width: 100%;
margin: 40px -24px;
}
#pair-device-dialog x-background {
@ -681,29 +671,24 @@ x-dialog .row {
margin-bottom: 8px;
}
x-dialog h2 {
margin-top: 1rem;
}
#receive-request-dialog h2,
#receive-file-dialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px 0;
/* button row*/
x-paper > div:last-child {
margin: auto -24px -15px;
border-top: solid 2.5px var(--border-color);
height: 50px;
}
.separator {
border: solid 1.25px var(--border-color);
margin-bottom: -16px;
x-paper > div:last-child > .button {
height: 100%;
width: 50%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
}
.file-description {
word-break: break-word;
width: 80%;
margin: auto;
margin-bottom: 25px;
}
.file-description .row {
@ -715,26 +700,26 @@ x-dialog .row-reverse {
word-break: normal;
}
#file-name {
.file-name {
font-style: italic;
max-width: 100%;
}
#file-stem {
max-width: 80%;
.file-stem {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
white-space: nowrap;
}
/* Send Text Dialog */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input {
min-height: 120px;
min-height: 200px;
margin: 14px auto;
}
/* Receive Text Dialog */
@ -742,14 +727,14 @@ x-dialog .row-reverse {
#receive-text-dialog #text {
width: 100%;
word-break: break-all;
max-height: 300px;
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
white-space: pre-wrap;
margin-top:36px;
padding: 15px 0;
}
#receive-text-dialog #text a {
@ -768,11 +753,7 @@ x-dialog .row-reverse {
.row-separator {
border-bottom: solid 2.5px var(--border-color);
margin: auto -25px;
}
#receive-text-description-container {
margin-bottom: 25px;
margin: auto -24px;
}
#base64-paste-btn {
@ -800,7 +781,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0;
box-sizing: border-box;
min-height: 36px;
min-width: 100px;
font-size: 14px;
line-height: 24px;
font-weight: 700;
@ -811,6 +791,7 @@ x-dialog .row-reverse {
user-select: none;
background: inherit;
color: var(--primary-color);
overflow: hidden;
}
.button[disabled] {
@ -848,7 +829,7 @@ x-dialog .row-reverse {
opacity: 0.1;
}
#cancel-paste-mode-btn {
#cancel-paste-mode {
z-index: 2;
margin: 0;
padding: 0;
@ -875,7 +856,6 @@ button::-moz-focus-inner {
/* Icon Button */
.icon-button {
width: 40px;
height: 40px;
@ -885,10 +865,7 @@ button::-moz-focus-inner {
border-radius: 50%;
}
/* Text Input */
.textarea {
box-sizing: border-box;
border: none;
@ -902,9 +879,8 @@ button::-moz-focus-inner {
display: block;
overflow: auto;
resize: none;
min-height: 40px;
line-height: 16px;
max-height: 300px;
max-height: calc(100vh - 254px);
white-space: pre;
}
@ -1094,6 +1070,14 @@ x-peers:empty~x-instructions {
}
/* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
padding: 15px;
}
x-paper > div:last-child {
margin: auto -15px -15px;
}
}
@media screen and (min-height: 800px) {
footer {
@ -1166,7 +1150,9 @@ x-dialog x-paper {
display: none;
}
.element-preview {
.file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%;
max-height: 40vh;
margin: auto;

View file

@ -69,7 +69,7 @@
<use xlink:href="#clear-pair-devices-icon" />
</svg>
</a>
<a id="cancel-paste-mode-btn" class="button" close hidden>Done</a>
<a id="cancel-paste-mode" class="button" hidden>Done</a>
</header>
<!-- Center -->
<div id="center">
@ -109,18 +109,17 @@
<div id="pair-instructions" class="font-subheading center text-center">Input this key on another device<br>or scan the QR-Code.</div>
<hr>
<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 id="char1" 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 id="char3" 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 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" autofocus contenteditable placeholder="" disabled>
<input 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 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 type="tel" class="textarea center" maxlength="1" autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable placeholder="" disabled>
</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>
<div class="separator"></div>
<a class="button" close>Cancel</a>
<button class="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@ -133,9 +132,9 @@
<x-paper shadow="2">
<h2 class="center">Unpair Devices</h2>
<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>
<a class="button" close>Cancel</a>
<button class="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@ -145,25 +144,23 @@
<x-dialog id="receive-request-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="center">PairDrop</h2>
<div class="text-center file-description">
<h2 class="center"></h2>
<div class="center column file-description">
<div>
<span id="requesting-peer-display-name"></span>
<span class="display-name"></span>
<span>would like to share</span>
</div>
<div id="file-name" class="row" >
<span id="file-stem"></span>
<span id="file-extension"></span>
<div class="row file-name" >
<span class="file-stem"></span>
<span class="file-extension"></span>
</div>
<div class="row">
<span id="file-other"></span>
<div class="row file-other">
</div>
<div class="row font-body2 file-size"></div>
</div>
<div class="font-body2 text-center file-size"></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>
<div class="separator"></div>
<button id="decline-request" class="button" title="ESCAPE">Decline</button>
</div>
</x-paper>
@ -173,13 +170,23 @@
<x-dialog id="receive-file-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2 id="receive-title" class="center"></h2>
<div class="text-center file-description"></div>
<div class="font-body2 text-center file-size"></div>
<h2 class="center"></h2>
<div class="center column file-description">
<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="row-reverse space-between">
<a id="share-or-download" class="button" autofocus></a>
<div class="separator"></div>
<div class="center row-reverse">
<button id="share-btn" class="button" autofocus hidden>Share</button>
<button id="download-btn" class="button" autofocus>Download</button>
<button class="button" close>Close</button>
</div>
</x-paper>
@ -190,16 +197,16 @@
<form action="#">
<x-background class="full center">
<x-paper shadow="2">
<h2 class="text-center">PairDrop</h2>
<div class="text-center">
<h2 class="text-center">Send Message</h2>
<div class="dialog-subheader text-center">
<span>Send a Message to</span>
<span id="text-send-peer-display-name"></span>
<span class="display-name"></span>
</div>
<div class="row-separator"></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>
<div class="separator"></div>
<a class="button" title="ESCAPE" close>Cancel</a>
<button class="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
</x-background>
@ -209,16 +216,15 @@
<x-dialog id="receive-text-dialog">
<x-background class="full center">
<x-paper shadow="2">
<h2>PairDrop - Message Received</h2>
<div id="receive-text-description-container">
<span id="receive-text-peer-display-name"></span>
<span>sent the following message:</span>
<h2 class="text-center">Message Received</h2>
<div class="text-center dialog-subheader">
<span class="display-name"></span>
<span>has sent:</span>
</div>
<div class="row-separator"></div>
<div id="text"></div>
<div class="row-reverse">
<div class="center row-reverse">
<button id="copy" class="button" title="CTRL/⌘ + C">Copy</button>
<div class="separator"></div>
<button id="close" class="button" title="ESCAPE">Close</button>
</div>
</x-paper>

View file

@ -34,6 +34,7 @@ class ServerConnection {
_onOpen() {
console.log('WS: server connected');
Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', 'Connected.');
}
_sendRoomSecrets(roomSecrets) {
@ -162,15 +163,17 @@ class ServerConnection {
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
}
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...');
Events.fire('notify-user', 'Connecting..');
clearTimeout(this._reconnectTimer);
this._reconnectTimer = setTimeout(_ => this._connect(), 5000);
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onVisibilityChange() {
@ -331,31 +334,30 @@ class Peer {
this.sendJSON({ type: 'progress', progress: progress });
}
_onMessage(message, logMessage = true) {
_onMessage(message) {
if (typeof message !== 'string') {
this._onChunkReceived(message);
return;
}
message = JSON.parse(message);
if (logMessage) console.log('RTC:', message);
switch (message.type) {
const messageJSON = JSON.parse(message);
switch (messageJSON.type) {
case 'request':
this._onFilesTransferRequest(message);
this._onFilesTransferRequest(messageJSON);
break;
case 'header':
this._onFilesHeader(message);
this._onFilesHeader(messageJSON);
break;
case 'partition':
this._onReceivedPartitionEnd(message);
this._onReceivedPartitionEnd(messageJSON);
break;
case 'partition-received':
this._sendNextPartition();
break;
case 'progress':
this._onDownloadProgress(message.progress);
this._onDownloadProgress(messageJSON.progress);
break;
case 'files-transfer-response':
this._onFileTransferRequestResponded(message);
this._onFileTransferRequestResponded(messageJSON);
break;
case 'file-transfer-complete':
this._onFileTransferCompleted();
@ -364,7 +366,7 @@ class Peer {
this._onMessageTransferCompleted();
break;
case 'text':
this._onTextReceived(message);
this._onTextReceived(messageJSON);
break;
}
}
@ -458,7 +460,7 @@ class Peer {
if (!this._requestAccepted.header.length) {
this._busy = false;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, request: this._requestAccepted});
Events.fire('files-received', {sender: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize});
this._filesReceived = [];
this._requestAccepted = null;
}
@ -509,6 +511,7 @@ class RTCPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = true;
if (!peerId) return; // we will listen for a caller
this._connect(peerId, true);
}
@ -585,6 +588,13 @@ class RTCPeer extends Peer {
this._channel = channel;
}
_onMessage(message) {
if (typeof message === 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
}
getConnectionHash() {
const localDescriptionLines = this._conn.localDescription.sdp.split("\r\n");
const remoteDescriptionLines = this._conn.remoteDescription.sdp.split("\r\n");
@ -689,6 +699,7 @@ class WSPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
this.rtcSupported = false;
if (!peerId) return; // we will listen for a caller
this._sendSignal();
}
@ -707,15 +718,15 @@ class WSPeer extends Peer {
this._server.send(message);
}
_sendSignal() {
this.sendJSON({type: 'signal'});
_sendSignal(connected = false) {
this.sendJSON({type: 'signal', connected: connected});
}
onServerMessage(message) {
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (this._peerId) return;
if (message.connected) return;
this._peerId = message.sender.id;
this._sendSignal();
this._sendSignal(true);
}
getConnectionHash() {
@ -737,6 +748,7 @@ class PeersManager {
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail));
}
@ -756,7 +768,7 @@ class PeersManager {
_onWsRelay(message) {
const messageJSON = JSON.parse(message)
if (messageJSON.type === 'ws-chunk') message = base64ToArrayBuffer(messageJSON.chunk);
this.peers[messageJSON.sender.id]._onMessage(message, false)
this.peers[messageJSON.sender.id]._onMessage(message)
}
_onPeers(msg) {
@ -804,15 +816,24 @@ class PeersManager {
}
_onPeerLeft(msg) {
if (this.peers[msg.peerId] && !this.peers[msg.peerId].rtcSupported) {
console.log('WSPeer left:', msg.peerId)
Events.fire('peer-disconnected', msg.peerId)
if (this.peers[msg.peerId] && (!this.peers[msg.peerId].rtcSupported || !window.isRtcSupported)) {
console.log('WSPeer left:', msg.peerId);
Events.fire('peer-disconnected', msg.peerId);
} else if (msg.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
}
}
_onWsDisconnected() {
for (const peerId in this.peers) {
console.debug(this.peers[peerId].rtcSupported);
if (this.peers[peerId] && (!this.peers[peerId].rtcSupported || !window.isRtcSupported)) {
Events.fire('peer-disconnected', peerId);
}
}
}
_onPeerDisconnected(peerId) {
const peer = this.peers[peerId];
delete this.peers[peerId];

View file

@ -1,6 +1,5 @@
const $ = query => document.getElementById(query);
const $$ = query => document.body.querySelector(query);
const isURL = text => /^(https?:\/\/|www)[^\s]+$/g.test(text.toLowerCase());
window.isProductionEnvironment = !window.location.host.startsWith('localhost');
window.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
window.android = /android/i.test(navigator.userAgent);
@ -28,7 +27,7 @@ class PeersUI {
Events.on('activate-paste-mode', e => this._activatePasteMode(e.detail.files, e.detail.text));
this.peers = {};
this.$cancelPasteModeBtn = $('cancel-paste-mode-btn');
this.$cancelPasteModeBtn = $('cancel-paste-mode');
this.$cancelPasteModeBtn.addEventListener('click', _ => this._cancelPasteMode());
Events.on('dragover', e => this._onDragOver(e));
@ -474,10 +473,14 @@ class Dialog {
class ReceiveDialog extends Dialog {
constructor(id) {
super(id);
this.$fileDescriptionNode = this.$el.querySelector('.file-description');
this.$fileSizeNode = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview')
this.$fileDescription = this.$el.querySelector('.file-description');
this.$displayName = this.$el.querySelector('.display-name');
this.$fileStem = this.$el.querySelector('.file-stem');
this.$fileExtension = this.$el.querySelector('.file-extension');
this.$fileOther = this.$el.querySelector('.file-other');
this.$fileSize = this.$el.querySelector('.file-size');
this.$previewBox = this.$el.querySelector('.file-preview');
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
}
_formatFileSize(bytes) {
@ -493,6 +496,26 @@ class ReceiveDialog extends Dialog {
return bytes + ' Bytes';
}
}
_parseFileData(displayName, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
fileOtherText += imagesOnly ? 'image' : 'file';
} else {
fileOtherText += imagesOnly ? 'images' : 'files';
}
this.$fileOther.innerText = fileOtherText;
}
const fileName = files[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
}
class ReceiveFileDialog extends ReceiveDialog {
@ -500,24 +523,25 @@ class ReceiveFileDialog extends ReceiveDialog {
constructor() {
super('receive-file-dialog');
this.$shareOrDownloadBtn = this.$el.querySelector('#share-or-download');
this.$receiveTitleNode = this.$el.querySelector('#receive-title')
this.$downloadBtn = this.$el.querySelector('#download-btn');
this.$shareBtn = this.$el.querySelector('#share-btn');
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.request));
Events.on('files-received', e => this._onFilesReceived(e.detail.sender, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
this._filesQueue = [];
}
_onFilesReceived(sender, files, request) {
this._nextFiles(sender, files, request);
_onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play();
}
_nextFiles(sender, nextFiles, nextRequest) {
if (nextFiles) this._filesQueue.push({peerId: sender, files: nextFiles, request: nextRequest});
_nextFiles() {
if (this._busy) return;
this._busy = true;
const {peerId, files, request} = this._filesQueue.shift();
this._displayFiles(peerId, files, request);
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
}
_dequeueFile() {
@ -548,7 +572,6 @@ class ReceiveFileDialog extends ReceiveDialog {
let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file);
element.controls = true;
element.classList.add('element-preview');
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true)
@ -559,30 +582,32 @@ class ReceiveFileDialog extends ReceiveDialog {
});
}
async _displayFiles(peerId, files, request) {
if (this.continueCallback) this.$shareOrDownloadBtn.removeEventListener("click", this.continueCallback);
let url;
let title;
let filenameDownload;
let descriptor = request.imagesOnly ? "Image" : "File";
let size = this._formatFileSize(request.totalSize);
let description = files[0].name;
let shareInsteadOfDownload = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload;
if (files.length === 1) {
url = URL.createObjectURL(files[0])
title = `PairDrop - ${descriptor} Received`
filenameDownload = files[0].name;
descriptor = imagesOnly ? 'Image' : 'File';
} else {
title = `PairDrop - ${files.length} ${descriptor}s Received`
description += ` and ${files.length-1} other ${descriptor.toLowerCase()}`;
if(files.length>2) description += "s";
descriptor = imagesOnly ? 'Images' : 'Files';
}
this.$receiveTitle.innerText = `${descriptor} Received`;
if(!shareInsteadOfDownload) {
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
if (canShare) {
this.$shareBtn.removeAttribute('hidden');
this.$shareBtn.onclick = _ => {
navigator.share({files: files})
.catch(err => {
console.error(err);
});
}
}
let downloadZipped = false;
if (files.length > 1) {
downloadZipped = true;
try {
let bytesCompleted = 0;
zipper.createNewZipWriter();
for (let i=0; i<files.length; i++) {
@ -590,7 +615,7 @@ class ReceiveFileDialog extends ReceiveDialog {
onprogress: (progress) => {
Events.fire('set-progress', {
peerId: peerId,
progress: (bytesCompleted + progress) / request.totalSize,
progress: (bytesCompleted + progress) / totalSize,
status: 'process'
})
}
@ -610,47 +635,58 @@ class ReceiveFileDialog extends ReceiveDialog {
let minutes = now.getMinutes().toString();
minutes = minutes.length < 2 ? "0" + minutes : minutes;
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
} catch (e) {
console.error(e);
downloadZipped = false;
}
}
this.$receiveTitleNode.textContent = title;
this.$fileDescriptionNode.textContent = description;
this.$fileSizeNode.textContent = size;
if (shareInsteadOfDownload) {
this.$shareOrDownloadBtn.innerText = "Share";
this.continue = _ => {
navigator.share({files: files})
.catch(err => console.error(err));
this.$downloadBtn.innerText = "Download";
this.$downloadBtn.onclick = _ => {
if (downloadZipped) {
let tmpZipBtn = document.createElement("a");
tmpZipBtn.download = filenameDownload;
tmpZipBtn.href = url;
tmpZipBtn.click();
} else {
this._downloadFilesIndividually(files);
}
this.continueCallback = _ => this.continue();
} else {
this.$shareOrDownloadBtn.innerText = "Download again";
this.continue = _ => {
let tmpBtn = document.createElement("a");
tmpBtn.download = filenameDownload;
tmpBtn.href = url;
tmpBtn.click();
};
this.continueCallback = _ => {
this.continue();
this.hide();
};
}
this.$shareOrDownloadBtn.addEventListener("click", this.continueCallback);
if (!canShare) {
this.$downloadBtn.innerText = "Download again";
}
Events.fire('notify-user', `${descriptor} downloaded successfully`);
this.$downloadBtn.style.pointerEvents = "none";
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => {
document.title = `PairDrop - ${files.length} Files received`;
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.continue();
this.show();
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r));
}
_downloadFilesIndividually(files) {
let tmpBtn = document.createElement("a");
for (let i=0; i<files.length; i++) {
tmpBtn.download = files[i].name;
tmpBtn.href = URL.createObjectURL(files[i]);
tmpBtn.click();
}
}
hide() {
this.$shareOrDownloadBtn.removeAttribute('href');
this.$shareOrDownloadBtn.removeAttribute('download');
this.$shareBtn.setAttribute('hidden', '');
this.$previewBox.innerHTML = '';
super.hide();
this._dequeueFile();
@ -662,11 +698,6 @@ class ReceiveRequestDialog extends ReceiveDialog {
constructor() {
super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-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.$declineRequestBtn = this.$el.querySelector('#decline-request');
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
@ -698,32 +729,18 @@ class ReceiveRequestDialog extends ReceiveDialog {
_showRequestDialog(request, peerId) {
this.correspondingPeerId = peerId;
this.$requestingPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
const fileName = request.header[0].name;
const fileNameSplit = fileName.split('.');
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
this.$fileStemNode.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtensionNode.innerText = fileExtension
if (request.header.length >= 2) {
let fileOtherText = ` and ${request.header.length - 1} other `;
fileOtherText += request.imagesOnly ? 'image' : 'file';
if (request.header.length > 2) fileOtherText += "s";
this.$fileOtherNode.innerText = fileOtherText;
}
this.$fileSizeNode.innerText = this._formatFileSize(request.totalSize);
const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img');
element.src = request.thumbnailDataUrl;
element.classList.add('element-preview');
this.$previewBox.appendChild(element)
}
document.title = 'PairDrop - File Transfer Requested';
this.$receiveTitle.innerText = `${request.imagesOnly ? 'Image' : 'File'} Transfer Request`
document.title = `${request.imagesOnly ? 'Image' : 'File'} Transfer Requested - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
}
@ -758,7 +775,7 @@ class PairDeviceDialog extends Dialog {
this.$clearSecretsBtn = $('clear-pair-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
let createJoinForm = this.$el.querySelector('form');
createJoinForm.addEventListener('submit', _ => this._onSubmit());
createJoinForm.addEventListener('submit', e => this._onSubmit(e));
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
@ -837,7 +854,7 @@ class PairDeviceDialog extends Dialog {
})
this.$submitBtn.removeAttribute("disabled");
if (document.activeElement === this.$inputRoomKeyChars[5]) {
this._onSubmit();
this._pairDeviceJoin(this.inputRoomKey);
}
}
}
@ -887,7 +904,8 @@ class PairDeviceDialog extends Dialog {
return url.href;
}
_onSubmit() {
_onSubmit(e) {
e.preventDefault();
this._pairDeviceJoin(this.inputRoomKey);
}
@ -974,14 +992,19 @@ class ClearDevicesDialog extends Dialog {
super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
}
_onClearPairDevices() {
this.show();
}
_onSubmit() {
_onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets');
this.hide();
}
@ -992,10 +1015,10 @@ class SendTextDialog extends Dialog {
super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name');
this.$peerDisplayName = this.$el.querySelector('.display-name');
this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send());
this.$form.addEventListener('submit', e => this._onSubmit(e));
this.$text.addEventListener('input', e => this._onChange(e));
Events.on("keydown", e => this._onKeyDown(e));
}
@ -1037,6 +1060,11 @@ class SendTextDialog extends Dialog {
sel.addRange(range);
}
_onSubmit(e) {
e.preventDefault();
this._send();
}
_send() {
Events.fire('send-text', {
to: this.correspondingPeerId,
@ -1060,7 +1088,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name');
this.$displayNameNode = this.$el.querySelector('.display-name');
this._receiveTextQueue = [];
}
@ -1078,6 +1106,7 @@ class ReceiveTextDialog extends Dialog {
_onText(text, peerId) {
window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return;
this._dequeueRequests();
}
@ -1089,23 +1118,35 @@ class ReceiveTextDialog extends Dialog {
}
_showReceiveTextDialog(text, peerId) {
this.$receiveTextPeerDisplayNameNode.innerText = $(peerId).ui._displayName();
this.$displayNameNode.innerText = $(peerId).ui._displayName();
if (isURL(text)) {
const $a = document.createElement('a');
$a.href = text;
$a.target = '_blank';
$a.textContent = text;
this.$text.innerHTML = '';
this.$text.appendChild($a);
} else {
this.$text.textContent = text;
this.$text.innerText = text;
this.$text.classList.remove('text-center');
// Beautify text if text is short
if (text.length < 2000) {
// replace urls with actual links
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => {
return `<a href="${url}" target="_blank">${url}</a>`;
});
if (!/\s/.test(text)) {
this.$text.classList.add('text-center');
}
}
document.title = 'PairDrop - Message Received';
this._setDocumentTitleMessages();
document.changeFavicon("images/favicon-96x96-notification.png");
this.show();
}
_setDocumentTitleMessages() {
document.title = !this._receiveTextQueue.length
? 'Message Received - PairDrop'
: `${this._receiveTextQueue.length + 1} Messages Received - PairDrop`;
}
async _onCopy() {
await navigator.clipboard.writeText(this.$text.textContent);
Events.fire('notify-user', 'Copied to clipboard');
@ -1178,7 +1219,7 @@ class Base64ZipDialog extends Dialog {
}
_setPasteBtnToProcessing() {
this.$pasteBtn.pointerEvents = "none";
this.$pasteBtn.style.pointerEvents = "none";
this.$pasteBtn.innerText = "Processing...";
}
@ -1323,7 +1364,7 @@ class Notifications {
_messageNotification(message, peerId) {
if (document.visibilityState !== 'visible') {
const peerDisplayName = $(peerId).ui._displayName();
if (isURL(message)) {
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
const notification = this._notify(`Link received by ${peerDisplayName} - Click to open`, message);
this._bind(notification, _ => window.open(message, '_blank', null, true));
} else {
@ -1384,7 +1425,7 @@ class NetworkStatusUI {
constructor() {
Events.on('offline', _ => this._showOfflineMessage());
Events.on('online', _ => this._showOnlineMessage());
Events.on('ws-connected', _ => this._showOnlineMessage());
Events.on('ws-connected', _ => this._onWsConnected());
Events.on('ws-disconnected', _ => this._onWsDisconnected());
if (!navigator.onLine) this._showOfflineMessage();
}
@ -1395,17 +1436,16 @@ class NetworkStatusUI {
}
_showOnlineMessage() {
window.animateBackground(true);
if (!this.firstConnect) {
this.firstConnect = true;
return;
}
Events.fire('notify-user', 'You are back online');
window.animateBackground(true);
}
_onWsConnected() {
window.animateBackground(true);
}
_onWsDisconnected() {
window.animateBackground(false);
if (!this.firstConnect) this.firstConnect = true;
}
}
@ -1743,8 +1783,8 @@ Events.on('load', () => {
let x0, y0, w, h, dw, offset;
function init() {
w = window.innerWidth;
h = window.innerHeight;
w = document.documentElement.clientWidth;
h = document.documentElement.clientHeight;
c.width = w;
c.height = h;
offset = $$('footer').offsetHeight - 32;

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 urlsToCache = [
'index.html',

View file

@ -590,7 +590,7 @@ x-dialog x-background {
z-index: 10;
transition: opacity 300ms;
will-change: opacity;
padding: 35px;
padding: 15px;
overflow: overlay;
}
@ -601,19 +601,20 @@ x-dialog x-paper {
padding: 16px 24px;
width: 100%;
max-width: 400px;
overflow: hidden;
box-sizing: border-box;
transition: transform 300ms;
will-change: transform;
}
#pair-device-dialog x-paper {
position: absolute;
top: max(50%, 350px);
height: 650px;
margin-top: -325px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: max(50%, 350px);
margin-top: -328.5px;
width: calc(100vw - 20px);
height: 625px;
}
x-dialog:not([show]) {
@ -628,12 +629,6 @@ x-dialog:not([show]) x-background {
opacity: 0;
}
x-dialog .row-reverse>.button {
margin-top: 0;
margin-bottom: -16px;
width: 50%;
height: 50px;
}
x-dialog a {
color: var(--primary-color);
@ -672,7 +667,7 @@ x-dialog .font-subheading {
}
#key-input-container>input:nth-of-type(4) {
margin-left: 18px;
margin-left: 5%;
}
#room-key {
@ -684,16 +679,11 @@ x-dialog .font-subheading {
}
#room-key-qr-code {
padding: inherit;
margin: auto;
width: 150px;
height: 150px;
margin: 16px;
}
#pair-device-dialog hr {
margin-top: 40px;
margin-bottom: 40px;
width: 100%;
margin: 40px -24px;
}
#pair-device-dialog x-background {
@ -707,29 +697,24 @@ x-dialog .row {
margin-bottom: 8px;
}
x-dialog h2 {
margin-top: 1rem;
}
#receive-request-dialog h2,
#receive-file-dialog h2 {
margin-bottom: 0.5rem;
}
x-dialog .row-reverse {
margin: 40px -24px 0;
/* button row*/
x-paper > div:last-child {
margin: auto -24px -15px;
border-top: solid 2.5px var(--border-color);
height: 50px;
}
.separator {
border: solid 1.25px var(--border-color);
margin-bottom: -16px;
x-paper > div:last-child > .button {
height: 100%;
width: 50%;
}
x-paper > div:last-child > .button:not(:last-child) {
border-left: solid 2.5px var(--border-color);
}
.file-description {
word-break: break-word;
width: 80%;
margin: auto;
margin-bottom: 25px;
}
.file-description .row {
@ -741,26 +726,26 @@ x-dialog .row-reverse {
word-break: normal;
}
#file-name {
.file-name {
font-style: italic;
max-width: 100%;
}
#file-stem {
max-width: 80%;
.file-stem {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
max-height: 20px;
}
.file-size{
margin-bottom: 30px;
white-space: nowrap;
}
/* Send Text Dialog */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
#text-input {
min-height: 120px;
min-height: 200px;
margin: 14px auto;
}
/* Receive Text Dialog */
@ -768,14 +753,14 @@ x-dialog .row-reverse {
#receive-text-dialog #text {
width: 100%;
word-break: break-all;
max-height: 300px;
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
white-space: pre-wrap;
margin-top:36px;
padding: 15px 0;
}
#receive-text-dialog #text a {
@ -794,11 +779,7 @@ x-dialog .row-reverse {
.row-separator {
border-bottom: solid 2.5px var(--border-color);
margin: auto -25px;
}
#receive-text-description-container {
margin-bottom: 25px;
margin: auto -24px;
}
#base64-paste-btn {
@ -826,7 +807,6 @@ x-dialog .row-reverse {
padding: 2px 16px 0;
box-sizing: border-box;
min-height: 36px;
min-width: 100px;
font-size: 14px;
line-height: 24px;
font-weight: 700;
@ -837,6 +817,7 @@ x-dialog .row-reverse {
user-select: none;
background: inherit;
color: var(--primary-color);
overflow: hidden;
}
.button[disabled] {
@ -874,7 +855,7 @@ x-dialog .row-reverse {
opacity: 0.1;
}
#cancel-paste-mode-btn {
#cancel-paste-mode {
z-index: 2;
margin: 0;
padding: 0;
@ -901,7 +882,6 @@ button::-moz-focus-inner {
/* Icon Button */
.icon-button {
width: 40px;
height: 40px;
@ -911,10 +891,7 @@ button::-moz-focus-inner {
border-radius: 50%;
}
/* Text Input */
.textarea {
box-sizing: border-box;
border: none;
@ -928,9 +905,8 @@ button::-moz-focus-inner {
display: block;
overflow: auto;
resize: none;
min-height: 40px;
line-height: 16px;
max-height: 300px;
max-height: calc(100vh - 254px);
white-space: pre;
}
@ -1120,6 +1096,14 @@ x-peers:empty~x-instructions {
}
/* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
padding: 15px;
}
x-paper > div:last-child {
margin: auto -15px -15px;
}
}
@media screen and (min-height: 800px) {
#websocket-fallback {
@ -1192,7 +1176,9 @@ x-dialog x-paper {
display: none;
}
.element-preview {
.file-preview > img,
.file-preview > audio,
.file-preview > video {
max-width: 100%;
max-height: 40vh;
margin: auto;