Merge pull request #107 from schlagmichdoch/add_auto_accept

Add auto accept functionality via Edit Paired Devices Dialog + implement pair secret regeneration functionality
This commit is contained in:
schlagmichdoch 2023-05-11 19:21:26 +02:00 committed by GitHub
commit de0afce4ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1643 additions and 510 deletions

View file

@ -78,9 +78,9 @@
<use xlink:href="#pair-device-icon" />
</svg>
</div>
<div id="clear-pair-devices" class="icon-button" title="Clear All Paired Devices" hidden>
<div id="edit-paired-devices" class="icon-button" title="Edit Paired Devices" hidden>
<svg class="icon">
<use xlink:href="#clear-pair-devices-icon" />
<use xlink:href="#edit-pair-devices-icon" />
</svg>
</div>
<div id="cancel-paste-mode" class="button" hidden>Done</div>
@ -142,16 +142,18 @@
</x-background>
</form>
</x-dialog>
<!-- Clear Devices Dialog -->
<x-dialog id="clear-devices-dialog">
<!-- Edit Paired Devices Dialog -->
<x-dialog id="edit-paired-devices-dialog">
<form action="#">
<x-background class="full center text-center">
<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>
<h2 class="center">Edit Paired Devices</h2>
<div class="paired-devices-wrapper"></div>
<div class="font-subheading center">
<p>Activate <u>auto-accept</u> to automatically accept all files sent from that device.</p>
</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" type="button" close>Cancel</button>
<button class="button" type="button" close>Close</button>
</div>
</x-paper>
</x-background>
@ -355,9 +357,11 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M579.8 267.7c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114L422.3 334.8c-31.5 31.5-82.5 31.5-114 0c-27.9-27.9-31.5-71.8-8.6-103.8l1.1-1.6c10.3-14.4 6.9-34.4-7.4-44.6s-34.4-6.9-44.6 7.4l-1.1 1.6C206.5 251.2 213 330 263 380c56.5 56.5 148 56.5 204.5 0L579.8 267.7zM60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5L217.7 177.2c31.5-31.5 82.5-31.5 114 0c27.9 27.9 31.5 71.8 8.6 103.9l-1.1 1.6c-10.3 14.4-6.9 34.4 7.4 44.6s34.4 6.9 44.6-7.4l1.1-1.6C433.5 260.8 427 182 377 132c-56.5-56.5-148-56.5-204.5 0L60.2 244.3z"/>
</symbol>
<symbol id="clear-pair-devices-icon" viewBox="0 0 640 512">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
<symbol id="edit-pair-devices-icon" viewBox="-159 25 640 512">
<!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<!--! edited by @schlagmichdoch -->
<path d="M218,155.4c-56.5-56.5-148-56.5-204.5,0L-98.8,267.7c-56.5,56.5-56.5,148,0,204.5c50,50,128.8,56.5,186.3,15.4l1.6-1.1 c14.4-10.3,17.7-30.3,7.4-44.6s-30.3-17.7-44.6-7.4l-1.6,1.1c-32.1,22.9-76,19.3-103.8-8.6c-31.5-31.6-31.5-82.6,0-114.1L58.7,200.6 c31.5-31.5,82.5-31.5,114,0c15.8,15.8,23.8,36.7,23.6,57.6c7.9-8.3,18.9-13,30.6-13c4.5,0,8.9,0.7,13.2,2l17.4,5.5 c0.9-0.5,1.8-1,2.7-1.5C258.7,216.2,244.4,181.8,218,155.4z M420.8,86.6c-50-50-128.8-56.5-186.3-15.4l-1.6,1.1 c-14.4,10.3-17.7,30.3-7.4,44.6s30.3,17.7,44.6,7.4l1.6-1.1c32.1-22.9,76-19.3,103.8,8.6c25.8,25.8,30.5,64.7,14,95.2 c0.7,2,1.3,4,1.8,6.1l3.9,17.9c1.1,0.6,2.1,1.2,3.2,1.8l17.4-5.5c4.3-1.4,8.7-2,13.1-2c7.3,0,14.3,1.8,20.5,5.2 C474.7,196.8,465.1,130.9,420.8,86.6z M140.7,254.4l1.1-1.6c10.3-14.4,6.9-34.4-7.4-44.6s-34.4-6.9-44.6,7.4l-1.1,1.6 C47.5,274.6,54,353.4,104,403.4c18.7,18.7,41.2,31.2,65,37.5c-1.4-3.1-2.6-6.2-3.8-9.3c-6-16.4-1.5-34.6,11.6-46.4l7.2-6.6 c-12.7-3.6-24.7-10.5-34.8-20.5C121.4,330.3,117.8,286.4,140.7,254.4z"/>
<path d="M458.9,407.4l-24.3-22.1c0.6-4.7,1-9.4,1-14.2s-0.3-9.6-1-14.2l24.3-22.1c3.9-3.5,5.4-8.9,3.6-13.8v-0.1 c-2.5-6.7-5.4-13.1-8.9-19.2l-2.6-4.5c-3.7-6.2-7.8-12-12.4-17.5c-3.3-4-8.8-5.4-13.7-3.8l-31.2,9.9c-7.5-5.8-15.8-10.6-24.7-14.2 l-7-32c-1.1-5.1-5-9.1-10.2-10c-7.7-1.3-15.7-2-23.8-2s-16.1,0.7-23.8,2c-5.2,0.9-9.1,4.9-10.2,10l-7,32 c-8.9,3.7-17.2,8.5-24.7,14.2l-31.2-9.9c-4.9-1.6-10.4-0.2-13.7,3.8c-4.5,5.5-8.7,11.3-12.4,17.5l-2.6,4.5 c-3.4,6.2-6.4,12.6-8.9,19.2c-1.8,4.9-0.3,10.3,3.6,13.8l24.3,22.1c-0.6,4.7-1,9.4-1,14.2s0.3,9.6,1,14.3L197,407.5 c-3.9,3.5-5.4,8.9-3.6,13.8c2.5,6.7,5.4,13.1,8.9,19.2l2.6,4.5c3.7,6.2,7.8,12,12.4,17.5c3.3,4,8.8,5.4,13.7,3.8l31.2-10 c7.5,5.8,15.8,10.6,24.7,14.2l7,32c1.1,5.1,5,9.1,10.2,10c7.7,1.3,15.7,2,23.8,2c8.1,0,16.1-0.7,23.8-2c5.2-0.8,9.1-4.9,10.2-10 l7-32c8.9-3.6,17.2-8.5,24.7-14.2l31.2,9.9c4.9,1.6,10.4,0.2,13.7-3.8c4.5-5.5,8.7-11.3,12.4-17.5l2.6-4.5 c3.4-6.2,6.4-12.6,8.9-19.2C464.2,416.3,462.7,410.9,458.9,407.4z M328,415.9c-24.8,0-44.9-20.1-44.9-44.8 c0-24.8,20.1-44.8,44.9-44.8s44.8,20.1,44.8,44.8C372.8,395.9,352.7,415.9,328,415.9z"/>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->

View file

@ -3,16 +3,25 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti
if (!window.isRtcSupported) alert("WebRTC must be enabled for PairDrop to work");
window.hiddenProperty = 'hidden' in document ? 'hidden' :
'webkitHidden' in document ? 'webkitHidden' :
'mozHidden' in document ? 'mozHidden' :
null;
window.visibilityChangeEvent = 'visibilitychange' in document ? 'visibilitychange' :
'webkitvisibilitychange' in document ? 'webkitvisibilitychange' :
'mozvisibilitychange' in document ? 'mozvisibilitychange' :
null;
class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
document.addEventListener(window.visibilityChangeEvent, _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
Events.on('room-secret-deleted', e => this.send({ type: 'room-secret-deleted', roomSecret: e.detail}));
Events.on('room-secrets-cleared', e => this.send({ type: 'room-secrets-cleared', roomSecrets: e.detail}));
Events.on('room-secrets-deleted', e => this.send({ type: 'room-secrets-deleted', roomSecrets: e.detail}));
Events.on('regenerate-room-secret', e => this.send({ type: 'regenerate-room-secret', roomSecret: e.detail}));
Events.on('resend-peers', _ => this.send({ type: 'resend-peers'}));
Events.on('pair-device-initiate', _ => this._onPairDeviceInitiate());
Events.on('pair-device-join', e => this._onPairDeviceJoin(e.detail));
@ -65,13 +74,13 @@ class ServerConnection {
_onMessage(msg) {
msg = JSON.parse(msg);
if (msg.type !== 'ping') console.log('WS:', msg);
if (msg.type !== 'ping') console.log('WS receive:', msg);
switch (msg.type) {
case 'rtc-config':
this._setRtcConfig(msg.config);
break;
case 'peers':
Events.fire('peers', msg);
this._onPeers(msg);
break;
case 'peer-joined':
Events.fire('peer-joined', msg);
@ -106,19 +115,42 @@ class ServerConnection {
case 'secret-room-deleted':
Events.fire('secret-room-deleted', msg.roomSecret);
break;
case 'room-secret-regenerated':
Events.fire('room-secret-regenerated', msg);
break;
default:
console.error('WS: unknown message type', msg);
console.error('WS receive: unknown message type', msg);
}
}
send(msg) {
if (!this._isConnected()) return;
if (msg.type !== 'pong') console.log("WS send:", msg)
this._socket.send(JSON.stringify(msg));
}
_onPeers(msg) {
Events.fire('peers', msg);
if (msg.roomType === "ip" && msg.peers.length === 0) {
BrowserTabsConnector.removePeerIdsFromLocalStorage();
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (peerId) return;
console.log("successfully added peerId from localStorage");
});
}
}
_onDisplayName(msg) {
// Add peerId and peerIdHash to sessionStorage to authenticate as the same device on page reload
sessionStorage.setItem("peerId", msg.message.peerId);
sessionStorage.setItem("peerIdHash", msg.message.peerIdHash);
// Add peerId to localStorage to mark it on other PairDrop tabs on the same browser
BrowserTabsConnector.addPeerIdToLocalStorage().then(peerId => {
if (peerId) return;
console.log("successfully added peerId from localStorage");
});
Events.fire('display-name', msg);
}
@ -138,13 +170,19 @@ class ServerConnection {
_disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
const peerId = sessionStorage.getItem("peerId");
BrowserTabsConnector.removePeerIdFromLocalStorage(peerId).then(_ => {
console.log("successfully removed peerId from localStorage");
});
if (!this._socket) return;
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
this._isReconnect = true;
}
_onDisconnect() {
@ -157,7 +195,7 @@ class ServerConnection {
}
_onVisibilityChange() {
if (document.hidden) return;
if (window.hiddenProperty) return;
this._connect();
}
@ -183,11 +221,16 @@ class Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
this._server = serverConnection;
this._isCaller = !!peerId;
this._peerId = peerId;
this._roomType = roomType;
this._roomSecret = roomSecret;
this._updateRoomSecret(roomSecret);
this._filesQueue = [];
this._busy = false;
// evaluate auto accept
this._evaluateAutoAccept();
}
sendJSON(message) {
@ -198,12 +241,48 @@ class Peer {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
mime: file.type,
size: file.size,
};
_updateRoomSecret(roomSecret) {
// if peer is another browser tab, peer is not identifiable with roomSecret as browser tabs share all roomSecrets
// -> abort
if (BrowserTabsConnector.peerIsSameBrowser(this._peerId)) {
this._roomSecret = "";
return;
}
if (this._roomSecret && this._roomSecret !== roomSecret) {
// remove old roomSecrets to prevent multiple pairings with same peer
PersistentStorage.deleteRoomSecret(this._roomSecret).then(deletedRoomSecret => {
if (deletedRoomSecret) console.log("Successfully deleted duplicate room secret with same peer: ", deletedRoomSecret);
})
}
this._roomSecret = roomSecret;
if (this._roomSecret && this._roomSecret.length !== 256 && this._isCaller) {
// increase security by increasing roomSecret length
console.log('RoomSecret is regenerated to increase security')
Events.fire('regenerate-room-secret', this._roomSecret);
}
}
_evaluateAutoAccept() {
if (!this._roomSecret) {
this._setAutoAccept(false);
return;
}
PersistentStorage.getRoomSecretEntry(this._roomSecret)
.then(roomSecretEntry => {
const autoAccept = roomSecretEntry ? roomSecretEntry.entry.auto_accept : false;
this._setAutoAccept(autoAccept);
})
.catch(_ => {
this._setAutoAccept(false);
});
}
_setAutoAccept(autoAccept) {
this._autoAccept = autoAccept;
}
getResizedImageDataUrl(file, width = undefined, height = undefined, quality = 0.7) {
@ -248,7 +327,11 @@ class Peer {
let imagesOnly = true
for (let i=0; i<files.length; i++) {
Events.fire('set-progress', {peerId: this._peerId, progress: 0.8*i/files.length, status: 'prepare'})
header.push(await this.createHeader(files[i]));
header.push({
name: files[i].name,
mime: files[i].type,
size: files[i].size
});
totalSize += files[i].size;
if (files[i].type.split('/')[0] !== 'image') imagesOnly = false;
}
@ -360,7 +443,7 @@ class Peer {
_onFilesTransferRequest(request) {
if (this._requestPending) {
// Only accept one request at a time
// Only accept one request at a time per peer
this.sendJSON({type: 'files-transfer-response', accepted: false});
return;
}
@ -372,6 +455,14 @@ class Peer {
}
this._requestPending = request;
if (this._autoAccept) {
// auto accept if set via Edit Paired Devices Dialog
this._respondToFileTransferRequest(true);
return;
}
// default behavior: show user transfer request
Events.fire('files-transfer-request', {
request: request,
peerId: this._peerId
@ -497,9 +588,16 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
const displayNameHasChanged = this._displayName !== message.displayName
if (message.displayName && displayNameHasChanged) {
this._displayName = message.displayName;
}
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
if (!displayNameHasChanged) return;
Events.fire('notify-peer-display-name-changed', this._peerId);
}
}
@ -508,22 +606,21 @@ 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
if (!this._isCaller) return; // we will listen for a caller
this._connect(peerId, true);
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
_connect(peerId) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId);
if (isCaller) {
if (this._isCaller) {
this._openChannel();
} else {
this._conn.ondatachannel = e => this._onChannelOpened(e);
}
}
_openConnection(peerId, isCaller) {
this._isCaller = isCaller;
_openConnection(peerId) {
this._peerId = peerId;
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
@ -688,7 +785,11 @@ class RTCPeer extends Peer {
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
this._connect(this._peerId, this._isCaller);
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect(this._peerId);
}
_isConnected() {
@ -716,32 +817,60 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('room-secret-regenerated', e => this._onRoomSecretRegenerated(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
Events.on('notify-peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail));
Events.on('auto-accept-updated', e => this._onAutoAcceptUpdated(e.detail.roomSecret, e.detail.autoAccept));
}
_onMessage(message) {
// if different roomType -> abort
if (this.peers[message.sender.id] && this.peers[message.sender.id]._roomType !== message.roomType) return;
if (!this.peers[message.sender.id]) {
this.peers[message.sender.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
}
this.peers[message.sender.id].onServerMessage(message);
const peerId = message.sender.id;
this.peers[peerId].onServerMessage(message);
}
_onPeers(msg) {
msg.peers.forEach(peer => {
if (this.peers[peer.id]) {
// if different roomType -> abort
if (this.peers[peer.id].roomType !== msg.roomType || this.peers[peer.id].roomSecret !== msg.roomSecret) return;
this.peers[peer.id].refresh();
return;
_refreshExistingPeer(peerId, roomType, roomSecret) {
const peer = this.peers[peerId];
if (peer) {
const roomTypeIsSecret = roomType === "secret";
const roomSecretsDiffer = peer._roomSecret !== roomSecret;
// if roomSecrets differs peer is already connected -> abort but update roomSecret and reevaluate auto accept
if (roomTypeIsSecret && roomSecretsDiffer) {
peer._updateRoomSecret(roomSecret);
peer._evaluateAutoAccept();
return true;
}
this.peers[peer.id] = new RTCPeer(this._server, peer.id, msg.roomType, msg.roomSecret);
const roomTypesDiffer = peer._roomType !== roomType;
// if roomTypes differ peer is already connected -> abort
if (roomTypesDiffer) return true;
peer.refresh();
return true;
}
// peer does not yet exist: return false
return false;
}
_onPeerJoined(message) {
if (this._refreshExistingPeer(message.peer.id, message.roomType, message.roomSecret)) return;
this.peers[message.peer.id] = new RTCPeer(this._server, undefined, message.roomType, message.roomSecret);
}
_onPeers(message) {
message.peers.forEach(messagePeer => {
if (this._refreshExistingPeer(messagePeer.id, message.roomType, message.roomSecret)) return;
this.peers[messagePeer.id] = new RTCPeer(this._server, messagePeer.id, message.roomType, message.roomSecret);
})
}
@ -769,10 +898,10 @@ class PeersManager {
this.peers[message.to].sendText(message.text);
}
_onPeerLeft(msg) {
if (msg.disconnect === true) {
// if user actively disconnected from PairDrop disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', msg.peerId);
_onPeerLeft(message) {
if (message.disconnect === true) {
// if user actively disconnected from PairDrop server, disconnect all peer to peer connections immediately
Events.fire('peer-disconnected', message.peerId);
}
}
@ -792,12 +921,19 @@ class PeersManager {
_onSecretRoomDeleted(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
if (peer._roomType === 'secret' && peer._roomSecret === roomSecret) {
this._onPeerDisconnected(peerId);
}
}
}
_onRoomSecretRegenerated(message) {
PersistentStorage.updateRoomSecret(message.oldRoomSecret, message.newRoomSecret).then(_ => {
console.log("successfully regenerated room secret");
Events.fire("room-secrets", [message.newRoomSecret]);
})
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
@ -813,6 +949,24 @@ class PeersManager {
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
// if the displayName has not been changed (yet) set the displayName to the original displayName
if (!this._displayName) this._displayName = displayName;
}
_onAutoAcceptUpdated(roomSecret, autoAccept) {
const peerId = this._getPeerIdFromRoomSecret(roomSecret);
if (!peerId) return;
this.peers[peerId]._setAutoAccept(autoAccept);
}
_getPeerIdFromRoomSecret(roomSecret) {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer._roomSecret === roomSecret) {
return peer._peerId;
}
}
return false;
}
}

View file

@ -50,7 +50,7 @@ class PeersUI {
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
// Load saved display name on page load
this._getSavedDisplayName().then(displayName => {
@ -87,26 +87,31 @@ class PeersUI {
if (newDisplayName === savedDisplayName) return;
if (newDisplayName) {
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.');
}).catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', 'Device name is changed only for this session.');
}).finally(_ => {
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
});
PersistentStorage.set('editedDisplayName', newDisplayName)
.then(_ => {
Events.fire('notify-user', 'Device name is changed permanently.');
})
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.");
localStorage.setItem('editedDisplayName', newDisplayName);
Events.fire('notify-user', 'Device name is changed only for this session.');
})
.finally(_ => {
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
});
} else {
PersistentStorage.delete('editedDisplayName').catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
}).finally(_ => {
Events.fire('notify-user', 'Device name is randomly generated again.');
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
PersistentStorage.delete('editedDisplayName')
.catch(_ => {
console.log("This browser does not support IndexedDB. Use localStorage instead.")
localStorage.removeItem('editedDisplayName');
Events.fire('notify-user', 'Random Display name is used again.');
})
.finally(_ => {
Events.fire('notify-user', 'Device name is randomly generated again.');
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
}
}
@ -129,6 +134,12 @@ class PeersUI {
this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
this._redrawPeerRoomTypes(peerId);
}
_onPeerDisplayNameChanged(e) {
if (!e.detail.displayName) return;
this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);
}
_onKeyDown(e) {
@ -142,26 +153,43 @@ class PeersUI {
}
_joinPeer(peer, roomType, roomSecret) {
const existingPeer = this.peers[peer.id];
if (existingPeer) {
// peer already exists. Abort but add roomType to GUI and update roomSecret
// skip if peer is a tab on the same browser
if (!existingPeer.sameBrowser()) {
// add roomType to PeerUI
if (!existingPeer.roomTypes.includes(roomType)) {
existingPeer.roomTypes.push(roomType);
}
this._redrawPeerRoomTypes(peer.id);
if (roomType === "secret") existingPeer.roomSecret = roomSecret;
}
return;
}
peer.sameBrowser = _ => BrowserTabsConnector.peerIsSameBrowser(peer.id);
peer.roomTypes = [roomType];
peer.roomSecret = roomSecret;
if (this.peers[peer.id]) {
if (!this.peers[peer.id].roomTypes.includes(roomType)) this.peers[peer.id].roomTypes.push(roomType);
this._redrawPeer(this.peers[peer.id]);
return; // peer already exists
}
this.peers[peer.id] = peer;
}
_onPeerConnected(peerId, connectionHash) {
if(this.peers[peerId] && !$(peerId))
new PeerUI(this.peers[peerId], connectionHash);
if (!this.peers[peerId] || $(peerId)) return;
const peer = this.peers[peerId];
new PeerUI(peer, connectionHash);
}
_redrawPeer(peer) {
const peerNode = $(peer.id);
_redrawPeerRoomTypes(peerId) {
const peer = this.peers[peerId]
const peerNode = $(peerId);
if (!peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret');
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
if (!peer.sameBrowser()) {
peer.roomTypes.forEach(roomType => peerNode.classList.add(`type-${roomType}`));
}
}
evaluateOverflowing() {
@ -187,7 +215,17 @@ class PeersUI {
for (const peerId in this.peers) {
const peer = this.peers[peerId];
if (peer.roomSecret === roomSecret) {
let index = peer.roomTypes.indexOf('secret');
peer.roomTypes.splice(index, 1);
peer.roomSecret = "";
if (peer.roomTypes.length) {
this._redrawPeerRoomTypes(peerId)
return;
}
this._onPeerDisconnected(peerId);
return;
}
}
}
@ -364,8 +402,8 @@ class PeerUI {
this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id;
this.$el.ui = this;
this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.$el.classList.add('center');
if (!this._peer.sameBrowser()) this._peer.roomTypes.forEach(roomType => this.$el.classList.add(`type-${roomType}`));
this.html();
this._callbackInput = e => this._onFilesSelected(e)
@ -535,6 +573,10 @@ class Dialog {
if (this.$autoFocus) this.$autoFocus.focus();
}
isShown() {
return !!this.$el.attributes["show"];
}
hide() {
this.$el.removeAttribute('show');
if (this.$autoFocus) {
@ -543,10 +585,11 @@ class Dialog {
}
document.title = 'PairDrop';
document.changeFavicon("images/favicon-96x96.png");
this.correspondingPeerId = undefined;
}
_onPeerDisconnected(peerId) {
if (this.correspondingPeerId === peerId) {
if (this.isShown() && this.correspondingPeerId === peerId) {
this.hide();
Events.fire('notify-user', 'Selected peer left.')
}
@ -812,14 +855,14 @@ class ReceiveRequestDialog extends ReceiveDialog {
}
_onKeyDown(e) {
if (this.$el.attributes["show"] && e.code === "Escape") {
if (this.isShown() && e.code === "Escape") {
this._respondToFileTransferRequest(false);
}
}
_onRequestFileTransfer(request, peerId) {
this._filesTransferRequestQueue.push({request: request, peerId: peerId});
if (this.$el.attributes["show"]) return;
if (this.isShown()) return;
this._dequeueRequests();
}
@ -862,8 +905,12 @@ class ReceiveRequestDialog extends ReceiveDialog {
}
hide() {
this.$previewBox.innerHTML = '';
// clear previewBox after dialog is closed
setTimeout(_ => this.$previewBox.innerHTML = '', 300);
super.hide();
// show next request
setTimeout(_ => this._dequeueRequests(), 500);
}
}
@ -876,7 +923,7 @@ class PairDeviceDialog extends Dialog {
this.$roomKey = this.$el.querySelector('#room-key');
this.$qrCode = this.$el.querySelector('#room-key-qr-code');
this.$pairDeviceBtn = $('pair-device');
this.$clearSecretsBtn = $('clear-pair-devices');
this.$editPairedDevicesBtn = $('edit-paired-devices');
this.$footerInstructionsPairedDevices = $('and-by-paired-devices');
this.$createJoinForm = this.$el.querySelector('form');
@ -894,14 +941,18 @@ class PairDeviceDialog extends Dialog {
Events.on('ws-disconnected', _ => this.hide());
Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail));
Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail.peerId, e.detail.roomSecret));
Events.on('peers', e => this._onPeers(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid());
Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail));
Events.on('clear-room-secrets', e => this._onClearRoomSecrets(e.detail))
Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets())
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
this.$el.addEventListener('paste', e => this._onPaste(e));
this.evaluateRoomKeyChars();
this.evaluateUrlAttributes();
this.pairPeer = {};
}
_onCharsInput(e) {
@ -917,7 +968,7 @@ class PairDeviceDialog extends Dialog {
}
_onKeyDown(e) {
if (this.$el.attributes["show"] && e.code === "Escape") {
if (this.isShown() && e.code === "Escape") {
// Timeout to prevent paste mode from getting cancelled simultaneously
setTimeout(_ => this._pairDeviceCancel(), 50);
}
@ -978,7 +1029,7 @@ class PairDeviceDialog extends Dialog {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets', roomSecrets);
this._evaluateNumberRoomSecrets();
}).catch(_ => PersistentStorage.logBrowserNotCapable());
});
}
_pairDeviceInitiate() {
@ -1026,22 +1077,69 @@ class PairDeviceDialog extends Dialog {
}
_pairDeviceJoined(peerId, roomSecret) {
this.hide();
PersistentStorage.addRoomSecret(roomSecret).then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
const oldRoomSecret = $(peerId).ui.roomSecret;
if (oldRoomSecret) PersistentStorage.deleteRoomSecret(oldRoomSecret);
$(peerId).ui.roomSecret = roomSecret;
this._evaluateNumberRoomSecrets();
}).finally(_ => {
// skip adding to IndexedDB if peer is another tab on the same browser
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
this._cleanUp();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
PersistentStorage.logBrowserNotCapable();
this.hide();
Events.fire('notify-user', 'Pairing of two browser tabs is not possible.');
return;
}
// save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret
this.pairPeer = {
"peerId": peerId,
"roomSecret": roomSecret
};
}
_onPeers(message) {
if (!Object.keys(this.pairPeer)) return;
message.peers.forEach(messagePeer => {
this._evaluateJoinedPeer(messagePeer.id, message.roomSecret);
});
}
_onPeerJoined(message) {
if (!Object.keys(this.pairPeer)) return;
this._evaluateJoinedPeer(message.peer.id, message.roomSecret);
}
_evaluateJoinedPeer(peerId, roomSecret) {
const samePeerId = peerId === this.pairPeer.peerId;
const sameRoomSecret = roomSecret === this.pairPeer.roomSecret;
if (!peerId || !roomSecret || !samePeerId || !sameRoomSecret) return;
this._onPairPeerJoined(peerId, roomSecret);
this.pairPeer = {};
}
_onPairPeerJoined(peerId, roomSecret) {
// if devices are paired that are already connected we must save the names at this point
const $peer = $(peerId);
let displayName, deviceName;
if ($peer) {
displayName = $peer.ui._peer.name.displayName;
deviceName = $peer.ui._peer.name.deviceName;
}
PersistentStorage.addRoomSecret(roomSecret, displayName, deviceName)
.then(_ => {
Events.fire('notify-user', 'Devices paired successfully.');
this._evaluateNumberRoomSecrets();
})
.finally(_ => {
this._cleanUp();
this.hide();
})
.catch(_ => {
Events.fire('notify-user', 'Paired devices are not persistent.');
PersistentStorage.logBrowserNotCapable();
});
}
_pairDeviceJoinKeyInvalid() {
Events.fire('notify-user', 'Key not valid');
}
@ -1062,58 +1160,123 @@ class PairDeviceDialog extends Dialog {
this.inputRoomKey = '';
this.$inputRoomKeyChars.forEach(el => el.value = '');
this.$inputRoomKeyChars.forEach(el => el.setAttribute("disabled", ""));
}
_onClearRoomSecrets() {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
Events.fire('room-secrets-cleared', roomSecrets);
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('notify-user', 'All Devices unpaired.')
this._evaluateNumberRoomSecrets();
})
}).catch(_ => PersistentStorage.logBrowserNotCapable());
this.pairPeer = {};
}
_onSecretRoomDeleted(roomSecret) {
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
this._evaluateNumberRoomSecrets();
}).catch(e => console.error(e));
});
}
_evaluateNumberRoomSecrets() {
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
if (roomSecrets.length > 0) {
this.$clearSecretsBtn.removeAttribute('hidden');
this.$editPairedDevicesBtn.removeAttribute('hidden');
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
} else {
this.$clearSecretsBtn.setAttribute('hidden', '');
this.$editPairedDevicesBtn.setAttribute('hidden', '');
this.$footerInstructionsPairedDevices.setAttribute('hidden', '');
}
Events.fire('bg-resize');
}).catch(_ => PersistentStorage.logBrowserNotCapable());
});
}
}
class ClearDevicesDialog extends Dialog {
class EditPairedDevicesDialog extends Dialog {
constructor() {
super('clear-devices-dialog');
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
let clearDevicesForm = this.$el.querySelector('form');
clearDevicesForm.addEventListener('submit', e => this._onSubmit(e));
super('edit-paired-devices-dialog');
this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');
$('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices());
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
Events.on('keydown', e => this._onKeyDown(e));
}
_onClearPairDevices() {
this.show();
_onKeyDown(e) {
if (this.isShown() && e.code === "Escape") {
this.hide();
}
}
_onSubmit(e) {
e.preventDefault();
this._clearRoomSecrets();
async _initDOM() {
const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries();
roomSecretsEntries.forEach(roomSecretsEntry => {
let $pairedDevice = document.createElement('div');
$pairedDevice.classList = ["paired-device"];
$pairedDevice.innerHTML = `
<div class="display-name">
<span>${roomSecretsEntry.display_name}</span>
</div>
<div class="device-name">
<span>${roomSecretsEntry.device_name}</span>
</div>
<div class="button-wrapper">
<label class="auto-accept">auto-accept
<input type="checkbox" ${roomSecretsEntry.auto_accept ? "checked" : ""}>
</label>
<button class="button" type="button">unpair</button>
</div>`
$pairedDevice.querySelector('input[type="checkbox"]').addEventListener('click', e => {
PersistentStorage.updateRoomSecretAutoAccept(roomSecretsEntry.secret, e.target.checked).then(roomSecretsEntry => {
Events.fire('auto-accept-updated', {
'roomSecret': roomSecretsEntry.entry.secret,
'autoAccept': e.target.checked
});
});
});
$pairedDevice.querySelector('button').addEventListener('click', e => {
PersistentStorage.deleteRoomSecret(roomSecretsEntry.secret).then(roomSecret => {
Events.fire('room-secrets-deleted', [roomSecret]);
Events.fire('evaluate-number-room-secrets');
e.target.parentNode.parentNode.remove();
});
})
this.$pairedDevicesWrapper.appendChild($pairedDevice)
})
}
hide() {
super.hide();
setTimeout(_ => {
this.$pairedDevicesWrapper.innerHTML = ""
}, 300);
}
_onEditPairedDevices() {
this._initDOM().then(_ => this.show());
}
_clearRoomSecrets() {
Events.fire('clear-room-secrets');
this.hide();
PersistentStorage.getAllRoomSecrets()
.then(roomSecrets => {
PersistentStorage.clearRoomSecrets().finally(_ => {
Events.fire('room-secrets-deleted', roomSecrets);
Events.fire('evaluate-number-room-secrets');
Events.fire('notify-user', 'All Devices unpaired.');
this.hide();
})
});
}
_onPeerDisplayNameChanged(e) {
const peerId = e.detail.peerId;
const peerNode = $(peerId);
if (!peerNode) return;
const peer = peerNode.ui._peer;
if (!peer.roomSecret) return;
PersistentStorage.updateRoomSecretNames(peer.roomSecret, peer.name.displayName, peer.name.deviceName).then(roomSecretEntry => {
console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`);
})
}
}
@ -1131,7 +1294,7 @@ class SendTextDialog extends Dialog {
}
async _onKeyDown(e) {
if (this.$el.attributes["show"]) {
if (this.isShown()) {
if (e.code === "Escape") {
this.hide();
} else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
@ -1200,7 +1363,7 @@ class ReceiveTextDialog extends Dialog {
}
async _onKeyDown(e) {
if (this.$el.attributes["show"]) {
if (this.isShown()) {
if (e.code === "KeyC" && (e.ctrlKey || e.metaKey)) {
await this._onCopy()
this.hide();
@ -1214,7 +1377,7 @@ class ReceiveTextDialog extends Dialog {
window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
if (this.$el.attributes["show"]) return;
if (this.isShown()) return;
this._dequeueRequests();
}
@ -1682,7 +1845,7 @@ class PersistentStorage {
PersistentStorage.logBrowserNotCapable();
return;
}
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 3);
const DBOpenRequest = window.indexedDB.open('pairdrop_store', 4);
DBOpenRequest.onerror = (e) => {
PersistentStorage.logBrowserNotCapable();
console.log('Error initializing database: ');
@ -1693,27 +1856,32 @@ class PersistentStorage {
};
DBOpenRequest.onupgradeneeded = (e) => {
const db = e.target.result;
const txn = e.target.transaction;
db.onerror = e => console.log('Error loading database: ' + e);
try {
console.log(`Upgrading IndexedDB database from version ${e.oldVersion} to version ${e.newVersion}`);
if (e.oldVersion === 0) {
// initiate v1
db.createObjectStore('keyval');
} catch (error) {
console.log("Object store named 'keyval' already exists")
let roomSecretsObjectStore1 = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore1.createIndex('secret', 'secret', { unique: true });
}
try {
const roomSecretsObjectStore = db.createObjectStore('room_secrets', {autoIncrement: true});
roomSecretsObjectStore.createIndex('secret', 'secret', { unique: true });
} catch (error) {
console.log("Object store named 'room_secrets' already exists")
if (e.oldVersion <= 1) {
// migrate to v2
db.createObjectStore('share_target_files');
}
try {
if (db.objectStoreNames.contains('share_target_files')) {
db.deleteObjectStore('share_target_files');
}
if (e.oldVersion <= 2) {
// migrate to v3
db.deleteObjectStore('share_target_files');
db.createObjectStore('share_target_files', {autoIncrement: true});
} catch (error) {
console.log("Object store named 'share_target_files' already exists")
}
if (e.oldVersion <= 3) {
// migrate to v4
let roomSecretsObjectStore4 = txn.objectStore('room_secrets');
roomSecretsObjectStore4.createIndex('display_name', 'display_name');
roomSecretsObjectStore4.createIndex('auto_accept', 'auto_accept');
}
}
}
@ -1746,7 +1914,7 @@ class PersistentStorage {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('keyval', 'readwrite');
const transaction = db.transaction('keyval', 'readonly');
const objectStore = transaction.objectStore('keyval');
const objectStoreRequest = objectStore.get(key);
objectStoreRequest.onsuccess = _ => {
@ -1779,16 +1947,21 @@ class PersistentStorage {
})
}
static addRoomSecret(roomSecret) {
static addRoomSecret(roomSecret, displayName, deviceName) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.add({'secret': roomSecret});
objectStoreRequest.onsuccess = _ => {
console.log(`Request successful. RoomSecret added: ${roomSecret}`);
const objectStoreRequest = objectStore.add({
'secret': roomSecret,
'display_name': displayName,
'device_name': deviceName,
'auto_accept': false
});
objectStoreRequest.onsuccess = e => {
console.log(`Request successful. RoomSecret added: ${e.target.result}`);
resolve();
}
}
@ -1798,21 +1971,26 @@ class PersistentStorage {
})
}
static getAllRoomSecrets() {
static async getAllRoomSecrets() {
const roomSecrets = await this.getAllRoomSecretEntries();
let secrets = [];
for (let i=0; i<roomSecrets.length; i++) {
secrets.push(roomSecrets[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
return(secrets);
}
static getAllRoomSecretEntries() {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequest = objectStore.getAll();
objectStoreRequest.onsuccess = e => {
let secrets = [];
for (let i=0; i<e.target.result.length; i++) {
secrets.push(e.target.result[i].secret);
}
console.log(`Request successful. Retrieved ${secrets.length} room_secrets`);
resolve(secrets);
resolve(e.target.result);
}
}
DBOpenRequest.onerror = (e) => {
@ -1821,24 +1999,59 @@ class PersistentStorage {
});
}
static deleteRoomSecret(room_secret) {
static getRoomSecretEntry(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readonly');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
const key = e.target.result;
if (!key) {
console.log(`Nothing to retrieve. Entry for room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestRetrieval = objectStore.get(key);
objectStoreRequestRetrieval.onsuccess = e => {
console.log(`Request successful. Retrieved entry for room_secret: ${key}`);
resolve({
"entry": e.target.result,
"key": key
});
}
objectStoreRequestRetrieval.onerror = (e) => {
reject(e);
}
};
}
DBOpenRequest.onerror = (e) => {
reject(e);
}
});
}
static deleteRoomSecret(roomSecret) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
const objectStoreRequestKey = objectStore.index("secret").getKey(room_secret);
const objectStoreRequestKey = objectStore.index("secret").getKey(roomSecret);
objectStoreRequestKey.onsuccess = e => {
if (!e.target.result) {
console.log(`Nothing to delete. room_secret not existing: ${room_secret}`);
console.log(`Nothing to delete. room_secret not existing: ${roomSecret}`);
resolve();
return;
}
const objectStoreRequestDeletion = objectStore.delete(e.target.result);
const key = e.target.result;
const objectStoreRequestDeletion = objectStore.delete(key);
objectStoreRequestDeletion.onsuccess = _ => {
console.log(`Request successful. Deleted room_secret: ${room_secret}`);
resolve();
console.log(`Request successful. Deleted room_secret: ${key}`);
resolve(roomSecret);
}
objectStoreRequestDeletion.onerror = (e) => {
reject(e);
@ -1869,22 +2082,108 @@ class PersistentStorage {
}
})
}
static updateRoomSecretNames(roomSecret, displayName, deviceName) {
return this.updateRoomSecret(roomSecret, undefined, displayName, deviceName);
}
static updateRoomSecretAutoAccept(roomSecret, autoAccept) {
return this.updateRoomSecret(roomSecret, undefined, undefined, undefined, autoAccept);
}
static updateRoomSecret(roomSecret, updatedRoomSecret = undefined, updatedDisplayName = undefined, updatedDeviceName = undefined, updatedAutoAccept = undefined) {
return new Promise((resolve, reject) => {
const DBOpenRequest = window.indexedDB.open('pairdrop_store');
DBOpenRequest.onsuccess = (e) => {
const db = e.target.result;
this.getRoomSecretEntry(roomSecret)
.then(roomSecretEntry => {
if (!roomSecretEntry) {
resolve(false);
return;
}
const transaction = db.transaction('room_secrets', 'readwrite');
const objectStore = transaction.objectStore('room_secrets');
// Do not use `updatedRoomSecret ?? roomSecretEntry.entry.secret` to ensure compatibility with older browsers
const updatedRoomSecretEntry = {
'secret': updatedRoomSecret !== undefined ? updatedRoomSecret : roomSecretEntry.entry.secret,
'display_name': updatedDisplayName !== undefined ? updatedDisplayName : roomSecretEntry.entry.display_name,
'device_name': updatedDeviceName !== undefined ? updatedDeviceName : roomSecretEntry.entry.device_name,
'auto_accept': updatedAutoAccept !== undefined ? updatedAutoAccept : roomSecretEntry.entry.auto_accept
};
const objectStoreRequestUpdate = objectStore.put(updatedRoomSecretEntry, roomSecretEntry.key);
objectStoreRequestUpdate.onsuccess = e => {
console.log(`Request successful. Updated room_secret: ${roomSecretEntry.key}`);
resolve({
"entry": updatedRoomSecretEntry,
"key": roomSecretEntry.key
});
}
objectStoreRequestUpdate.onerror = (e) => {
reject(e);
}
})
.catch(e => reject(e));
};
DBOpenRequest.onerror = e => reject(e);
})
}
}
class Broadcast {
class BrowserTabsConnector {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
Events.on('broadcast-send', e => this._broadcastSend(e.detail));
}
_broadcastMessage(message) {
_broadcastSend(message) {
this.bc.postMessage(message);
}
_onMessage(e) {
console.log('Broadcast message received:', e.data)
Events.fire(e.data.type, e.data.detail);
console.log('Broadcast:', e.data)
switch (e.data.type) {
case 'self-display-name-changed':
Events.fire('self-display-name-changed', e.data.detail);
break;
}
}
static peerIsSameBrowser(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
return peerIdsBrowser
? peerIdsBrowser.indexOf(peerId) !== -1
: false;
}
static async addPeerIdToLocalStorage() {
const peerId = sessionStorage.getItem("peerId");
if (!peerId) return false;
let peerIdsBrowser = [];
let peerIdsBrowserOld = JSON.parse(localStorage.getItem("peerIdsBrowser"));
if (peerIdsBrowserOld) peerIdsBrowser.push(...peerIdsBrowserOld);
peerIdsBrowser.push(peerId);
peerIdsBrowser = peerIdsBrowser.filter(onlyUnique);
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerId;
}
static async removePeerIdFromLocalStorage(peerId) {
let peerIdsBrowser = JSON.parse(localStorage.getItem("peerIdsBrowser"));
const index = peerIdsBrowser.indexOf(peerId);
peerIdsBrowser.splice(index, 1);
localStorage.setItem("peerIdsBrowser", JSON.stringify(peerIdsBrowser));
return peerId;
}
static removePeerIdsFromLocalStorage() {
localStorage.removeItem("peerIdsBrowser");
}
}
@ -1899,7 +2198,7 @@ class PairDrop {
const sendTextDialog = new SendTextDialog();
const receiveTextDialog = new ReceiveTextDialog();
const pairDeviceDialog = new PairDeviceDialog();
const clearDevicesDialog = new ClearDevicesDialog();
const clearDevicesDialog = new EditPairedDevicesDialog();
const base64ZipDialog = new Base64ZipDialog();
const toast = new Toast();
const notifications = new Notifications();
@ -1907,7 +2206,7 @@ class PairDrop {
const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI();
const broadCast = new Broadcast();
const broadCast = new BrowserTabsConnector();
});
}
}

View file

@ -398,3 +398,7 @@ const cyrb53 = function(str, seed = 0) {
h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1>>>0);
};
function onlyUnique (value, index, array) {
return array.indexOf(value) === index;
}

View file

@ -19,6 +19,10 @@ body {
overflow-x: hidden;
overscroll-behavior: none;
overflow-y: hidden;
/* Only allow selection on message and pair key */
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
body {
@ -215,6 +219,14 @@ hr {
color: white;
}
input {
cursor: pointer;
}
input[type="checkbox"] {
min-width: 13px;
}
x-noscript {
background: var(--primary-color);
color: white;
@ -273,7 +285,7 @@ x-noscript {
}
@media screen and (max-width: 425px) {
header:has(#clear-pair-devices:not([hidden]))~#center {
header:has(#edit-pair-devices:not([hidden]))~#center {
--footer-height: 150px;
}
}
@ -442,8 +454,6 @@ x-no-peers[drop-bg] * {
/* Peer */
x-peer {
-webkit-user-select: none;
user-select: none;
padding: 8px;
align-content: start;
flex-wrap: wrap;
@ -530,7 +540,6 @@ x-peer[status] x-icon {
.status,
.device-name,
.connection-hash {
height: 18px;
opacity: 0.7;
}
@ -691,6 +700,12 @@ x-dialog x-paper {
height: 625px;
}
#pair-device-dialog ::-moz-selection,
#pair-device-dialog ::selection {
color: black;
background: var(--paired-device-color);
}
x-dialog:not([show]) {
pointer-events: none;
}
@ -712,7 +727,7 @@ x-dialog .font-subheading {
margin-bottom: 5px;
}
/* PairDevicesDialog */
/* Pair Devices Dialog */
#key-input-container {
width: 100%;
@ -720,7 +735,7 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input {
#key-input-container > input {
width: 45px;
height: 45px;
font-size: 30px;
@ -736,15 +751,18 @@ x-dialog .font-subheading {
justify-content: center;
}
#key-input-container>input + * {
#key-input-container > input + * {
margin-left: 6px;
}
#key-input-container>input:nth-of-type(4) {
#key-input-container > input:nth-of-type(4) {
margin-left: 5%;
}
#room-key {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
font-size: 50px;
letter-spacing: min(calc((100vw - 80px - 99px) / 100 * 7), 23px);
display: inline-block;
@ -756,14 +774,106 @@ x-dialog .font-subheading {
margin: 16px;
}
#pair-device-dialog hr {
margin: 40px -24px;
x-dialog hr {
margin: 40px -24px 30px -24px;
border: solid 1.25px var(--border-color);
}
#pair-device-dialog x-background {
padding: 16px!important;
}
/* Edit Paired Devices Dialog */
.paired-devices-wrapper:empty:before {
content: "No paired devices.";
}
.paired-devices-wrapper:empty {
padding: 10px;
}
.paired-devices-wrapper {
border-top: solid 4px var(--paired-device-color);
border-bottom: solid 4px var(--paired-device-color);
max-height: 65vh;
overflow: scroll;
background: /* Shadow covers */ linear-gradient(rgb(var(--bg-color)) 30%, rgba(var(--bg-color), 0)),
linear-gradient(rgba(var(--bg-color), 0), rgb(var(--bg-color)) 70%) 0 100%,
/* Shadows */ radial-gradient(farthest-side at 50% 0, rgba(var(--text-color), .3), rgba(var(--text-color), 0)),
radial-gradient(farthest-side at 50% 100%, rgba(var(--text-color), .3), rgba(var(--text-color), 0)) 0 100%;
background-repeat: no-repeat;
background-size: 100% 80px, 100% 80px, 100% 24px, 100% 24px;
/* Opera doesn't support this in the shorthand */
background-attachment: local, local, scroll, scroll;
}
.paired-device {
display: flex;
justify-content: space-between;
flex-direction: column;
align-items: center;
}
.paired-device:not(:last-child) {
border-bottom: solid 4px var(--paired-device-color);
}
.paired-device > .display-name,
.paired-device > .device-name {
width: 100%;
height: 36px;
display: flex;
align-items: center;
text-align: center;
align-self: center;
border-bottom: solid 2px rgba(128, 128, 128, 0.5);
opacity: 1;
}
.paired-device span {
width: 100%;
}
.paired-device > .button-wrapper {
display: flex;
height: 36px;
justify-content: space-between;
flex-direction: row;
align-items: center;
width: 100%;
}
.paired-device > .button-wrapper > label,
.paired-device > .button-wrapper > button {
display: flex;
align-items: center;
text-align: center;
white-space: nowrap;
justify-content: center;
width: 50%;
padding-left: 6px;
padding-right: 6px;
height: 36px;
}
.paired-device > .button-wrapper > :not(:last-child) {
border-right: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device > .button-wrapper > :not(:first-child) {
border-left: solid 1px rgba(128, 128, 128, 0.5);
}
.paired-device * {
overflow: hidden;
text-overflow: ellipsis;
}
.paired-device > .auto-accept {
cursor: pointer;
}
/* Receive Dialog */
x-dialog .row {
@ -812,7 +922,7 @@ x-paper > div:last-child > .button:not(:last-child) {
}
/* Send Text Dialog */
/* Todo: add pair underline to send / receive dialogs displayName */
x-dialog .dialog-subheader {
margin-bottom: 25px;
}
@ -830,9 +940,9 @@ x-dialog .dialog-subheader {
max-height: calc(100vh - 393px);
overflow-x: hidden;
overflow-y: auto;
-webkit-user-select: all;
-moz-user-select: all;
user-select: all;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
white-space: pre-wrap;
padding: 15px 0;
}
@ -1034,8 +1144,6 @@ button::-moz-focus-inner {
#about x-background {
position: absolute;
top: calc(28px - 250px);
right: calc(36px - 250px);
width: 500px;
height: 500px;
border-radius: 50%;
@ -1178,23 +1286,6 @@ x-peers:empty~x-instructions {
}
}
#websocket-fallback {
margin-left: 5px;
margin-right: 5px;
padding: 5px;
text-align: center;
opacity: 0.5;
transition: opacity 300ms;
}
#websocket-fallback>span {
margin: 2px;
}
#websocket-fallback > span > span {
border-bottom: solid 4px var(--ws-peer-color);
}
/* Responsive Styles */
@media screen and (max-width: 360px) {
x-dialog x-paper {
@ -1337,3 +1428,9 @@ x-dialog x-paper {
background: #bfbfbf;
border-radius: 4px;
}
::-moz-selection,
::selection {
color: black;
background: var(--primary-color);
}