Compare commits

...

7 Commits

11 changed files with 296 additions and 225 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ If applicable, add screenshots to help explain your problem.
**Bug occurs on official PairDrop instance https://pairdrop.net/** **Bug occurs on official PairDrop instance https://pairdrop.net/**
No | Yes No | Yes
Version: v1.9.0 Version: v1.9.1
**Bug occurs on self-hosted PairDrop instance** **Bug occurs on self-hosted PairDrop instance**
No | Yes No | Yes
@@ -44,7 +44,7 @@ No | Yes
**Self-Hosted Setup** **Self-Hosted Setup**
Proxy: Nginx | Apache2 Proxy: Nginx | Apache2
Deployment: docker run | docker-compose | npm run start:prod Deployment: docker run | docker-compose | npm run start:prod
Version: v1.9.0 Version: v1.9.1
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.9.0", "version": "1.9.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pairdrop", "name": "pairdrop",
"version": "1.9.0", "version": "1.9.1",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.9.0", "version": "1.9.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+1 -1
View File
@@ -480,7 +480,7 @@
</svg> </svg>
<div class="title-wrapper" dir="ltr"> <div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1> <h1>PairDrop</h1>
<div class="font-subheading">v1.9.0</div> <div class="font-subheading">v1.9.1</div>
</div> </div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div> <div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row"> <div class="row">
+66 -55
View File
@@ -37,7 +37,7 @@ class ServerConnection {
_connect() { _connect() {
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return; if (this._isConnected() || this._isConnecting() || this._isOffline()) return;
if (this._isReconnect) { if (this._isReconnect) {
Events.fire('notify-user', { Events.fire('notify-user', {
message: Localization.getTranslation("notifications.connecting"), message: Localization.getTranslation("notifications.connecting"),
@@ -246,6 +246,10 @@ class ServerConnection {
return this._socket && this._socket.readyState === this._socket.CONNECTING; return this._socket && this._socket.readyState === this._socket.CONNECTING;
} }
_isOffline() {
return !navigator.onLine;
}
_onError(e) { _onError(e) {
console.error(e); console.error(e);
} }
@@ -266,6 +270,9 @@ class Peer {
this._roomIds = {}; this._roomIds = {};
this._updateRoomIds(roomType, roomId); this._updateRoomIds(roomType, roomId);
this._chunkSize = 262144;
this._lowWaterMark = 2* this._chunkSize;
this._filesQueue = []; this._filesQueue = [];
this._busy = false; this._busy = false;
@@ -444,22 +451,9 @@ class Peer {
mime: file.type mime: file.type
}); });
this._chunker = new FileChunker(file, this._chunker = new FileChunker(file,
chunk => this._send(chunk), this._chunkSize,
offset => this._onPartitionEnd(offset)); chunk => this._send(chunk));
this._chunker.nextPartition(); this._chunker._readChunksIntoBuffer(this._channel ? this._channel.bufferedAmount : 0);
}
_onPartitionEnd(offset) {
this.sendJSON({ type: 'partition', offset: offset });
}
_onReceivedPartitionEnd(offset) {
this.sendJSON({ type: 'partition-received', offset: offset });
}
_sendNextPartition() {
if (!this._chunker || this._chunker.isFileEnd()) return;
this._chunker.nextPartition();
} }
_sendProgress(progress) { _sendProgress(progress) {
@@ -479,12 +473,6 @@ class Peer {
case 'header': case 'header':
this._onFileHeader(messageJSON); this._onFileHeader(messageJSON);
break; break;
case 'partition':
this._onReceivedPartitionEnd(messageJSON);
break;
case 'partition-received':
this._sendNextPartition();
break;
case 'progress': case 'progress':
this._onDownloadProgress(messageJSON.progress); this._onDownloadProgress(messageJSON.progress);
break; break;
@@ -492,7 +480,7 @@ class Peer {
this._onFileTransferRequestResponded(messageJSON); this._onFileTransferRequestResponded(messageJSON);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted(messageJSON);
break; break;
case 'message-transfer-complete': case 'message-transfer-complete':
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
@@ -548,6 +536,7 @@ class Peer {
_onFileHeader(header) { _onFileHeader(header) {
if (this._requestAccepted && this._requestAccepted.header.length) { if (this._requestAccepted && this._requestAccepted.header.length) {
this._lastProgress = 0; this._lastProgress = 0;
this._timeStart = Date.now();
this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},
this._requestAccepted.totalSize, this._requestAccepted.totalSize,
this._totalBytesReceived, this._totalBytesReceived,
@@ -567,7 +556,6 @@ class Peer {
_onChunkReceived(chunk) { _onChunkReceived(chunk) {
if(!this._digester || !(chunk.byteLength || chunk.size)) return; if(!this._digester || !(chunk.byteLength || chunk.size)) return;
this._digester.unchunk(chunk); this._digester.unchunk(chunk);
const progress = this._digester.progress; const progress = this._digester.progress;
@@ -591,7 +579,13 @@ class Peer {
const acceptedHeader = this._requestAccepted.header.shift(); const acceptedHeader = this._requestAccepted.header.shift();
this._totalBytesReceived += fileBlob.size; this._totalBytesReceived += fileBlob.size;
this.sendJSON({type: 'file-transfer-complete'}); let duration = (Date.now() - this._timeStart) / 1000;
let size = Math.round(10 * fileBlob.size / 1000000) / 10;
let speed = Math.round(100 * fileBlob.size / 1000000 / duration) / 100;
console.log(`File received.\n\nSize: ${size} MB\tDuration: ${duration} s\tSpeed: ${speed} MB/s`);
this.sendJSON({type: 'file-transfer-complete', size: size, duration: duration, speed: speed});
const sameSize = fileBlob.size === acceptedHeader.size; const sameSize = fileBlob.size === acceptedHeader.size;
const sameName = fileBlob.name === acceptedHeader.name const sameName = fileBlob.name === acceptedHeader.name
@@ -612,8 +606,10 @@ class Peer {
} }
} }
_onFileTransferCompleted() { _onFileTransferCompleted(message) {
this._chunker = null; console.log(`File sent.\n\nSize: ${message.size} MB\tDuration: ${message.duration} s\tSpeed: ${message.speed} MB/s`);
if (!this._filesQueue.length) { if (!this._filesQueue.length) {
this._busy = false; this._busy = false;
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed")); Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
@@ -621,6 +617,8 @@ class Peer {
} else { } else {
this._dequeueFile(); this._dequeueFile();
} }
this._chunker._removeEventListener();
this._chunker = null;
} }
_onFileTransferRequestResponded(message) { _onFileTransferRequestResponded(message) {
@@ -698,8 +696,7 @@ class RTCPeer extends Peer {
if (!this._conn) return; if (!this._conn) return;
const channel = this._conn.createDataChannel('data-channel', { const channel = this._conn.createDataChannel('data-channel', {
ordered: true, ordered: false,
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
}); });
channel.onopen = e => this._onChannelOpened(e); channel.onopen = e => this._onChannelOpened(e);
channel.onerror = e => this._onError(e); channel.onerror = e => this._onError(e);
@@ -745,7 +742,15 @@ class RTCPeer extends Peer {
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
channel.onclose = _ => this._onChannelClosed(); channel.onclose = _ => this._onChannelClosed();
this._chunkSize = Math.min(this._conn.sctp.maxMessageSize, 262144); // max chunk size: 256 KB
this._lowWaterMark = 2 * this._chunkSize;
channel.bufferedAmountLowThreshold = this._lowWaterMark;
channel.onbufferedamountlow = () => Events.fire("bufferedamountlow", channel.bufferedAmount);
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e)); Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide()); Events.on('pagehide', _ => this._onPageHide());
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()}); Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
@@ -1091,51 +1096,56 @@ class PeersManager {
class FileChunker { class FileChunker {
constructor(file, onChunk, onPartitionEnd) { constructor(file, chunkSize, onChunk) {
this._chunkSize = 64000; // 64 KB
this._maxPartitionSize = 1e6; // 1 MB
this._offset = 0;
this._partitionSize = 0;
this._file = file; this._file = file;
this._chunkSize = chunkSize;
this._highWaterMark = 8 * chunkSize;
this._bytesToSend = file.size;
this._sendProgress = 0;
this._onChunk = onChunk; this._onChunk = onChunk;
this._onPartitionEnd = onPartitionEnd;
this._reader = new FileReader(); this._reader = new FileReader();
this._reader.addEventListener('error', err => console.error('Error reading file:', err));
this._reader.addEventListener('abort', e => console.log('File reading aborted:', e));
this._reader.addEventListener('load', e => this._onChunkRead(e.target.result)); this._reader.addEventListener('load', e => this._onChunkRead(e.target.result));
this.bufferedAmountCallback = e => this._readChunksIntoBuffer(e.detail);
Events.on('bufferedamountlow', this.bufferedAmountCallback);
} }
nextPartition() { _removeEventListener() {
this._partitionSize = 0; Events.off('bufferedamountlow', this.bufferedAmountCallback);
}
_readChunksIntoBuffer(bufferedAmount) {
this._bufferedAmount = bufferedAmount;
this._readChunk(); this._readChunk();
} }
_readChunk() { _readChunk() {
const chunk = this._file.slice(this._offset, this._offset + this._chunkSize); const chunk = this._file.slice(this._sendProgress, this._sendProgress + this._chunkSize);
this._reader.readAsArrayBuffer(chunk); this._reader.readAsArrayBuffer(chunk);
} }
_onChunkRead(chunk) { _onChunkRead(chunk) {
this._offset += chunk.byteLength;
this._partitionSize += chunk.byteLength;
this._onChunk(chunk); this._onChunk(chunk);
if (this.isFileEnd()) return; this._bufferedAmount += this._chunkSize;
if (this._isPartitionEnd()) { this._sendProgress += this._chunkSize;
this._onPartitionEnd(this._offset);
return; if (this._isBufferFull() || this._isFileEnd()) return;
}
this._readChunk(); this._readChunk();
} }
repeatPartition() { _isBufferFull() {
this._offset -= this._partitionSize; return this._bufferedAmount >= this._highWaterMark;
this.nextPartition();
} }
_isPartitionEnd() { _isFileEnd() {
return this._partitionSize >= this._maxPartitionSize; return this._sendProgress >= this._bytesToSend;
}
isFileEnd() {
return this._offset >= this._file.size;
} }
} }
@@ -1159,6 +1169,7 @@ class FileDigester {
if (isNaN(this.progress)) this.progress = 1 if (isNaN(this.progress)) this.progress = 1
if (this._bytesReceived < this._size) return; if (this._bytesReceived < this._size) return;
// we are done // we are done
const blob = new Blob(this._buffer) const blob = new Blob(this._buffer)
this._buffer = null; this._buffer = null;
+41 -23
View File
@@ -68,12 +68,25 @@ class PeersUI {
this.fadedIn = false; this.fadedIn = false;
this.$header = document.querySelector('header.opacity-0'); this.$header = document.querySelector('header.opacity-0');
Events.on('header-evaluated', () => this._fadeInHeader()); Events.on('header-evaluated', e => this._fadeInHeader(e.detail));
// wait for evaluation of notification, install and edit-paired-devices buttons
this.evaluateHeaderCount = 3;
if (!('Notification' in window)) this.evaluateHeaderCount -= 1;
if (
!('BeforeInstallPromptEvent' in window) ||
('BeforeInstallPromptEvent' in window && window.matchMedia('(display-mode: minimal-ui)').matches)
) {
this.evaluateHeaderCount -= 1;
}
} }
_fadeInHeader() { _fadeInHeader(id) {
//prevent flickering this.evaluateHeaderCount -= 1;
setTimeout(() => this.$header.classList.remove('opacity-0'), 50); console.log(`Header btn ${id} evaluated. ${this.evaluateHeaderCount} to go.`);
if (this.evaluateHeaderCount !== 0) return;
this.$header.classList.remove('opacity-0');
} }
_fadeInUI() { _fadeInUI() {
@@ -735,14 +748,14 @@ class ReceiveDialog extends Dialog {
_formatFileSize(bytes) { _formatFileSize(bytes) {
// 1 GB = 1024 MB = 1024^2 KB = 1024^3 B // 1 GB = 1024 MB = 1024^2 KB = 1024^3 B
// 1024^2 = 104876; 1024^3 = 1073741824 // 1024^2 = 104876; 1024^3 = 1073741824
if (bytes >= 1073741824) { if (bytes >= 1000000000) {
return Math.round(10 * bytes / 1073741824) / 10 + ' GB'; return Math.round(10 * bytes / 1000000000) / 10 + ' GB';
} else if (bytes >= 1048576) { } else if (bytes >= 1000000) {
return Math.round(bytes / 1048576) + ' MB'; return Math.round(10 * bytes / 1000000) / 10 + ' MB';
} else if (bytes > 1024) { } else if (bytes >= (1000)) {
return Math.round(bytes / 1024) + ' KB'; return Math.round(10 * bytes / 1000) / 10 + ' KB';
} else { } else {
return bytes + ' Bytes'; return bytes + ' bytes';
} }
} }
@@ -1413,6 +1426,7 @@ class PairDeviceDialog extends Dialog {
this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
} }
Events.fire('evaluate-footer-badges'); Events.fire('evaluate-footer-badges');
Events.fire('header-evaluated', 'edit-paired-devices');
}); });
} }
} }
@@ -2092,10 +2106,7 @@ class Notifications {
constructor() { constructor() {
// Check if the browser supports notifications // Check if the browser supports notifications
if (!('Notification' in window)) { if (!('Notification' in window)) return;
Events.fire('header-evaluated');
return;
}
// Check whether notification permissions have already been granted // Check whether notification permissions have already been granted
if (Notification.permission !== 'granted') { if (Notification.permission !== 'granted') {
@@ -2104,7 +2115,7 @@ class Notifications {
this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission()); this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission());
} }
Events.fire('header-evaluated'); Events.fire('header-evaluated', 'notification');
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files)); Events.on('files-received', e => this._downloadNotification(e.detail.files));
@@ -2258,7 +2269,10 @@ class NetworkStatusUI {
} }
_showOfflineMessage() { _showOfflineMessage() {
Events.fire('notify-user', Localization.getTranslation("notifications.offline")); Events.fire('notify-user', {
message: Localization.getTranslation("notifications.offline"),
persistent: true
});
} }
_showOnlineMessage() { _showOnlineMessage() {
@@ -2815,12 +2829,16 @@ if ('serviceWorker' in navigator) {
}); });
} }
window.addEventListener('beforeinstallprompt', e => { window.addEventListener('beforeinstallprompt', installEvent => {
if (!window.matchMedia('(display-mode: minimal-ui)').matches) { if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
// only display install btn when installed // only display install btn when not installed
const btn = document.querySelector('#install') const installBtn = document.querySelector('#install')
btn.hidden = false; installBtn.removeAttribute('hidden');
btn.onclick = _ => e.prompt(); installBtn.addEventListener('click', () => {
installBtn.setAttribute('hidden', '');
installEvent.prompt();
});
Events.fire('header-evaluated', 'install');
} }
return e.preventDefault(); return installEvent.preventDefault();
}); });
+35 -28
View File
@@ -1,16 +1,18 @@
const cacheVersion = 'v1.9.0'; const cacheVersion = 'v1.9.1';
const cacheTitle = `pairdrop-cache-${cacheVersion}`; const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html',
'./', './',
'index.html',
'manifest.json',
'styles.css', 'styles.css',
'scripts/localization.js',
'scripts/network.js', 'scripts/network.js',
'scripts/NoSleep.min.js',
'scripts/QRCode.min.js',
'scripts/theme.js',
'scripts/ui.js', 'scripts/ui.js',
'scripts/util.js', 'scripts/util.js',
'scripts/qrcode.js',
'scripts/zip.min.js', 'scripts/zip.min.js',
'scripts/NoSleep.min.js',
'scripts/theme.js',
'sounds/blop.mp3', 'sounds/blop.mp3',
'images/favicon-96x96.png', 'images/favicon-96x96.png',
'images/favicon-96x96-notification.png', 'images/favicon-96x96-notification.png',
@@ -53,7 +55,7 @@ const fromNetwork = (request, timeout) =>
fetch(request).then(response => { fetch(request).then(response => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
fulfill(response); fulfill(response);
update(request); update(request).then(() => console.log("Cache successfully updated for", request.url));
}, reject); }, reject);
}); });
@@ -62,9 +64,7 @@ const fromCache = request =>
caches caches
.open(cacheTitle) .open(cacheTitle)
.then(cache => .then(cache =>
cache cache.match(request)
.match(request)
.then(matching => matching || cache.match('/offline/'))
); );
// cache the current page to make it available for offline // cache the current page to make it available for offline
@@ -72,15 +72,16 @@ const update = request =>
caches caches
.open(cacheTitle) .open(cacheTitle)
.then(cache => .then(cache =>
fetch(request).then(response => { fetch(request)
cache.put(request, response).then(_ => { .then(async response => {
console.log("Page successfully cached.") await cache.put(request, response);
}) })
}) .catch(() => console.log(`Cache could not be updated. ${request.url}`))
); );
// general strategy when making a request (eg if online try to fetch it // general strategy when making a request (eg if online try to fetch it
// from the network with a timeout, if something fails serve from cache) // from cache, if something fails fetch from network. Update cache everytime files are fetched.
// This way files should only be fetched if cacheVersion is changed
self.addEventListener('fetch', function(event) { self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") { if (event.request.method === "POST") {
// Requests related to Web Share Target. // Requests related to Web Share Target.
@@ -90,27 +91,33 @@ self.addEventListener('fetch', function(event) {
})()); })());
} else { } else {
// Regular requests not related to Web Share Target. // Regular requests not related to Web Share Target.
// FOR DEVELOPMENT: Comment in next line to always update assets instead of using cached versions
// event.respondWith(fromNetwork(event.request, 10000));return;
event.respondWith( event.respondWith(
fromNetwork(event.request, 10000).catch(() => fromCache(event.request)) fromCache(event.request).then(rsp => {
// if fromCache resolves to undefined fetch from network instead
return rsp || fromNetwork(event.request, 10000);
})
); );
event.waitUntil(update(event.request));
} }
}); });
// on activation, we clean up the previously registered service workers // on activation, we clean up the previously registered service workers
self.addEventListener('activate', evt => self.addEventListener('activate', evt => {
evt.waitUntil( return evt.waitUntil(
caches.keys().then(cacheNames => { caches.keys().then(cacheNames => {
return Promise.all( return Promise.all(
cacheNames.map(cacheName => { cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) { if (cacheName !== cacheTitle) {
return caches.delete(cacheName); return caches.delete(cacheName);
} }
}) })
); );
}) })
) )
}
); );
const evaluateRequestData = function (request) { const evaluateRequestData = function (request) {
+1 -1
View File
@@ -485,7 +485,7 @@
</svg> </svg>
<div class="title-wrapper" dir="ltr"> <div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1> <h1>PairDrop</h1>
<div class="font-subheading">v1.9.0</div> <div class="font-subheading">v1.9.1</div>
</div> </div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div> <div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row"> <div class="row">
+69 -59
View File
@@ -1,5 +1,5 @@
window.URL = window.URL || window.webkitURL; window.URL = window.URL || window.webkitURL;
window.isRtcSupported = false; //!!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection); window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
window.hiddenProperty = 'hidden' in document ? 'hidden' : window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' : 'webkitHidden' in document ? 'webkitHidden' :
@@ -35,7 +35,7 @@ class ServerConnection {
_connect() { _connect() {
clearTimeout(this._reconnectTimer); clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return; if (this._isConnected() || this._isConnecting() || this._isOffline()) return;
if (this._isReconnect) { if (this._isReconnect) {
Events.fire('notify-user', { Events.fire('notify-user', {
message: Localization.getTranslation("notifications.connecting"), message: Localization.getTranslation("notifications.connecting"),
@@ -158,8 +158,6 @@ class ServerConnection {
break; break;
case 'request': case 'request':
case 'header': case 'header':
case 'partition':
case 'partition-received':
case 'progress': case 'progress':
case 'files-transfer-response': case 'files-transfer-response':
case 'file-transfer-complete': case 'file-transfer-complete':
@@ -257,6 +255,10 @@ class ServerConnection {
return this._socket && this._socket.readyState === this._socket.CONNECTING; return this._socket && this._socket.readyState === this._socket.CONNECTING;
} }
_isOffline() {
return !navigator.onLine;
}
_onError(e) { _onError(e) {
console.error(e); console.error(e);
} }
@@ -277,6 +279,9 @@ class Peer {
this._roomIds = {}; this._roomIds = {};
this._updateRoomIds(roomType, roomId); this._updateRoomIds(roomType, roomId);
this._chunkSize = 262144;
this._lowWaterMark = 2* this._chunkSize;
this._filesQueue = []; this._filesQueue = [];
this._busy = false; this._busy = false;
@@ -455,22 +460,9 @@ class Peer {
mime: file.type mime: file.type
}); });
this._chunker = new FileChunker(file, this._chunker = new FileChunker(file,
chunk => this._send(chunk), this._chunkSize,
offset => this._onPartitionEnd(offset)); chunk => this._send(chunk));
this._chunker.nextPartition(); this._chunker._readChunksIntoBuffer(this._channel ? this._channel.bufferedAmount : 0);
}
_onPartitionEnd(offset) {
this.sendJSON({ type: 'partition', offset: offset });
}
_onReceivedPartitionEnd(offset) {
this.sendJSON({ type: 'partition-received', offset: offset });
}
_sendNextPartition() {
if (!this._chunker || this._chunker.isFileEnd()) return;
this._chunker.nextPartition();
} }
_sendProgress(progress) { _sendProgress(progress) {
@@ -490,12 +482,6 @@ class Peer {
case 'header': case 'header':
this._onFileHeader(messageJSON); this._onFileHeader(messageJSON);
break; break;
case 'partition':
this._onReceivedPartitionEnd(messageJSON);
break;
case 'partition-received':
this._sendNextPartition();
break;
case 'progress': case 'progress':
this._onDownloadProgress(messageJSON.progress); this._onDownloadProgress(messageJSON.progress);
break; break;
@@ -503,7 +489,7 @@ class Peer {
this._onFileTransferRequestResponded(messageJSON); this._onFileTransferRequestResponded(messageJSON);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted(messageJSON);
break; break;
case 'message-transfer-complete': case 'message-transfer-complete':
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
@@ -559,6 +545,7 @@ class Peer {
_onFileHeader(header) { _onFileHeader(header) {
if (this._requestAccepted && this._requestAccepted.header.length) { if (this._requestAccepted && this._requestAccepted.header.length) {
this._lastProgress = 0; this._lastProgress = 0;
this._timeStart = Date.now();
this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime}, this._digester = new FileDigester({size: header.size, name: header.name, mime: header.mime},
this._requestAccepted.totalSize, this._requestAccepted.totalSize,
this._totalBytesReceived, this._totalBytesReceived,
@@ -578,7 +565,6 @@ class Peer {
_onChunkReceived(chunk) { _onChunkReceived(chunk) {
if(!this._digester || !(chunk.byteLength || chunk.size)) return; if(!this._digester || !(chunk.byteLength || chunk.size)) return;
this._digester.unchunk(chunk); this._digester.unchunk(chunk);
const progress = this._digester.progress; const progress = this._digester.progress;
@@ -602,7 +588,13 @@ class Peer {
const acceptedHeader = this._requestAccepted.header.shift(); const acceptedHeader = this._requestAccepted.header.shift();
this._totalBytesReceived += fileBlob.size; this._totalBytesReceived += fileBlob.size;
this.sendJSON({type: 'file-transfer-complete'}); let duration = (Date.now() - this._timeStart) / 1000;
let size = Math.round(10 * fileBlob.size / 1000000) / 10;
let speed = Math.round(100 * fileBlob.size / 1000000 / duration) / 100;
console.log(`File received.\n\nSize: ${size} MB\tDuration: ${duration} s\tSpeed: ${speed} MB/s`);
this.sendJSON({type: 'file-transfer-complete', size: size, duration: duration, speed: speed});
const sameSize = fileBlob.size === acceptedHeader.size; const sameSize = fileBlob.size === acceptedHeader.size;
const sameName = fileBlob.name === acceptedHeader.name const sameName = fileBlob.name === acceptedHeader.name
@@ -623,8 +615,10 @@ class Peer {
} }
} }
_onFileTransferCompleted() { _onFileTransferCompleted(message) {
this._chunker = null; console.log(`File sent.\n\nSize: ${message.size} MB\tDuration: ${message.duration} s\tSpeed: ${message.speed} MB/s`);
if (!this._filesQueue.length) { if (!this._filesQueue.length) {
this._busy = false; this._busy = false;
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed")); Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
@@ -632,6 +626,8 @@ class Peer {
} else { } else {
this._dequeueFile(); this._dequeueFile();
} }
this._chunker._removeEventListener();
this._chunker = null;
} }
_onFileTransferRequestResponded(message) { _onFileTransferRequestResponded(message) {
@@ -709,8 +705,7 @@ class RTCPeer extends Peer {
if (!this._conn) return; if (!this._conn) return;
const channel = this._conn.createDataChannel('data-channel', { const channel = this._conn.createDataChannel('data-channel', {
ordered: true, ordered: false,
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable
}); });
channel.onopen = e => this._onChannelOpened(e); channel.onopen = e => this._onChannelOpened(e);
channel.onerror = e => this._onError(e); channel.onerror = e => this._onError(e);
@@ -756,7 +751,15 @@ class RTCPeer extends Peer {
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
channel.onclose = _ => this._onChannelClosed(); channel.onclose = _ => this._onChannelClosed();
this._chunkSize = Math.min(this._conn.sctp.maxMessageSize, 262144); // max chunk size: 256 KB
this._lowWaterMark = 2 * this._chunkSize;
channel.bufferedAmountLowThreshold = this._lowWaterMark;
channel.onbufferedamountlow = () => Events.fire("bufferedamountlow", channel.bufferedAmount);
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e)); Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide()); Events.on('pagehide', _ => this._onPageHide());
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()}); Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
@@ -900,6 +903,7 @@ class WSPeer extends Peer {
type: 'ws-chunk', type: 'ws-chunk',
chunk: arrayBufferToBase64(chunk) chunk: arrayBufferToBase64(chunk)
}); });
Events.fire("bufferedamountlow", 0);
} }
sendJSON(message) { sendJSON(message) {
@@ -988,7 +992,7 @@ class PeersManager {
} }
if (window.isRtcSupported && rtcSupported) { if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server,isCaller, peerId, roomType, roomId); this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId);
} else { } else {
this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId); this.peers[peerId] = new WSPeer(this._server, isCaller, peerId, roomType, roomId);
} }
@@ -1163,51 +1167,56 @@ class PeersManager {
class FileChunker { class FileChunker {
constructor(file, onChunk, onPartitionEnd) { constructor(file, chunkSize, onChunk) {
this._chunkSize = 64000; // 64 KB
this._maxPartitionSize = 1e6; // 1 MB
this._offset = 0;
this._partitionSize = 0;
this._file = file; this._file = file;
this._chunkSize = chunkSize;
this._highWaterMark = 8 * chunkSize;
this._bytesToSend = file.size;
this._sendProgress = 0;
this._onChunk = onChunk; this._onChunk = onChunk;
this._onPartitionEnd = onPartitionEnd;
this._reader = new FileReader(); this._reader = new FileReader();
this._reader.addEventListener('error', err => console.error('Error reading file:', err));
this._reader.addEventListener('abort', e => console.log('File reading aborted:', e));
this._reader.addEventListener('load', e => this._onChunkRead(e.target.result)); this._reader.addEventListener('load', e => this._onChunkRead(e.target.result));
this.bufferedAmountCallback = e => this._readChunksIntoBuffer(e.detail);
Events.on('bufferedamountlow', this.bufferedAmountCallback);
} }
nextPartition() { _removeEventListener() {
this._partitionSize = 0; Events.off('bufferedamountlow', this.bufferedAmountCallback);
}
_readChunksIntoBuffer(bufferedAmount) {
this._bufferedAmount = bufferedAmount;
this._readChunk(); this._readChunk();
} }
_readChunk() { _readChunk() {
const chunk = this._file.slice(this._offset, this._offset + this._chunkSize); const chunk = this._file.slice(this._sendProgress, this._sendProgress + this._chunkSize);
this._reader.readAsArrayBuffer(chunk); this._reader.readAsArrayBuffer(chunk);
} }
_onChunkRead(chunk) { _onChunkRead(chunk) {
this._offset += chunk.byteLength;
this._partitionSize += chunk.byteLength;
this._onChunk(chunk); this._onChunk(chunk);
if (this.isFileEnd()) return; this._bufferedAmount += this._chunkSize;
if (this._isPartitionEnd()) { this._sendProgress += this._chunkSize;
this._onPartitionEnd(this._offset);
return; if (this._isBufferFull() || this._isFileEnd()) return;
}
this._readChunk(); this._readChunk();
} }
repeatPartition() { _isBufferFull() {
this._offset -= this._partitionSize; return this._bufferedAmount >= this._highWaterMark;
this.nextPartition();
} }
_isPartitionEnd() { _isFileEnd() {
return this._partitionSize >= this._maxPartitionSize; return this._sendProgress >= this._bytesToSend;
}
isFileEnd() {
return this._offset >= this._file.size;
} }
} }
@@ -1231,6 +1240,7 @@ class FileDigester {
if (isNaN(this.progress)) this.progress = 1 if (isNaN(this.progress)) this.progress = 1
if (this._bytesReceived < this._size) return; if (this._bytesReceived < this._size) return;
// we are done // we are done
const blob = new Blob(this._buffer) const blob = new Blob(this._buffer)
this._buffer = null; this._buffer = null;
+41 -23
View File
@@ -68,12 +68,25 @@ class PeersUI {
this.fadedIn = false; this.fadedIn = false;
this.$header = document.querySelector('header.opacity-0'); this.$header = document.querySelector('header.opacity-0');
Events.on('header-evaluated', () => this._fadeInHeader()); Events.on('header-evaluated', e => this._fadeInHeader(e.detail));
// wait for evaluation of notification, install and edit-paired-devices buttons
this.evaluateHeaderCount = 3;
if (!('Notification' in window)) this.evaluateHeaderCount -= 1;
if (
!('BeforeInstallPromptEvent' in window) ||
('BeforeInstallPromptEvent' in window && window.matchMedia('(display-mode: minimal-ui)').matches)
) {
this.evaluateHeaderCount -= 1;
}
} }
_fadeInHeader() { _fadeInHeader(id) {
//prevent flickering this.evaluateHeaderCount -= 1;
setTimeout(() => this.$header.classList.remove('opacity-0'), 50); console.log(`Header btn ${id} evaluated. ${this.evaluateHeaderCount} to go.`);
if (this.evaluateHeaderCount !== 0) return;
this.$header.classList.remove('opacity-0');
} }
_fadeInUI() { _fadeInUI() {
@@ -737,14 +750,14 @@ class ReceiveDialog extends Dialog {
_formatFileSize(bytes) { _formatFileSize(bytes) {
// 1 GB = 1024 MB = 1024^2 KB = 1024^3 B // 1 GB = 1024 MB = 1024^2 KB = 1024^3 B
// 1024^2 = 104876; 1024^3 = 1073741824 // 1024^2 = 104876; 1024^3 = 1073741824
if (bytes >= 1073741824) { if (bytes >= 1000000000) {
return Math.round(10 * bytes / 1073741824) / 10 + ' GB'; return Math.round(10 * bytes / 1000000000) / 10 + ' GB';
} else if (bytes >= 1048576) { } else if (bytes >= 1000000) {
return Math.round(bytes / 1048576) + ' MB'; return Math.round(10 * bytes / 1000000) / 10 + ' MB';
} else if (bytes > 1024) { } else if (bytes >= (1000)) {
return Math.round(bytes / 1024) + ' KB'; return Math.round(10 * bytes / 1000) / 10 + ' KB';
} else { } else {
return bytes + ' Bytes'; return bytes + ' bytes';
} }
} }
@@ -1415,6 +1428,7 @@ class PairDeviceDialog extends Dialog {
this.$footerInstructionsPairedDevices.setAttribute('hidden', ''); this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
} }
Events.fire('evaluate-footer-badges'); Events.fire('evaluate-footer-badges');
Events.fire('header-evaluated', 'edit-paired-devices');
}); });
} }
} }
@@ -2094,10 +2108,7 @@ class Notifications {
constructor() { constructor() {
// Check if the browser supports notifications // Check if the browser supports notifications
if (!('Notification' in window)) { if (!('Notification' in window)) return;
Events.fire('header-evaluated');
return;
}
// Check whether notification permissions have already been granted // Check whether notification permissions have already been granted
if (Notification.permission !== 'granted') { if (Notification.permission !== 'granted') {
@@ -2106,7 +2117,7 @@ class Notifications {
this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission()); this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission());
} }
Events.fire('header-evaluated'); Events.fire('header-evaluated', 'notification');
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files)); Events.on('files-received', e => this._downloadNotification(e.detail.files));
@@ -2260,7 +2271,10 @@ class NetworkStatusUI {
} }
_showOfflineMessage() { _showOfflineMessage() {
Events.fire('notify-user', Localization.getTranslation("notifications.offline")); Events.fire('notify-user', {
message: Localization.getTranslation("notifications.offline"),
persistent: true
});
} }
_showOnlineMessage() { _showOnlineMessage() {
@@ -2817,12 +2831,16 @@ if ('serviceWorker' in navigator) {
}); });
} }
window.addEventListener('beforeinstallprompt', e => { window.addEventListener('beforeinstallprompt', installEvent => {
if (!window.matchMedia('(display-mode: minimal-ui)').matches) { if (!window.matchMedia('(display-mode: minimal-ui)').matches) {
// only display install btn when installed // only display install btn when not installed
const btn = document.querySelector('#install') const installBtn = document.querySelector('#install')
btn.hidden = false; installBtn.removeAttribute('hidden');
btn.onclick = _ => e.prompt(); installBtn.addEventListener('click', () => {
installBtn.setAttribute('hidden', '');
installEvent.prompt();
});
Events.fire('header-evaluated', 'install');
} }
return e.preventDefault(); return installEvent.preventDefault();
}); });
+35 -28
View File
@@ -1,16 +1,18 @@
const cacheVersion = 'v1.9.0'; const cacheVersion = 'v1.9.1';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html',
'./', './',
'index.html',
'manifest.json',
'styles.css', 'styles.css',
'scripts/localization.js',
'scripts/network.js', 'scripts/network.js',
'scripts/NoSleep.min.js',
'scripts/QRCode.min.js',
'scripts/theme.js',
'scripts/ui.js', 'scripts/ui.js',
'scripts/util.js', 'scripts/util.js',
'scripts/qrcode.js',
'scripts/zip.min.js', 'scripts/zip.min.js',
'scripts/NoSleep.min.js',
'scripts/theme.js',
'sounds/blop.mp3', 'sounds/blop.mp3',
'images/favicon-96x96.png', 'images/favicon-96x96.png',
'images/favicon-96x96-notification.png', 'images/favicon-96x96-notification.png',
@@ -53,7 +55,7 @@ const fromNetwork = (request, timeout) =>
fetch(request).then(response => { fetch(request).then(response => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
fulfill(response); fulfill(response);
update(request); update(request).then(() => console.log("Cache successfully updated for", request.url));
}, reject); }, reject);
}); });
@@ -62,9 +64,7 @@ const fromCache = request =>
caches caches
.open(cacheTitle) .open(cacheTitle)
.then(cache => .then(cache =>
cache cache.match(request)
.match(request)
.then(matching => matching || cache.match('/offline/'))
); );
// cache the current page to make it available for offline // cache the current page to make it available for offline
@@ -72,15 +72,16 @@ const update = request =>
caches caches
.open(cacheTitle) .open(cacheTitle)
.then(cache => .then(cache =>
fetch(request).then(response => { fetch(request)
cache.put(request, response).then(_ => { .then(async response => {
console.log("Page successfully cached.") await cache.put(request, response);
}) })
}) .catch(() => console.log(`Cache could not be updated. ${request.url}`))
); );
// general strategy when making a request (eg if online try to fetch it // general strategy when making a request (eg if online try to fetch it
// from the network with a timeout, if something fails serve from cache) // from cache, if something fails fetch from network. Update cache everytime files are fetched.
// This way files should only be fetched if cacheVersion is changed
self.addEventListener('fetch', function(event) { self.addEventListener('fetch', function(event) {
if (event.request.method === "POST") { if (event.request.method === "POST") {
// Requests related to Web Share Target. // Requests related to Web Share Target.
@@ -90,27 +91,33 @@ self.addEventListener('fetch', function(event) {
})()); })());
} else { } else {
// Regular requests not related to Web Share Target. // Regular requests not related to Web Share Target.
// FOR DEVELOPMENT: Comment in next line to always update assets instead of using cached versions
// event.respondWith(fromNetwork(event.request, 10000));return;
event.respondWith( event.respondWith(
fromNetwork(event.request, 10000).catch(() => fromCache(event.request)) fromCache(event.request).then(rsp => {
// if fromCache resolves to undefined fetch from network instead
return rsp || fromNetwork(event.request, 10000);
})
); );
event.waitUntil(update(event.request));
} }
}); });
// on activation, we clean up the previously registered service workers // on activation, we clean up the previously registered service workers
self.addEventListener('activate', evt => self.addEventListener('activate', evt => {
evt.waitUntil( return evt.waitUntil(
caches.keys().then(cacheNames => { caches.keys().then(cacheNames => {
return Promise.all( return Promise.all(
cacheNames.map(cacheName => { cacheNames.map(cacheName => {
if (cacheName !== cacheTitle) { if (cacheName !== cacheTitle) {
return caches.delete(cacheName); return caches.delete(cacheName);
} }
}) })
); );
}) })
) )
}
); );
const evaluateRequestData = function (request) { const evaluateRequestData = function (request) {