mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-21 15:26:17 -04:00
implement device pairing via 6-digit code and qr-code
This commit is contained in:
parent
e559aecde7
commit
3c07a4199b
11 changed files with 1098 additions and 195 deletions
|
@ -25,30 +25,56 @@ class PeersUI {
|
|||
Events.on('peers', e => this._onPeers(e.detail));
|
||||
Events.on('file-progress', e => this._onFileProgress(e.detail));
|
||||
Events.on('paste', e => this._onPaste(e));
|
||||
Events.on('ws-disconnect', _ => this._clearPeers());
|
||||
Events.on('ws-disconnected', _ => this._clearPeers());
|
||||
Events.on('secret-room-deleted', _ => this._clearPeers('secret'));
|
||||
this.peers = {};
|
||||
}
|
||||
|
||||
_onPeerJoined(peer) {
|
||||
if (this.peers[peer.id]) return; // peer already exists
|
||||
_onPeerJoined(msg) {
|
||||
this._joinPeer(msg.peer, msg.roomType, msg.roomType);
|
||||
}
|
||||
|
||||
_joinPeer(peer, roomType, roomSecret) {
|
||||
peer.roomType = roomType;
|
||||
peer.roomSecret = roomSecret;
|
||||
if (this.peers[peer.id]) {
|
||||
this.peers[peer.id].roomType = peer.roomType;
|
||||
this._redrawPeer(peer);
|
||||
return; // peer already exists
|
||||
}
|
||||
this.peers[peer.id] = peer;
|
||||
}
|
||||
|
||||
_onPeerConnected(peerId) {
|
||||
if(this.peers[peerId])
|
||||
if(this.peers[peerId] && !$(peerId))
|
||||
new PeerUI(this.peers[peerId]);
|
||||
}
|
||||
|
||||
_onPeers(peers) {
|
||||
_redrawPeer(peer) {
|
||||
const peerNode = $(peer.id);
|
||||
if (!peerNode) return;
|
||||
peerNode.classList.remove('type-ip', 'type-secret');
|
||||
peerNode.classList.add(`type-${peer.roomType}`)
|
||||
}
|
||||
|
||||
_redrawPeers() {
|
||||
const peers = this._getPeers();
|
||||
this._clearPeers();
|
||||
peers.forEach(peer => this._onPeerJoined(peer));
|
||||
peers.forEach(peer => {
|
||||
this._joinPeer(peer, peer.roomType, peer.roomSecret);
|
||||
this._onPeerConnected(peer.id);
|
||||
});
|
||||
}
|
||||
|
||||
_onPeers(msg) {
|
||||
msg.peers.forEach(peer => this._joinPeer(peer, msg.roomType, msg.roomSecret));
|
||||
}
|
||||
|
||||
_onPeerDisconnected(peerId) {
|
||||
const $peer = $(peerId);
|
||||
if (!$peer) return;
|
||||
$peer.remove();
|
||||
setTimeout(e => window.animateBackground(true), 1750); // Start animation again
|
||||
setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
|
||||
_onPeerLeft(peerId) {
|
||||
|
@ -56,6 +82,16 @@ class PeersUI {
|
|||
delete this.peers[peerId];
|
||||
}
|
||||
|
||||
_onSecretRoomDeleted(roomSecret) {
|
||||
for (const peerId in this.peers) {
|
||||
const peer = this.peers[peerId];
|
||||
console.debug(peer);
|
||||
if (peer.roomSecret === roomSecret) {
|
||||
this._onPeerLeft(peerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onFileProgress(progress) {
|
||||
const peerId = progress.sender || progress.recipient;
|
||||
const $peer = $(peerId);
|
||||
|
@ -63,10 +99,17 @@ class PeersUI {
|
|||
$peer.ui.setProgress(progress.progress);
|
||||
}
|
||||
|
||||
_clearPeers() {
|
||||
const $peers = $$('x-peers').innerHTML = '';
|
||||
Object.keys(this.peers).forEach(peerId => delete this.peers[peerId]);
|
||||
setTimeout(e => window.animateBackground(true), 1750); // Start animation again
|
||||
_clearPeers(roomType = 'all') {
|
||||
for (const peerId in this.peers) {
|
||||
if (roomType === 'all' || this.peers[peerId].roomType === roomType) {
|
||||
const peerNode = $(peerId);
|
||||
if(peerNode) peerNode.remove();
|
||||
delete this.peers[peerId];
|
||||
}
|
||||
}
|
||||
if ($$('x-peers').innerHTML === '') {
|
||||
setTimeout(_ => window.animateBackground(true), 1750); // Start animation again
|
||||
}
|
||||
}
|
||||
|
||||
_getPeers() {
|
||||
|
@ -76,7 +119,9 @@ class PeersUI {
|
|||
peers.push({
|
||||
id: peersNode.id,
|
||||
name: peersNode.name,
|
||||
rtcSupported: peersNode.rtcSupported
|
||||
rtcSupported: peersNode.rtcSupported,
|
||||
roomType: peersNode.roomType,
|
||||
roomSecret: peersNode.roomSecret
|
||||
})
|
||||
});
|
||||
return peers;
|
||||
|
@ -103,7 +148,6 @@ class PeersUI {
|
|||
descriptor = files[0].name;
|
||||
noPeersMessage = `Open Snapdrop on other devices to send <i>${descriptor}</i> directly`;
|
||||
} else if (files.length > 1) {
|
||||
console.debug(files);
|
||||
descriptor = `${files.length} files`;
|
||||
noPeersMessage = `Open Snapdrop on other devices to send ${descriptor} directly`;
|
||||
} else if (text.length > 0) {
|
||||
|
@ -132,7 +176,7 @@ class PeersUI {
|
|||
window.pasteMode.activated = true;
|
||||
console.log('Paste mode activated.')
|
||||
|
||||
this._onPeers(this._getPeers());
|
||||
this._redrawPeers();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,7 +203,7 @@ class PeersUI {
|
|||
cancelPasteModeBtn.removeEventListener('click', this._cancelPasteMode);
|
||||
cancelPasteModeBtn.setAttribute('hidden', "");
|
||||
|
||||
this._onPeers(this._getPeers());
|
||||
this._redrawPeers();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,22 +257,23 @@ class PeerUI {
|
|||
|
||||
constructor(peer) {
|
||||
this._peer = peer;
|
||||
this._roomType = peer.roomType;
|
||||
this._roomSecret = peer.roomSecret;
|
||||
this._initDom();
|
||||
this._bindListeners(this.$el);
|
||||
$$('x-peers').appendChild(this.$el);
|
||||
setTimeout(e => window.animateBackground(false), 1750); // Stop animation
|
||||
setTimeout(_ => window.animateBackground(false), 1750); // Stop animation
|
||||
}
|
||||
|
||||
_initDom() {
|
||||
const el = document.createElement('x-peer');
|
||||
el.id = this._peer.id;
|
||||
el.name = this._peer.name;
|
||||
el.rtcSupported = this._peer.rtcSupported;
|
||||
el.innerHTML = this.html();
|
||||
el.ui = this;
|
||||
el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
||||
el.querySelector('.name').textContent = this._displayName();
|
||||
el.querySelector('.device-name').textContent = this._deviceName();
|
||||
el.classList.add(`type-${this._roomType}`);
|
||||
this.$el = el;
|
||||
this.$progress = el.querySelector('.progress');
|
||||
}
|
||||
|
@ -241,7 +286,7 @@ class PeerUI {
|
|||
el.addEventListener('dragleave', e => this._onDragEnd(e));
|
||||
el.addEventListener('dragover', e => this._onDragOver(e));
|
||||
el.addEventListener('contextmenu', e => this._onRightClick(e));
|
||||
el.addEventListener('touchstart', e => this._onTouchStart(e));
|
||||
el.addEventListener('touchstart', _ => this._onTouchStart());
|
||||
el.addEventListener('touchend', e => this._onTouchEnd(e));
|
||||
// prevent browser's default file drop behavior
|
||||
Events.on('dragover', e => e.preventDefault());
|
||||
|
@ -329,7 +374,7 @@ class PeerUI {
|
|||
Events.fire('text-recipient', this._peer.id);
|
||||
}
|
||||
|
||||
_onTouchStart(e) {
|
||||
_onTouchStart() {
|
||||
this._touchStart = Date.now();
|
||||
this._touchTimer = setTimeout(_ => this._onTouchEnd(), 610);
|
||||
}
|
||||
|
@ -348,8 +393,9 @@ class PeerUI {
|
|||
class Dialog {
|
||||
constructor(id) {
|
||||
this.$el = $(id);
|
||||
this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', e => this.hide()))
|
||||
this.$el.querySelectorAll('[close]').forEach(el => el.addEventListener('click', _ => this.hide()))
|
||||
this.$autoFocus = this.$el.querySelector('[autofocus]');
|
||||
Events.on('ws-disconnected', _ => this.hide());
|
||||
}
|
||||
|
||||
show() {
|
||||
|
@ -359,8 +405,10 @@ class Dialog {
|
|||
|
||||
hide() {
|
||||
this.$el.removeAttribute('show');
|
||||
document.activeElement.blur();
|
||||
window.blur();
|
||||
if (this.$autoFocus) {
|
||||
document.activeElement.blur();
|
||||
window.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -419,7 +467,7 @@ class ReceiveDialog extends Dialog {
|
|||
// fallback for iOS
|
||||
$a.target = '_blank';
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => $a.href = reader.result;
|
||||
reader.onload = _ => $a.href = reader.result;
|
||||
reader.readAsDataURL(file.blob);
|
||||
}
|
||||
|
||||
|
@ -448,10 +496,254 @@ class ReceiveDialog extends Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
class PairDeviceDialog extends Dialog {
|
||||
constructor() {
|
||||
super('pairDeviceDialog');
|
||||
$('pair-device').addEventListener('click', _ => this._pairDeviceInitiate());
|
||||
this.$inputRoomKeyChars = this.$el.querySelectorAll('#keyInputContainer>input');
|
||||
this.$submitBtn = this.$el.querySelector('button[type="submit"]');
|
||||
this.$roomKey = this.$el.querySelector('#roomKey');
|
||||
this.$qrCode = this.$el.querySelector('#roomKeyQrCode');
|
||||
this.$clearSecretsBtn = $('clear-pair-devices');
|
||||
this.$footerInstructions = $$('footer>.font-body2');
|
||||
let createJoinForm = this.$el.querySelector('form');
|
||||
createJoinForm.addEventListener('submit', _ => this._onSubmit());
|
||||
|
||||
this.$el.querySelector('[close]').addEventListener('click', _ => this._pairDeviceCancel())
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('input', e => this._onCharsInput(e)));
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('keyup', _ => this.evaluateRoomKeyChars()));
|
||||
this.$inputRoomKeyChars.forEach(el => el.addEventListener('keydown', e => this._onCharsKeyDown(e)));
|
||||
|
||||
Events.on('keydown', e => this._onKeyDown(e));
|
||||
Events.on('ws-connected', _ => this._onWsConnected());
|
||||
Events.on('pair-device-initiated', e => this._pairDeviceInitiated(e.detail));
|
||||
Events.on('pair-device-joined', e => this._pairDeviceJoined(e.detail));
|
||||
Events.on('pair-device-join-key-invalid', _ => this._pairDeviceJoinKeyInvalid());
|
||||
Events.on('pair-device-canceled', e => this._pairDeviceCanceled(e.detail));
|
||||
Events.on('room-secret-delete', e => this._onRoomSecretDelete(e.detail))
|
||||
Events.on('clear-room-secrets', e => this._onClearRoomSecrets(e.detail))
|
||||
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
|
||||
this.$el.addEventListener('paste', e => this._onPaste(e));
|
||||
|
||||
this.evaluateRoomKeyChars();
|
||||
this.evaluateUrlAttributes();
|
||||
}
|
||||
|
||||
_onCharsInput(e) {
|
||||
e.target.value = e.target.value.replace(/\D/g,'');
|
||||
if (!e.target.value) return;
|
||||
let nextSibling = e.target.nextElementSibling;
|
||||
if (nextSibling) {
|
||||
e.preventDefault();
|
||||
nextSibling.focus();
|
||||
nextSibling.select();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyDown(e) {
|
||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||
this.hide();
|
||||
this._pairDeviceCancel();
|
||||
}
|
||||
if (this.$el.attributes["show"] && e.code === "keyO") {
|
||||
this._onRoomSecretDelete()
|
||||
}
|
||||
}
|
||||
|
||||
_onCharsKeyDown(e) {
|
||||
if (this.$el.attributes["show"] && e.code === "Escape") {
|
||||
this.hide();
|
||||
this._pairDeviceCancel();
|
||||
}
|
||||
let previousSibling = e.target.previousElementSibling;
|
||||
let nextSibling = e.target.nextElementSibling;
|
||||
if (e.key === "Backspace" && previousSibling && !e.target.value) {
|
||||
previousSibling.value = '';
|
||||
previousSibling.focus();
|
||||
} else if (e.key === "ArrowRight" && nextSibling) {
|
||||
e.preventDefault();
|
||||
nextSibling.focus();
|
||||
nextSibling.select();
|
||||
} else if (e.key === "ArrowLeft" && previousSibling) {
|
||||
e.preventDefault();
|
||||
previousSibling.focus();
|
||||
previousSibling.select();
|
||||
}
|
||||
}
|
||||
|
||||
_onPaste(e) {
|
||||
e.preventDefault();
|
||||
let num = e.clipboardData.getData("Text").replace(/\D/g,'').substring(0, 6);
|
||||
for (let i = 0; i < num.length; i++) {
|
||||
document.activeElement.value = num.charAt(i);
|
||||
let nextSibling = document.activeElement.nextElementSibling;
|
||||
if (!nextSibling) break;
|
||||
nextSibling.focus();
|
||||
nextSibling.select();
|
||||
}
|
||||
}
|
||||
|
||||
evaluateRoomKeyChars() {
|
||||
if (this.$el.querySelectorAll('#keyInputContainer>input:placeholder-shown').length > 0) {
|
||||
this.$submitBtn.setAttribute("disabled", "");
|
||||
} else {
|
||||
this.inputRoomKey = "";
|
||||
this.$inputRoomKeyChars.forEach(el => {
|
||||
this.inputRoomKey += el.value;
|
||||
})
|
||||
this.$submitBtn.removeAttribute("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
evaluateUrlAttributes() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has('room_key')) {
|
||||
this._pairDeviceJoin(urlParams.get('room_key'));
|
||||
window.history.replaceState({}, "title**", '/'); //remove room_key from url
|
||||
}
|
||||
}
|
||||
|
||||
_onWsConnected() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets', roomSecrets);
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
_pairDeviceInitiate() {
|
||||
Events.fire('pair-device-initiate');
|
||||
}
|
||||
|
||||
_pairDeviceInitiated(msg) {
|
||||
this.roomKey = msg.roomKey;
|
||||
this.roomSecret = msg.roomSecret;
|
||||
this.$roomKey.innerText = `${this.roomKey.substring(0,3)} ${this.roomKey.substring(3,6)}`
|
||||
// Display the QR code for the url
|
||||
const qr = new QRCode({
|
||||
content: this._getShareRoomURL(),
|
||||
width: 80,
|
||||
height: 80,
|
||||
padding: 0,
|
||||
background: "transparent",
|
||||
color: getComputedStyle(document.body).getPropertyValue('--text-color'),
|
||||
ecl: "L",
|
||||
join: true
|
||||
});
|
||||
this.$qrCode.innerHTML = qr.svg();
|
||||
this.show();
|
||||
}
|
||||
|
||||
_getShareRoomURL() {
|
||||
let url = new URL(location.href);
|
||||
url.searchParams.append('room_key', this.roomKey)
|
||||
return url.href;
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
this._pairDeviceJoin(this.inputRoomKey);
|
||||
}
|
||||
|
||||
_pairDeviceJoin(roomKey) {
|
||||
if (/^\d{6}$/g.test(roomKey)) {
|
||||
roomKey = roomKey.substring(0,6);
|
||||
Events.fire('pair-device-join', roomKey);
|
||||
let lastChar = this.$inputRoomKeyChars[5];
|
||||
lastChar.focus();
|
||||
lastChar.select();
|
||||
}
|
||||
}
|
||||
|
||||
_pairDeviceJoined(roomSecret) {
|
||||
this.hide();
|
||||
PersistentStorage.addRoomSecret(roomSecret).then(_ => {
|
||||
Events.fire('notify-user', 'Devices paired successfully.')
|
||||
this._evaluateNumberRoomSecrets()
|
||||
}).finally(_ => {
|
||||
this._cleanUp()
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
_pairDeviceJoinKeyInvalid() {
|
||||
Events.fire('notify-user', 'Key not valid')
|
||||
}
|
||||
|
||||
_pairDeviceCancel() {
|
||||
this.hide();
|
||||
this._cleanUp();
|
||||
Events.fire('pair-device-cancel');
|
||||
}
|
||||
|
||||
_pairDeviceCanceled(roomKey) {
|
||||
Events.fire('notify-user', `Key ${roomKey} invalidated.`)
|
||||
}
|
||||
|
||||
_cleanUp() {
|
||||
this.roomSecret = null;
|
||||
this.roomKey = null;
|
||||
this.inputRoomKey = '';
|
||||
this.$inputRoomKeyChars.forEach(el => el.value = '');
|
||||
}
|
||||
|
||||
_onRoomSecretDelete(roomSecret) {
|
||||
PersistentStorage.deleteRoomSecret(roomSecret).then(_ => {
|
||||
console.debug("then secret: " + roomSecret)
|
||||
Events.fire('room-secret-deleted', roomSecret)
|
||||
this._evaluateNumberRoomSecrets();
|
||||
}).catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
_onClearRoomSecrets() {
|
||||
PersistentStorage.getAllRoomSecrets().then(roomSecrets => {
|
||||
Events.fire('room-secrets-cleared', roomSecrets);
|
||||
PersistentStorage.clearRoomSecrets().finally(_ => {
|
||||
Events.fire('notify-user', 'All Devices unpaired.')
|
||||
this._evaluateNumberRoomSecrets();
|
||||
})
|
||||
}).catch((e) => console.error(e));
|
||||
}
|
||||
|
||||
_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.$footerInstructions.innerText = "You can be discovered on this network and by paired devices";
|
||||
} else {
|
||||
this.$clearSecretsBtn.setAttribute('hidden', '');
|
||||
this.$footerInstructions.innerText = "You can be discovered by everyone on this network";
|
||||
}
|
||||
}).catch((e) => console.error(e));
|
||||
}
|
||||
}
|
||||
|
||||
class ClearDevicesDialog extends Dialog {
|
||||
constructor() {
|
||||
super('clearDevicesDialog');
|
||||
$('clear-pair-devices').addEventListener('click', _ => this._onClearPairDevices());
|
||||
let clearDevicesForm = this.$el.querySelector('form');
|
||||
clearDevicesForm.addEventListener('submit', _ => this._onSubmit());
|
||||
}
|
||||
|
||||
_onClearPairDevices() {
|
||||
this.show();
|
||||
}
|
||||
|
||||
_onSubmit() {
|
||||
Events.fire('clear-room-secrets');
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
class SendTextDialog extends Dialog {
|
||||
constructor() {
|
||||
super('sendTextDialog');
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail))
|
||||
Events.on('text-recipient', e => this._onRecipient(e.detail));
|
||||
this.$text = this.$el.querySelector('#textInput');
|
||||
const button = this.$el.querySelector('form');
|
||||
button.addEventListener('submit', e => this._send(e));
|
||||
|
@ -490,6 +782,7 @@ class SendTextDialog extends Dialog {
|
|||
to: this._recipient,
|
||||
text: this.$text.innerText
|
||||
});
|
||||
this.$text.innerText = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -545,7 +838,6 @@ class Toast extends Dialog {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class Notifications {
|
||||
|
||||
constructor() {
|
||||
|
@ -556,7 +848,7 @@ class Notifications {
|
|||
if (Notification.permission !== 'granted') {
|
||||
this.$button = $('notification');
|
||||
this.$button.removeAttribute('hidden');
|
||||
this.$button.addEventListener('click', e => this._requestPermission());
|
||||
this.$button.addEventListener('click', _ => this._requestPermission());
|
||||
}
|
||||
Events.on('text-received', e => this._messageNotification(e.detail.text));
|
||||
Events.on('file-received', e => this._downloadNotification(e.detail.name));
|
||||
|
@ -568,7 +860,7 @@ class Notifications {
|
|||
Events.fire('notify-user', Notifications.PERMISSION_ERROR || 'Error');
|
||||
return;
|
||||
}
|
||||
this._notify('Even more snappy sharing!');
|
||||
this._notify('Notifications enabled.');
|
||||
this.$button.setAttribute('hidden', 1);
|
||||
});
|
||||
}
|
||||
|
@ -603,10 +895,10 @@ class Notifications {
|
|||
if (document.visibilityState !== 'visible') {
|
||||
if (isURL(message)) {
|
||||
const notification = this._notify(message, 'Click to open link');
|
||||
this._bind(notification, e => window.open(message, '_blank', null, true));
|
||||
this._bind(notification, _ => window.open(message, '_blank', null, true));
|
||||
} else {
|
||||
const notification = this._notify(message, 'Click to copy text');
|
||||
this._bind(notification, e => this._copyText(message, notification));
|
||||
this._bind(notification, _ => this._copyText(message, notification));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -615,7 +907,7 @@ class Notifications {
|
|||
if (document.visibilityState !== 'visible') {
|
||||
const notification = this._notify(message, 'Click to download');
|
||||
if (!window.isDownloadSupported) return;
|
||||
this._bind(notification, e => this._download(notification));
|
||||
this._bind(notification, _ => this._download(notification));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -625,14 +917,18 @@ class Notifications {
|
|||
}
|
||||
|
||||
_copyText(message, notification) {
|
||||
notification.close();
|
||||
if (!navigator.clipboard.writeText(message)) return;
|
||||
this._notify('Copied text to clipboard');
|
||||
if (navigator.clipboard.writeText(message)) {
|
||||
notification.close();
|
||||
this._notify('Copied text to clipboard');
|
||||
} else {
|
||||
this._notify('Writing to clipboard failed. Copy manually!');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_bind(notification, handler) {
|
||||
if (notification.then) {
|
||||
notification.then(e => serviceWorker.getNotifications().then(notifications => {
|
||||
notification.then(_ => serviceWorker.getNotifications().then(notifications => {
|
||||
serviceWorker.addEventListener('notificationclick', handler);
|
||||
}));
|
||||
} else {
|
||||
|
@ -641,7 +937,6 @@ class Notifications {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class NetworkStatusUI {
|
||||
|
||||
constructor() {
|
||||
|
@ -658,6 +953,10 @@ class NetworkStatusUI {
|
|||
}
|
||||
|
||||
_showOnlineMessage() {
|
||||
if (!this.firstConnect) {
|
||||
this.firstConnect = true;
|
||||
return;
|
||||
}
|
||||
Events.fire('notify-user', 'You are back online');
|
||||
window.animateBackground(true);
|
||||
}
|
||||
|
@ -682,16 +981,193 @@ class WebShareTargetUI {
|
|||
}
|
||||
}
|
||||
|
||||
class PersistentStorage {
|
||||
constructor() {
|
||||
if (!('indexedDB' in window)) {
|
||||
this.logBrowserNotCapable();
|
||||
return;
|
||||
}
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
this.logBrowserNotCapable();
|
||||
console.log('Error initializing database: ');
|
||||
console.error(e)
|
||||
};
|
||||
DBOpenRequest.onsuccess = () => {
|
||||
console.log('Database initialised.');
|
||||
};
|
||||
DBOpenRequest.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
db.onerror = e => console.log('Error loading database: ' + e);
|
||||
db.createObjectStore('keyval');
|
||||
const roomSecretsObjectStore = db.createObjectStore('room_secrets', {autoIncrement: true});
|
||||
roomSecretsObjectStore.createIndex('secret', 'secret', { unique: true });
|
||||
}
|
||||
}
|
||||
|
||||
logBrowserNotCapable() {
|
||||
console.log("This browser does not support IndexedDB. Paired devices will be gone after closing the browser.");
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.put(value, key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Added key-pair: ${key} - ${value}`);
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static get(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.get(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Retrieved key-pair: ${key} - ${objectStoreRequest.result}`);
|
||||
resolve(objectStoreRequest.result);
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
static delete(key) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_store');
|
||||
DBOpenRequest.onsuccess = (e) => {
|
||||
const db = e.target.result;
|
||||
const transaction = db.transaction('keyval', 'readwrite');
|
||||
const objectStore = transaction.objectStore('keyval');
|
||||
const objectStoreRequest = objectStore.delete(key);
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted key: ${key}`);
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static addRoomSecret(roomSecret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_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}`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static getAllRoomSecrets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_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.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);
|
||||
}
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static deleteRoomSecret(room_secret) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_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);
|
||||
objectStoreRequestKey.onsuccess = e => {
|
||||
if (!e.target.result) {
|
||||
console.log(`Nothing to delete. room_secret not existing: ${room_secret}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const objectStoreRequestDeletion = objectStore.delete(e.target.result);
|
||||
objectStoreRequestDeletion.onsuccess = _ => {
|
||||
console.log(`Request successful. Deleted room_secret: ${room_secret}`);
|
||||
resolve();
|
||||
}
|
||||
objectStoreRequestDeletion.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static clearRoomSecrets() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const DBOpenRequest = window.indexedDB.open('snapdrop_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.clear();
|
||||
objectStoreRequest.onsuccess = _ => {
|
||||
console.log('Request successful. All room_secrets cleared');
|
||||
resolve();
|
||||
};
|
||||
}
|
||||
DBOpenRequest.onerror = (e) => {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Snapdrop {
|
||||
constructor() {
|
||||
const server = new ServerConnection();
|
||||
const peers = new PeersManager(server);
|
||||
const peersUI = new PeersUI();
|
||||
Events.on('load', e => {
|
||||
Events.on('load', _ => {
|
||||
const server = new ServerConnection();
|
||||
const peers = new PeersManager(server);
|
||||
const peersUI = new PeersUI();
|
||||
const receiveDialog = new ReceiveDialog();
|
||||
const sendTextDialog = new SendTextDialog();
|
||||
const receiveTextDialog = new ReceiveTextDialog();
|
||||
const pairDeviceDialog = new PairDeviceDialog();
|
||||
const clearDevicesDialog = new ClearDevicesDialog();
|
||||
const toast = new Toast();
|
||||
const notifications = new Notifications();
|
||||
const networkStatusUI = new NetworkStatusUI();
|
||||
|
@ -700,10 +1176,10 @@ class Snapdrop {
|
|||
}
|
||||
}
|
||||
|
||||
const persistentStorage = new PersistentStorage();
|
||||
const snapdrop = new Snapdrop();
|
||||
|
||||
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js')
|
||||
.then(serviceWorker => {
|
||||
|
@ -714,6 +1190,11 @@ if ('serviceWorker' in navigator) {
|
|||
|
||||
window.addEventListener('beforeinstallprompt', e => {
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
// make peerId persistent when pwa installed
|
||||
PersistentStorage.get('peerId').then(peerId => {
|
||||
sessionStorage.setItem("peerId", peerId);
|
||||
}).catch(e => console.error(e));
|
||||
|
||||
// don't display install banner when installed
|
||||
return e.preventDefault();
|
||||
} else {
|
||||
|
@ -805,7 +1286,7 @@ as the user has dismissed the permission prompt several times.
|
|||
This can be reset in Page Info
|
||||
which can be accessed by clicking the lock icon next to the URL.`;
|
||||
|
||||
document.body.onclick = e => { // safari hack to fix audio
|
||||
document.body.onclick = _ => { // safari hack to fix audio
|
||||
document.body.onclick = null;
|
||||
if (!(/.*Version.*Safari.*/.test(navigator.userAgent))) return;
|
||||
blop.play();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue