mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-20 07:05:05 -04:00
2714 lines
93 KiB
JavaScript
2714 lines
93 KiB
JavaScript
class PeersUI {
|
|
|
|
constructor() {
|
|
this.$xPeers = $$('x-peers');
|
|
this.$xNoPeers = $$('x-no-peers');
|
|
this.$xInstructions = $$('x-instructions');
|
|
this.$wsFallbackWarning = $('websocket-fallback');
|
|
|
|
this.$sharePanel = $$('.shr-panel');
|
|
this.$shareModeImageThumb = $$('.shr-panel .image-thumb');
|
|
this.$shareModeTextThumb = $$('.shr-panel .text-thumb');
|
|
this.$shareModeFileThumb = $$('.shr-panel .file-thumb');
|
|
this.$shareModeDescriptor = $$('.shr-panel .share-descriptor');
|
|
this.$shareModeDescriptorItem = $$('.shr-panel .descriptor-item');
|
|
this.$shareModeDescriptorOther = $$('.shr-panel .descriptor-other');
|
|
this.$shareModeCancelBtn = $$('.shr-panel .cancel-btn');
|
|
this.$shareModeEditBtn = $$('.shr-panel .edit-btn');
|
|
|
|
this.peerUIs = {};
|
|
|
|
this.shareMode = {};
|
|
this.shareMode.active = false;
|
|
this.shareMode.descriptor = "";
|
|
this.shareMode.files = [];
|
|
this.shareMode.text = "";
|
|
|
|
Events.on('peer-joined', e => this._onPeerJoined(e.detail.peer, e.detail.roomType, e.detail.roomId));
|
|
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash));
|
|
Events.on('peer-connecting', e => this._onPeerConnecting(e.detail));
|
|
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
|
Events.on('peers', e => this._onPeers(e.detail));
|
|
Events.on('set-progress', e => this._onSetProgress(e.detail));
|
|
|
|
Events.on('drop', e => this._onDrop(e));
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
Events.on('dragover', e => this._onDragOver(e));
|
|
Events.on('dragleave', _ => this._onDragEnd());
|
|
Events.on('dragend', _ => this._onDragEnd());
|
|
Events.on('resize', _ => this._evaluateOverflowingPeers());
|
|
Events.on('header-changed', _ => this._evaluateOverflowingPeers());
|
|
|
|
Events.on('paste', e => this._onPaste(e));
|
|
Events.on('activate-share-mode', e => this._activateShareMode(e.detail.files, e.detail.text));
|
|
Events.on('translation-loaded', _ => this._reloadShareMode());
|
|
Events.on('room-type-removed', e => this._onRoomTypeRemoved(e.detail.peerId, e.detail.roomType));
|
|
Events.on('room-secret-added', e => this._onRoomSecretAdded(e.detail.peerId, e.detail.roomSecret));
|
|
|
|
this.$shareModeCancelBtn.addEventListener('click', _ => this._deactivateShareMode());
|
|
|
|
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e.detail.peerId, e.detail.displayName));
|
|
|
|
Events.on('ws-config', e => this._evaluateRtcSupport(e.detail))
|
|
}
|
|
|
|
_evaluateRtcSupport(wsConfig) {
|
|
if (wsConfig.wsFallback) {
|
|
this.$wsFallbackWarning.removeAttribute("hidden");
|
|
}
|
|
else {
|
|
this.$wsFallbackWarning.setAttribute("hidden", true);
|
|
if (!window.isRtcSupported) {
|
|
alert(Localization.getTranslation("instructions.webrtc-requirement"));
|
|
}
|
|
}
|
|
}
|
|
|
|
_changePeerDisplayName(peerId, displayName) {
|
|
const peerUI = this.peerUIs[peerId];
|
|
|
|
if (!peerUI) return;
|
|
|
|
peerUI._setDisplayName(displayName);
|
|
}
|
|
|
|
_onPeerDisplayNameChanged(peerId, displayName) {
|
|
if (!peerId || !displayName) return;
|
|
|
|
this._changePeerDisplayName(peerId, displayName);
|
|
}
|
|
|
|
async _onKeyDown(e) {
|
|
if (!this.shareMode.active || Dialog.anyDialogShown()) return;
|
|
|
|
if (e.key === "Escape") {
|
|
await this._deactivateShareMode();
|
|
}
|
|
|
|
// close About PairDrop page on Escape
|
|
if (e.key === "Escape") {
|
|
window.location.hash = '#';
|
|
}
|
|
}
|
|
|
|
_onPeerJoined(peer, roomType, roomId) {
|
|
this._joinPeer(peer, roomType, roomId);
|
|
}
|
|
|
|
_joinPeer(peer, roomType, roomId) {
|
|
const existingPeerUI = this.peerUIs[peer.id];
|
|
if (existingPeerUI) {
|
|
// peerUI already exists. Abort but add roomType to GUI
|
|
existingPeerUI._addRoomId(roomType, roomId);
|
|
return;
|
|
}
|
|
|
|
const peerUI = new PeerUI(peer, roomType, roomId, {
|
|
active: this.shareMode.active,
|
|
descriptor: this.shareMode.descriptor,
|
|
});
|
|
this.peerUIs[peer.id] = peerUI;
|
|
}
|
|
|
|
_onPeerConnected(peerId, connectionHash) {
|
|
const peerUI = this.peerUIs[peerId];
|
|
|
|
if (!peerUI) return;
|
|
|
|
peerUI._peerConnected(true, connectionHash);
|
|
|
|
this._addPeerUIIfMissing(peerUI);
|
|
}
|
|
|
|
_addPeerUIIfMissing(peerUI) {
|
|
if (this.$xPeers.contains(peerUI.$el)) return;
|
|
|
|
this.$xPeers.appendChild(peerUI.$el);
|
|
this._evaluateOverflowingPeers();
|
|
}
|
|
|
|
_onPeerConnecting(peerId) {
|
|
const peerUI = this.peerUIs[peerId];
|
|
|
|
if (!peerUI) return;
|
|
|
|
peerUI._peerConnected(false);
|
|
}
|
|
|
|
_evaluateOverflowingPeers() {
|
|
if (this.$xPeers.clientHeight < this.$xPeers.scrollHeight) {
|
|
this.$xPeers.classList.add('overflowing');
|
|
}
|
|
else {
|
|
this.$xPeers.classList.remove('overflowing');
|
|
}
|
|
}
|
|
|
|
_onPeers(msg) {
|
|
msg.peers.forEach(peer => this._joinPeer(peer, msg.roomType, msg.roomId));
|
|
}
|
|
|
|
_onPeerDisconnected(peerId) {
|
|
const peerUI = this.peerUIs[peerId];
|
|
|
|
if (!peerUI) return;
|
|
|
|
peerUI._removeDom();
|
|
|
|
delete this.peerUIs[peerId];
|
|
|
|
this._evaluateOverflowingPeers();
|
|
}
|
|
|
|
_onRoomTypeRemoved(peerId, roomType) {
|
|
const peerUI = this.peerUIs[peerId];
|
|
|
|
if (!peerUI) return;
|
|
|
|
peerUI._removeRoomId(roomType);
|
|
}
|
|
|
|
|
|
_onRoomSecretAdded(peerId, roomSecret) {
|
|
// If a device is paired that is already connected we must update the displayName saved for the roomSecret
|
|
// here as the "display-name-changed" message has already been received
|
|
const peerUI = this.peerUIs[peerId];
|
|
|
|
if (!peerUI || !peerUI._connected) return;
|
|
|
|
const displayName = peerUI._displayName();
|
|
|
|
PersistentStorage
|
|
.updateRoomSecretDisplayName(roomSecret, displayName)
|
|
.then(roomSecretEntry => {
|
|
console.log(`Successfully updated DisplayName for roomSecretEntry ${roomSecretEntry.key}`);
|
|
})
|
|
}
|
|
|
|
_onSetProgress(progress) {
|
|
const peerUI = this.peerUIs[progress.peerId];
|
|
|
|
if (!peerUI) return;
|
|
|
|
peerUI.setProgress(progress.progress, progress.status);
|
|
}
|
|
|
|
_onDrop(e) {
|
|
e.preventDefault();
|
|
|
|
if (this.shareMode.active || Dialog.anyDialogShown()) return;
|
|
|
|
if (!$$('x-peer') || !$$('x-peer').contains(e.target)) {
|
|
if (e.dataTransfer.files.length > 0) {
|
|
Events.fire('activate-share-mode', {files: e.dataTransfer.files});
|
|
} else {
|
|
for (let i=0; i<e.dataTransfer.items.length; i++) {
|
|
if (e.dataTransfer.items[i].type === "text/plain") {
|
|
e.dataTransfer.items[i].getAsString(text => {
|
|
Events.fire('activate-share-mode', {text: text});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this._onDragEnd();
|
|
}
|
|
|
|
_onDragOver(e) {
|
|
e.preventDefault();
|
|
|
|
if (this.shareMode.active || Dialog.anyDialogShown()) return;
|
|
|
|
this.$xInstructions.setAttribute('drop-bg', true);
|
|
this.$xNoPeers.setAttribute('drop-bg', true);
|
|
}
|
|
|
|
_onDragEnd() {
|
|
this.$xInstructions.removeAttribute('drop-bg');
|
|
this.$xNoPeers.removeAttribute('drop-bg');
|
|
}
|
|
|
|
_onPaste(e) {
|
|
// prevent send on paste when dialog is open
|
|
if (this.shareMode.active || Dialog.anyDialogShown()) return;
|
|
|
|
e.preventDefault()
|
|
const files = e.clipboardData.files;
|
|
const text = e.clipboardData.getData("Text");
|
|
|
|
if (files.length > 0) {
|
|
Events.fire('activate-share-mode', {files: files});
|
|
} else if (text.length > 0) {
|
|
if (ShareTextDialog.isApproveShareTextSet()) {
|
|
Events.fire('share-text-dialog', text);
|
|
} else {
|
|
Events.fire('activate-share-mode', {text: text});
|
|
}
|
|
}
|
|
}
|
|
|
|
async _activateShareMode(files = [], text = "") {
|
|
if (this.shareMode.active || (files.length === 0 && text.length === 0)) return;
|
|
|
|
this._activateCallback = e => this._sendShareData(e);
|
|
this._editShareTextCallback = _ => {
|
|
this._deactivateShareMode();
|
|
Events.fire('share-text-dialog', text);
|
|
};
|
|
|
|
Events.on('share-mode-pointerdown', this._activateCallback);
|
|
|
|
const sharedText = Localization.getTranslation("instructions.activate-share-mode-shared-text");
|
|
const andOtherFilesPlural = Localization.getTranslation("instructions.activate-share-mode-and-other-files-plural", null, {count: files.length-1});
|
|
const andOtherFiles = Localization.getTranslation("instructions.activate-share-mode-and-other-file");
|
|
|
|
let descriptorComplete, descriptorItem, descriptorOther, descriptorInstructions;
|
|
|
|
if (files.length > 2) {
|
|
// files shared
|
|
descriptorItem = files[0].name;
|
|
descriptorOther = andOtherFilesPlural;
|
|
descriptorComplete = `${descriptorItem} ${descriptorOther}`;
|
|
}
|
|
else if (files.length === 2) {
|
|
descriptorItem = files[0].name;
|
|
descriptorOther = andOtherFiles;
|
|
descriptorComplete = `${descriptorItem} ${descriptorOther}`;
|
|
} else if (files.length === 1) {
|
|
descriptorItem = files[0].name;
|
|
descriptorComplete = descriptorItem;
|
|
}
|
|
else {
|
|
// text shared
|
|
descriptorItem = text.replace(/\s/g," ");
|
|
descriptorComplete = sharedText;
|
|
}
|
|
|
|
if (files.length > 0) {
|
|
if (descriptorOther) {
|
|
this.$shareModeDescriptorOther.innerText = descriptorOther;
|
|
this.$shareModeDescriptorOther.removeAttribute('hidden');
|
|
}
|
|
if (files.length > 1) {
|
|
descriptorInstructions = Localization.getTranslation("instructions.activate-share-mode-shared-files-plural", null, {count: files.length});
|
|
}
|
|
else {
|
|
descriptorInstructions = Localization.getTranslation("instructions.activate-share-mode-shared-file");
|
|
}
|
|
|
|
files = await mime.addMissingMimeTypesToFiles(files);
|
|
|
|
if (files[0].type.split('/')[0] === 'image') {
|
|
try {
|
|
let imageUrl = await getThumbnailAsDataUrl(files[0], 80, null, 0.9);
|
|
|
|
this.$shareModeImageThumb.style.backgroundImage = `url(${imageUrl})`;
|
|
|
|
this.$shareModeImageThumb.removeAttribute('hidden');
|
|
} catch (e) {
|
|
console.error(e);
|
|
this.$shareModeFileThumb.removeAttribute('hidden');
|
|
}
|
|
} else {
|
|
this.$shareModeFileThumb.removeAttribute('hidden');
|
|
}
|
|
}
|
|
else {
|
|
this.$shareModeTextThumb.removeAttribute('hidden');
|
|
|
|
this.$shareModeEditBtn.addEventListener('click', this._editShareTextCallback);
|
|
this.$shareModeEditBtn.removeAttribute('hidden');
|
|
|
|
descriptorInstructions = Localization.getTranslation("instructions.activate-share-mode-shared-text");
|
|
}
|
|
|
|
const desktop = Localization.getTranslation("instructions.x-instructions-share-mode_desktop", null, {descriptor: descriptorInstructions});
|
|
const mobile = Localization.getTranslation("instructions.x-instructions-share-mode_mobile", null, {descriptor: descriptorInstructions});
|
|
|
|
this.$xInstructions.setAttribute('desktop', desktop);
|
|
this.$xInstructions.setAttribute('mobile', mobile);
|
|
|
|
this.$sharePanel.removeAttribute('hidden');
|
|
|
|
this.$shareModeDescriptor.removeAttribute('hidden');
|
|
this.$shareModeDescriptorItem.innerText = descriptorItem;
|
|
|
|
this.shareMode.active = true;
|
|
this.shareMode.descriptor = descriptorComplete;
|
|
this.shareMode.files = files;
|
|
this.shareMode.text = text;
|
|
|
|
console.log('Share mode activated.');
|
|
|
|
Events.fire('share-mode-changed', {
|
|
active: true,
|
|
descriptor: descriptorComplete
|
|
});
|
|
}
|
|
|
|
async _reloadShareMode() {
|
|
// If shareMode is active only
|
|
if (!this.shareMode.active) return;
|
|
|
|
let files = this.shareMode.files;
|
|
let text = this.shareMode.text;
|
|
|
|
await this._deactivateShareMode();
|
|
await this._activateShareMode(files, text);
|
|
}
|
|
|
|
async _deactivateShareMode() {
|
|
if (!this.shareMode.active) return;
|
|
|
|
this.shareMode.active = false;
|
|
this.shareMode.descriptor = "";
|
|
this.shareMode.files = [];
|
|
this.shareMode.text = "";
|
|
|
|
Events.off('share-mode-pointerdown', this._activateCallback);
|
|
|
|
const desktop = Localization.getTranslation("instructions.x-instructions_desktop");
|
|
const mobile = Localization.getTranslation("instructions.x-instructions_mobile");
|
|
|
|
this.$xInstructions.setAttribute('desktop', desktop);
|
|
this.$xInstructions.setAttribute('mobile', mobile);
|
|
|
|
this.$sharePanel.setAttribute('hidden', true);
|
|
|
|
this.$shareModeImageThumb.setAttribute('hidden', true);
|
|
this.$shareModeFileThumb.setAttribute('hidden', true);
|
|
this.$shareModeTextThumb.setAttribute('hidden', true);
|
|
|
|
this.$shareModeDescriptorItem.innerHTML = "";
|
|
this.$shareModeDescriptorItem.classList.remove('cursive');
|
|
this.$shareModeDescriptorOther.innerHTML = "";
|
|
this.$shareModeDescriptorOther.setAttribute('hidden', true);
|
|
this.$shareModeEditBtn.removeEventListener('click', this._editShareTextCallback);
|
|
this.$shareModeEditBtn.setAttribute('hidden', true);
|
|
|
|
console.log('Share mode deactivated.')
|
|
Events.fire('share-mode-changed', { active: false });
|
|
}
|
|
|
|
_sendShareData(e) {
|
|
// send the shared file/text content
|
|
const peerId = e.detail.peerId;
|
|
const files = this.shareMode.files;
|
|
const text = this.shareMode.text;
|
|
|
|
if (files.length > 0) {
|
|
Events.fire('files-selected', {
|
|
files: files,
|
|
to: peerId
|
|
});
|
|
}
|
|
else if (text.length > 0) {
|
|
Events.fire('send-text', {
|
|
text: text,
|
|
to: peerId
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
class PeerUI {
|
|
|
|
static _badgeClassNames = ["badge-room-ip", "badge-room-secret", "badge-room-public-id"];
|
|
|
|
constructor(peer, roomType, roomId, shareMode = {active: false, descriptor: ""}) {
|
|
this.$xInstructions = $$('x-instructions');
|
|
|
|
this._peer = peer;
|
|
this._connectionHash = "";
|
|
this._connected = false;
|
|
|
|
this._roomIds = {}
|
|
this._roomIds[roomType] = roomId;
|
|
|
|
this._shareMode = shareMode;
|
|
|
|
this._createCallbacks();
|
|
this._initDom();
|
|
|
|
// ShareMode
|
|
Events.on('share-mode-changed', e => this._onShareModeChanged(e.detail.active, e.detail.descriptor));
|
|
}
|
|
|
|
_initDom() {
|
|
this.$el = document.createElement('x-peer');
|
|
this.$el.id = this._peer.id;
|
|
this.$el.ui = this;
|
|
this.$el.classList.add('center');
|
|
|
|
this.html();
|
|
|
|
this.$label = this.$el.querySelector('label');
|
|
this.$input = this.$el.querySelector('input');
|
|
this.$displayName = this.$el.querySelector('.name');
|
|
|
|
this.updateTypesClassList();
|
|
|
|
this.setStatus("connect");
|
|
|
|
this._evaluateShareMode();
|
|
this._bindListeners();
|
|
}
|
|
|
|
_removeDom() {
|
|
this.$el.remove();
|
|
}
|
|
|
|
html() {
|
|
let title= Localization.getTranslation("peer-ui.click-to-send");
|
|
|
|
this.$el.innerHTML = `
|
|
<label class="column center pointer" title="${title}">
|
|
<input type="file" multiple/>
|
|
<x-icon>
|
|
<div class="icon-wrapper" shadow="1">
|
|
<svg class="icon"><use xlink:href="#"/></svg>
|
|
</div>
|
|
<div class="highlight-wrapper center">
|
|
<div class="highlight highlight-room-ip" shadow="1"></div>
|
|
<div class="highlight highlight-room-secret" shadow="1"></div>
|
|
<div class="highlight highlight-room-public-id" shadow="1"></div>
|
|
</div>
|
|
</x-icon>
|
|
<div class="progress">
|
|
<div class="circle"></div>
|
|
<div class="circle right"></div>
|
|
</div>
|
|
<div class="device-descriptor">
|
|
<div class="name font-subheading"></div>
|
|
<div class="device-name font-body2"></div>
|
|
<div class="status font-body2"></div>
|
|
</div>
|
|
</label>`;
|
|
|
|
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
|
|
this.$el.querySelector('.name').textContent = this._displayName();
|
|
this.$el.querySelector('.device-name').textContent = this._deviceName();
|
|
}
|
|
|
|
updateTypesClassList() {
|
|
// Remove all classes
|
|
this.$el.classList.remove('type-ip', 'type-secret', 'type-public-id', 'type-same-browser', 'ws-peer');
|
|
|
|
// Add classes accordingly
|
|
Object.keys(this._roomIds).forEach(roomType => this.$el.classList.add(`type-${roomType}`));
|
|
|
|
if (BrowserTabsConnector.peerIsSameBrowser(this._peer.id)) {
|
|
this.$el.classList.add(`type-same-browser`);
|
|
}
|
|
|
|
if (!this._peer.rtcSupported || !window.isRtcSupported) {
|
|
this.$el.classList.add('ws-peer');
|
|
}
|
|
}
|
|
|
|
_addRoomId(roomType, roomId) {
|
|
this._roomIds[roomType] = roomId;
|
|
this.updateTypesClassList();
|
|
}
|
|
|
|
_removeRoomId(roomType) {
|
|
delete this._roomIds[roomType];
|
|
this.updateTypesClassList();
|
|
}
|
|
|
|
_onShareModeChanged(active = false, descriptor = "") {
|
|
this._shareMode.active = active;
|
|
this._shareMode.descriptor = descriptor;
|
|
|
|
this._evaluateShareMode();
|
|
this._bindListeners();
|
|
}
|
|
|
|
_evaluateShareMode() {
|
|
let title;
|
|
if (!this._shareMode.active) {
|
|
title = Localization.getTranslation("peer-ui.click-to-send");
|
|
this.$input.removeAttribute('disabled');
|
|
}
|
|
else {
|
|
title = Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: this._shareMode.descriptor});
|
|
this.$input.setAttribute('disabled', true);
|
|
}
|
|
this.$label.setAttribute('title', title);
|
|
}
|
|
|
|
_createCallbacks() {
|
|
this._callbackInput = e => this._onFilesSelected(e);
|
|
this._callbackClickSleep = _ => NoSleepUI.enable();
|
|
this._callbackTouchStartSleep = _ => NoSleepUI.enable();
|
|
this._callbackDrop = e => this._onDrop(e);
|
|
this._callbackDragEnd = e => this._onDragEnd(e);
|
|
this._callbackDragLeave = e => this._onDragEnd(e);
|
|
this._callbackDragOver = e => this._onDragOver(e);
|
|
this._callbackContextMenu = e => this._onRightClick(e);
|
|
this._callbackTouchStart = e => this._onTouchStart(e);
|
|
this._callbackTouchEnd = e => this._onTouchEnd(e);
|
|
this._callbackPointerDown = e => this._onPointerDown(e);
|
|
}
|
|
|
|
_bindListeners() {
|
|
if(!this._shareMode.active) {
|
|
// Remove Events Share mode
|
|
this.$el.removeEventListener('pointerdown', this._callbackPointerDown);
|
|
|
|
// Add Events Normal Mode
|
|
this.$el.querySelector('input').addEventListener('change', this._callbackInput);
|
|
this.$el.addEventListener('click', this._callbackClickSleep);
|
|
this.$el.addEventListener('touchstart', this._callbackTouchStartSleep);
|
|
this.$el.addEventListener('drop', this._callbackDrop);
|
|
this.$el.addEventListener('dragend', this._callbackDragEnd);
|
|
this.$el.addEventListener('dragleave', this._callbackDragLeave);
|
|
this.$el.addEventListener('dragover', this._callbackDragOver);
|
|
this.$el.addEventListener('contextmenu', this._callbackContextMenu);
|
|
this.$el.addEventListener('touchstart', this._callbackTouchStart);
|
|
this.$el.addEventListener('touchend', this._callbackTouchEnd);
|
|
}
|
|
else {
|
|
// Remove Events Normal Mode
|
|
this.$el.removeEventListener('click', this._callbackClickSleep);
|
|
this.$el.removeEventListener('touchstart', this._callbackTouchStartSleep);
|
|
this.$el.removeEventListener('drop', this._callbackDrop);
|
|
this.$el.removeEventListener('dragend', this._callbackDragEnd);
|
|
this.$el.removeEventListener('dragleave', this._callbackDragLeave);
|
|
this.$el.removeEventListener('dragover', this._callbackDragOver);
|
|
this.$el.removeEventListener('contextmenu', this._callbackContextMenu);
|
|
this.$el.removeEventListener('touchstart', this._callbackTouchStart);
|
|
this.$el.removeEventListener('touchend', this._callbackTouchEnd);
|
|
|
|
// Add Events Share mode
|
|
this.$el.addEventListener('pointerdown', this._callbackPointerDown);
|
|
}
|
|
}
|
|
|
|
_onPointerDown(e) {
|
|
// Prevents triggering of event twice on touch devices
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
Events.fire('share-mode-pointerdown', {
|
|
peerId: this._peer.id
|
|
});
|
|
}
|
|
|
|
_peerConnected(connected = true, connectionHash = "") {
|
|
if (connected) {
|
|
this._connected = true;
|
|
|
|
// on reconnect
|
|
this.setStatus(this.oldStatus);
|
|
this.oldStatus = null;
|
|
|
|
this._connectionHash = connectionHash;
|
|
}
|
|
else {
|
|
this._connected = false;
|
|
|
|
if (!this.oldStatus && this.currentStatus !== "connect") {
|
|
// save old status when reconnecting
|
|
this.oldStatus = this.currentStatus;
|
|
}
|
|
this.setStatus("connect");
|
|
|
|
this._connectionHash = "";
|
|
}
|
|
}
|
|
|
|
getConnectionHashWithSpaces() {
|
|
if (this._connectionHash.length !== 16) {
|
|
return ""
|
|
}
|
|
|
|
return `${this._connectionHash.substring(0, 4)} ${this._connectionHash.substring(4, 8)} ${this._connectionHash.substring(8, 12)} ${this._connectionHash.substring(12, 16)}`;
|
|
}
|
|
|
|
_displayName() {
|
|
return this._peer.name.displayName;
|
|
}
|
|
|
|
_deviceName() {
|
|
return this._peer.name.deviceName;
|
|
}
|
|
|
|
_setDisplayName(displayName) {
|
|
this._peer.name.displayName = displayName;
|
|
this.$displayName.textContent = displayName;
|
|
}
|
|
|
|
_roomTypes() {
|
|
return Object.keys(this._roomIds);
|
|
}
|
|
|
|
_badgeClassName() {
|
|
const roomTypes = this._roomTypes();
|
|
if (roomTypes.includes('secret')) {
|
|
return 'badge-room-secret';
|
|
}
|
|
else if (roomTypes.includes('ip')) {
|
|
return 'badge-room-ip';
|
|
}
|
|
else {
|
|
return 'badge-room-public-id';
|
|
}
|
|
}
|
|
|
|
_icon() {
|
|
const device = this._peer.name.device || this._peer.name;
|
|
if (device.type === 'mobile') {
|
|
return '#phone-iphone';
|
|
}
|
|
if (device.type === 'tablet') {
|
|
return '#tablet-mac';
|
|
}
|
|
return '#desktop-mac';
|
|
}
|
|
|
|
_onFilesSelected(e) {
|
|
const $input = e.target;
|
|
const files = $input.files;
|
|
|
|
Events.fire('files-selected', {
|
|
files: files,
|
|
to: this._peer.id
|
|
});
|
|
$input.files = null; // reset input
|
|
}
|
|
|
|
setProgress(progress, status) {
|
|
const $progress = this.$el.querySelector('.progress');
|
|
|
|
if (0.5 < progress && progress < 1) {
|
|
$progress.classList.add('over50');
|
|
}
|
|
else {
|
|
$progress.classList.remove('over50');
|
|
}
|
|
|
|
if (progress === 1) {
|
|
progress = 0;
|
|
status = null;
|
|
}
|
|
|
|
this.setStatus(status);
|
|
|
|
const degrees = `rotate(${360 * progress}deg)`;
|
|
$progress.style.setProperty('--progress', degrees);
|
|
}
|
|
|
|
setStatus(status) {
|
|
if (!status) {
|
|
this.$el.removeAttribute('status');
|
|
this.$el.querySelector('.status').innerHTML = '';
|
|
this.currentStatus = null;
|
|
NoSleepUI.disableIfPeersIdle();
|
|
return;
|
|
}
|
|
|
|
if (status === this.currentStatus) return;
|
|
|
|
let statusName = {
|
|
"connect": Localization.getTranslation("peer-ui.connecting"),
|
|
"prepare": Localization.getTranslation("peer-ui.preparing"),
|
|
"transfer": Localization.getTranslation("peer-ui.transferring"),
|
|
"receive": Localization.getTranslation("peer-ui.receiving"),
|
|
"process": Localization.getTranslation("peer-ui.processing"),
|
|
"wait": Localization.getTranslation("peer-ui.waiting")
|
|
}[status];
|
|
|
|
this.$el.setAttribute('status', status);
|
|
this.$el.querySelector('.status').innerText = statusName;
|
|
this.currentStatus = status;
|
|
}
|
|
|
|
_onDrop(e) {
|
|
if (this._shareMode.active || Dialog.anyDialogShown()) return;
|
|
|
|
e.preventDefault();
|
|
|
|
if (e.dataTransfer.files.length > 0) {
|
|
Events.fire('files-selected', {
|
|
files: e.dataTransfer.files,
|
|
to: this._peer.id
|
|
});
|
|
} else {
|
|
for (let i=0; i<e.dataTransfer.items.length; i++) {
|
|
if (e.dataTransfer.items[i].type === "text/plain") {
|
|
e.dataTransfer.items[i].getAsString(text => {
|
|
Events.fire('send-text', {
|
|
text: text,
|
|
to: this._peer.id
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
this._onDragEnd();
|
|
}
|
|
|
|
_onDragOver() {
|
|
this.$el.setAttribute('drop', true);
|
|
this.$xInstructions.setAttribute('drop-peer', true);
|
|
}
|
|
|
|
_onDragEnd() {
|
|
this.$el.removeAttribute('drop');
|
|
this.$xInstructions.removeAttribute('drop-peer');
|
|
}
|
|
|
|
_onRightClick(e) {
|
|
e.preventDefault();
|
|
Events.fire('text-recipient', {
|
|
peerId: this._peer.id,
|
|
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
|
});
|
|
}
|
|
|
|
_onTouchStart(e) {
|
|
this._touchStart = Date.now();
|
|
this._touchTimer = setTimeout(() => this._onTouchEnd(e), 610);
|
|
}
|
|
|
|
_onTouchEnd(e) {
|
|
if (Date.now() - this._touchStart < 500) {
|
|
clearTimeout(this._touchTimer);
|
|
}
|
|
else if (this._touchTimer) { // this was a long tap
|
|
e.preventDefault();
|
|
Events.fire('text-recipient', {
|
|
peerId: this._peer.id,
|
|
deviceName: e.target.closest('x-peer').querySelector('.name').innerText
|
|
});
|
|
}
|
|
this._touchTimer = null;
|
|
}
|
|
}
|
|
|
|
class Dialog {
|
|
constructor(id) {
|
|
this.$el = $(id);
|
|
this.$autoFocus = this.$el.querySelector('[autofocus]');
|
|
this.$xBackground = this.$el.querySelector('x-background');
|
|
this.$closeBtns = this.$el.querySelectorAll('[close]');
|
|
|
|
this.$closeBtns.forEach(el => {
|
|
el.addEventListener('click', _ => this.hide())
|
|
});
|
|
|
|
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
|
|
}
|
|
|
|
static anyDialogShown() {
|
|
return document.querySelectorAll('x-dialog[show]').length > 0;
|
|
}
|
|
|
|
show() {
|
|
if (this.$xBackground) {
|
|
this.$xBackground.scrollTop = 0;
|
|
}
|
|
|
|
this.$el.setAttribute('show', true);
|
|
|
|
if (!window.isMobile && this.$autoFocus) {
|
|
this.$autoFocus.focus();
|
|
}
|
|
}
|
|
|
|
isShown() {
|
|
return !!this.$el.attributes["show"];
|
|
}
|
|
|
|
hide() {
|
|
this.$el.removeAttribute('show');
|
|
if (!window.isMobile) {
|
|
document.activeElement.blur();
|
|
window.blur();
|
|
}
|
|
document.title = 'PairDrop | Transfer Files Cross-Platform. No Setup, No Signup.';
|
|
changeFavicon("images/favicon-96x96.png");
|
|
this.correspondingPeerId = undefined;
|
|
}
|
|
|
|
_onPeerDisconnected(peerId) {
|
|
if (this.isShown() && this.correspondingPeerId === peerId) {
|
|
this.hide();
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.selected-peer-left"));
|
|
}
|
|
}
|
|
|
|
_evaluateOverflowing(element) {
|
|
if (element.clientHeight < element.scrollHeight) {
|
|
element.classList.add('overflowing');
|
|
}
|
|
else {
|
|
element.classList.remove('overflowing');
|
|
}
|
|
}
|
|
}
|
|
|
|
class LanguageSelectDialog extends Dialog {
|
|
|
|
constructor() {
|
|
super('language-select-dialog');
|
|
|
|
this.$languageSelectBtn = $('language-selector');
|
|
this.$languageSelectBtn.addEventListener('click', _ => this.show());
|
|
|
|
this.$languageButtons = this.$el.querySelectorAll(".language-buttons .btn");
|
|
this.$languageButtons.forEach($btn => {
|
|
$btn.addEventListener("click", e => this.selectLanguage(e));
|
|
})
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (!this.isShown()) return;
|
|
|
|
if (e.code === "Escape") {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
show() {
|
|
let locale = Localization.getLocale();
|
|
this.currentLanguageBtn = Localization.isSystemLocale()
|
|
? this.$languageButtons[0]
|
|
: this.$el.querySelector(`.btn[value="${locale}"]`);
|
|
|
|
this.currentLanguageBtn.classList.add("current");
|
|
|
|
super.show();
|
|
}
|
|
|
|
hide() {
|
|
this.currentLanguageBtn.classList.remove("current");
|
|
|
|
super.hide();
|
|
}
|
|
|
|
selectLanguage(e) {
|
|
e.preventDefault()
|
|
let languageCode = e.target.value;
|
|
|
|
if (languageCode) {
|
|
localStorage.setItem('language_code', languageCode);
|
|
}
|
|
else {
|
|
localStorage.removeItem('language_code');
|
|
}
|
|
|
|
Localization.setTranslation(languageCode)
|
|
.then(_ => this.hide());
|
|
}
|
|
}
|
|
|
|
class ReceiveDialog extends Dialog {
|
|
constructor(id) {
|
|
super(id);
|
|
this.$fileDescription = this.$el.querySelector('.file-description');
|
|
this.$displayName = this.$el.querySelector('.display-name');
|
|
this.$fileStem = this.$el.querySelector('.file-stem');
|
|
this.$fileExtension = this.$el.querySelector('.file-extension');
|
|
this.$fileOther = this.$el.querySelector('.file-other');
|
|
this.$fileSize = this.$el.querySelector('.file-size');
|
|
this.$previewBox = this.$el.querySelector('.file-preview');
|
|
this.$receiveTitle = this.$el.querySelector('h2:first-of-type');
|
|
}
|
|
|
|
_formatFileSize(bytes) {
|
|
// 1 GB = 1e3 MB = 1e6 KB = 1e9 B
|
|
if (bytes >= 1e9) {
|
|
const rndGigabytes = Math.round(10 * bytes / 1e9) / 10;
|
|
return `${rndGigabytes} GB`;
|
|
}
|
|
else if (bytes >= 1e6) {
|
|
const rndMegabytes = Math.round(10 * bytes / 1e6) / 10;
|
|
return `${rndMegabytes} MB`;
|
|
}
|
|
else if (bytes >= (1e3)) {
|
|
const rndKilobytes = Math.round(10 * bytes / 1e3) / 10;
|
|
return `${rndKilobytes} KB`;
|
|
}
|
|
else {
|
|
return `${bytes} bytes`;
|
|
}
|
|
}
|
|
|
|
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) {
|
|
let fileOther = "";
|
|
|
|
if (files.length === 2) {
|
|
fileOther = imagesOnly
|
|
? Localization.getTranslation("dialogs.file-other-description-image")
|
|
: Localization.getTranslation("dialogs.file-other-description-file");
|
|
}
|
|
else if (files.length >= 2) {
|
|
fileOther = imagesOnly
|
|
? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1})
|
|
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
|
|
}
|
|
|
|
this.$fileOther.innerText = fileOther;
|
|
|
|
const fileName = files[0].name;
|
|
const fileNameSplit = fileName.split('.');
|
|
const fileExtension = '.' + fileNameSplit[fileNameSplit.length - 1];
|
|
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
|
|
this.$fileExtension.innerText = fileExtension;
|
|
this.$fileSize.innerText = this._formatFileSize(totalSize);
|
|
this.$displayName.innerText = displayName;
|
|
this.$displayName.title = connectionHash;
|
|
this.$displayName.classList.remove("badge-room-ip", "badge-room-secret", "badge-room-public-id");
|
|
this.$displayName.classList.add(badgeClassName)
|
|
}
|
|
}
|
|
|
|
class ReceiveFileDialog extends ReceiveDialog {
|
|
|
|
constructor() {
|
|
super('receive-file-dialog');
|
|
|
|
this.$downloadBtn = this.$el.querySelector('#download-btn');
|
|
this.$shareBtn = this.$el.querySelector('#share-btn');
|
|
|
|
Events.on('files-received', e => this._onFilesReceived(e.detail.peerId, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
|
|
this._filesQueue = [];
|
|
}
|
|
|
|
async _onFilesReceived(peerId, files, imagesOnly, totalSize) {
|
|
const displayName = $(peerId).ui._displayName();
|
|
const connectionHash = $(peerId).ui._connectionHash;
|
|
const badgeClassName = $(peerId).ui._badgeClassName();
|
|
|
|
this._filesQueue.push({
|
|
peerId: peerId,
|
|
displayName: displayName,
|
|
connectionHash: connectionHash,
|
|
files: files,
|
|
imagesOnly: imagesOnly,
|
|
totalSize: totalSize,
|
|
badgeClassName: badgeClassName
|
|
});
|
|
|
|
audioPlayer.playBlop();
|
|
|
|
await this._nextFiles();
|
|
}
|
|
|
|
async _nextFiles() {
|
|
if (this._busy || !this._filesQueue.length) return;
|
|
this._busy = true;
|
|
const {peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName} = this._filesQueue.shift();
|
|
await this._displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName);
|
|
}
|
|
|
|
createPreviewElement(file) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
let mime = file.type.split('/')[0]
|
|
let previewElement = {
|
|
image: 'img',
|
|
audio: 'audio',
|
|
video: 'video'
|
|
}
|
|
|
|
if (Object.keys(previewElement).indexOf(mime) === -1) {
|
|
resolve(false);
|
|
}
|
|
else {
|
|
let element = document.createElement(previewElement[mime]);
|
|
element.controls = true;
|
|
element.onload = _ => {
|
|
this.$previewBox.appendChild(element);
|
|
resolve(true);
|
|
};
|
|
element.onloadeddata = _ => {
|
|
this.$previewBox.appendChild(element);
|
|
resolve(true);
|
|
};
|
|
element.onerror = _ => {
|
|
reject(`${mime} preview could not be loaded from type ${file.type}`);
|
|
};
|
|
element.src = URL.createObjectURL(file);
|
|
}
|
|
} catch (e) {
|
|
reject(`preview could not be loaded from type ${file.type}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) {
|
|
this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName);
|
|
|
|
let descriptor, url, filenameDownload;
|
|
if (files.length === 1) {
|
|
descriptor = imagesOnly
|
|
? Localization.getTranslation("dialogs.title-image")
|
|
: Localization.getTranslation("dialogs.title-file");
|
|
}
|
|
else {
|
|
descriptor = imagesOnly
|
|
? Localization.getTranslation("dialogs.title-image-plural")
|
|
: Localization.getTranslation("dialogs.title-file-plural");
|
|
}
|
|
this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor});
|
|
|
|
const canShare = (window.iOS || window.android) && !!navigator.share && navigator.canShare({files});
|
|
if (canShare) {
|
|
this.$shareBtn.removeAttribute('hidden');
|
|
this.$shareBtn.onclick = _ => {
|
|
navigator.share({files: files})
|
|
.catch(err => {
|
|
console.error(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
let downloadZipped = false;
|
|
if (files.length > 1) {
|
|
downloadZipped = true;
|
|
try {
|
|
let bytesCompleted = 0;
|
|
zipper.createNewZipWriter();
|
|
for (let i=0; i<files.length; i++) {
|
|
await zipper.addFile(files[i], {
|
|
onprogress: (progress) => {
|
|
Events.fire('set-progress', {
|
|
peerId: peerId,
|
|
progress: (bytesCompleted + progress) / totalSize,
|
|
status: 'process'
|
|
})
|
|
}
|
|
});
|
|
bytesCompleted += files[i].size;
|
|
}
|
|
url = await zipper.getBlobURL();
|
|
|
|
let now = new Date(Date.now());
|
|
let year = now.getFullYear().toString();
|
|
let month = (now.getMonth()+1).toString();
|
|
month = month.length < 2 ? "0" + month : month;
|
|
let date = now.getDate().toString();
|
|
date = date.length < 2 ? "0" + date : date;
|
|
let hours = now.getHours().toString();
|
|
hours = hours.length < 2 ? "0" + hours : hours;
|
|
let minutes = now.getMinutes().toString();
|
|
minutes = minutes.length < 2 ? "0" + minutes : minutes;
|
|
filenameDownload = `PairDrop_files_${year+month+date}_${hours+minutes}.zip`;
|
|
} catch (e) {
|
|
console.error(e);
|
|
downloadZipped = false;
|
|
}
|
|
}
|
|
|
|
this.$downloadBtn.removeAttribute('disabled');
|
|
this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download");
|
|
this.$downloadBtn.onclick = _ => {
|
|
if (downloadZipped) {
|
|
let tmpZipBtn = document.createElement("a");
|
|
tmpZipBtn.download = filenameDownload;
|
|
tmpZipBtn.href = url;
|
|
tmpZipBtn.click();
|
|
}
|
|
else {
|
|
this._downloadFilesIndividually(files);
|
|
}
|
|
|
|
if (!canShare) {
|
|
this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download-again");
|
|
}
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor}));
|
|
|
|
// Prevent clicking the button multiple times
|
|
this.$downloadBtn.style.pointerEvents = "none";
|
|
setTimeout(() => this.$downloadBtn.style.pointerEvents = "unset", 2000);
|
|
};
|
|
|
|
document.title = files.length === 1
|
|
? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop`
|
|
: `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`;
|
|
changeFavicon("images/favicon-96x96-notification.png");
|
|
|
|
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
|
|
this.show();
|
|
|
|
setTimeout(() => {
|
|
// wait for the dialog to be shown
|
|
if (canShare) {
|
|
this.$shareBtn.click();
|
|
}
|
|
else {
|
|
this.$downloadBtn.click();
|
|
}
|
|
}, 500);
|
|
|
|
this.createPreviewElement(files[0])
|
|
.then(canPreview => {
|
|
if (canPreview) {
|
|
console.log('the file is able to preview');
|
|
}
|
|
else {
|
|
console.log('the file is not able to preview');
|
|
}
|
|
})
|
|
.catch(r => console.error(r));
|
|
}
|
|
|
|
_downloadFilesIndividually(files) {
|
|
let tmpBtn = document.createElement("a");
|
|
for (let i=0; i<files.length; i++) {
|
|
tmpBtn.download = files[i].name;
|
|
tmpBtn.href = URL.createObjectURL(files[i]);
|
|
tmpBtn.click();
|
|
}
|
|
}
|
|
|
|
hide() {
|
|
super.hide();
|
|
setTimeout(async () => {
|
|
this.$shareBtn.setAttribute('hidden', true);
|
|
this.$downloadBtn.setAttribute('disabled', true);
|
|
this.$previewBox.innerHTML = '';
|
|
this._busy = false;
|
|
await this._nextFiles();
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
class ReceiveRequestDialog extends ReceiveDialog {
|
|
|
|
constructor() {
|
|
super('receive-request-dialog');
|
|
|
|
this.$acceptRequestBtn = this.$el.querySelector('#accept-request');
|
|
this.$declineRequestBtn = this.$el.querySelector('#decline-request');
|
|
this.$acceptRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(true));
|
|
this.$declineRequestBtn.addEventListener('click', _ => this._respondToFileTransferRequest(false));
|
|
|
|
Events.on('files-transfer-request', e => this._onRequestFileTransfer(e.detail.request, e.detail.peerId))
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
this._filesTransferRequestQueue = [];
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (!this.isShown()) return;
|
|
|
|
if (e.code === "Escape") {
|
|
this._respondToFileTransferRequest(false);
|
|
}
|
|
}
|
|
|
|
_onRequestFileTransfer(request, peerId) {
|
|
this._filesTransferRequestQueue.push({request: request, peerId: peerId});
|
|
if (this.isShown()) return;
|
|
this._dequeueRequests();
|
|
}
|
|
|
|
_dequeueRequests() {
|
|
if (!this._filesTransferRequestQueue.length) return;
|
|
let {request, peerId} = this._filesTransferRequestQueue.shift();
|
|
this._showRequestDialog(request, peerId)
|
|
}
|
|
|
|
_showRequestDialog(request, peerId) {
|
|
this.correspondingPeerId = peerId;
|
|
|
|
const displayName = $(peerId).ui._displayName();
|
|
const connectionHash = $(peerId).ui._connectionHash;
|
|
|
|
const badgeClassName = $(peerId).ui._badgeClassName();
|
|
|
|
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize, badgeClassName);
|
|
|
|
if (request.thumbnailDataUrl && request.thumbnailDataUrl.substring(0, 22) === "data:image/jpeg;base64") {
|
|
let element = document.createElement('img');
|
|
element.src = request.thumbnailDataUrl;
|
|
this.$previewBox.appendChild(element)
|
|
}
|
|
|
|
const transferRequestTitle= request.imagesOnly
|
|
? Localization.getTranslation('document-titles.image-transfer-requested')
|
|
: Localization.getTranslation('document-titles.file-transfer-requested');
|
|
|
|
this.$receiveTitle.innerText = transferRequestTitle;
|
|
|
|
document.title = `${transferRequestTitle} - PairDrop`;
|
|
changeFavicon("images/favicon-96x96-notification.png");
|
|
|
|
this.$acceptRequestBtn.removeAttribute('disabled');
|
|
this.show();
|
|
}
|
|
|
|
_respondToFileTransferRequest(accepted) {
|
|
Events.fire('respond-to-files-transfer-request', {
|
|
to: this.correspondingPeerId,
|
|
accepted: accepted
|
|
})
|
|
if (accepted) {
|
|
Events.fire('set-progress', {peerId: this.correspondingPeerId, progress: 0, status: 'wait'});
|
|
NoSleepUI.enable();
|
|
}
|
|
this.hide();
|
|
}
|
|
|
|
hide() {
|
|
// clear previewBox after dialog is closed
|
|
setTimeout(() => {
|
|
this.$previewBox.innerHTML = '';
|
|
this.$acceptRequestBtn.setAttribute('disabled', true);
|
|
}, 300);
|
|
|
|
super.hide();
|
|
|
|
// show next request
|
|
setTimeout(() => this._dequeueRequests(), 300);
|
|
}
|
|
}
|
|
|
|
class InputKeyContainer {
|
|
constructor(inputKeyContainer, evaluationRegex, onAllCharsFilled, onNoAllCharsFilled, onLastCharFilled) {
|
|
|
|
this.$inputKeyContainer = inputKeyContainer;
|
|
this.$inputKeyChars = inputKeyContainer.querySelectorAll('input');
|
|
|
|
this.$inputKeyChars.forEach(char => char.addEventListener('input', e => this._onCharsInput(e)));
|
|
this.$inputKeyChars.forEach(char => char.addEventListener('keydown', e => this._onCharsKeyDown(e)));
|
|
this.$inputKeyChars.forEach(char => char.addEventListener('keyup', e => this._onCharsKeyUp(e)));
|
|
this.$inputKeyChars.forEach(char => char.addEventListener('focus', e => e.target.select()));
|
|
this.$inputKeyChars.forEach(char => char.addEventListener('click', e => e.target.select()));
|
|
|
|
this.evalRgx = evaluationRegex
|
|
|
|
this._onAllCharsFilled = onAllCharsFilled;
|
|
this._onNotAllCharsFilled = onNoAllCharsFilled;
|
|
this._onLastCharFilled = onLastCharFilled;
|
|
}
|
|
|
|
_enableChars() {
|
|
this.$inputKeyChars.forEach(char => char.removeAttribute('disabled'));
|
|
}
|
|
|
|
_disableChars() {
|
|
this.$inputKeyChars.forEach(char => char.setAttribute('disabled', true));
|
|
}
|
|
|
|
_clearChars() {
|
|
this.$inputKeyChars.forEach(char => char.value = '');
|
|
}
|
|
|
|
_cleanUp() {
|
|
this._clearChars();
|
|
this._disableChars();
|
|
}
|
|
|
|
_onCharsInput(e) {
|
|
if (!e.target.value.match(this.evalRgx)) {
|
|
e.target.value = '';
|
|
return;
|
|
}
|
|
this._evaluateKeyChars();
|
|
|
|
let nextSibling = e.target.nextElementSibling;
|
|
if (nextSibling) {
|
|
e.preventDefault();
|
|
nextSibling.focus();
|
|
}
|
|
}
|
|
|
|
_onCharsKeyDown(e) {
|
|
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();
|
|
}
|
|
else if (e.key === "ArrowLeft" && previousSibling) {
|
|
e.preventDefault();
|
|
previousSibling.focus();
|
|
}
|
|
}
|
|
|
|
_onCharsKeyUp(e) {
|
|
// deactivate submit btn when e.g. using backspace to clear element
|
|
if (!e.target.value) {
|
|
this._evaluateKeyChars();
|
|
}
|
|
}
|
|
|
|
_getInputKey() {
|
|
let key = "";
|
|
this.$inputKeyChars.forEach(char => {
|
|
key += char.value;
|
|
})
|
|
return key;
|
|
}
|
|
|
|
_onPaste(pastedKey) {
|
|
let rgx = new RegExp("(?!" + this.evalRgx.source + ").", "g");
|
|
pastedKey = pastedKey.replace(rgx,'').substring(0, this.$inputKeyChars.length)
|
|
for (let i = 0; i < pastedKey.length; i++) {
|
|
document.activeElement.value = pastedKey.charAt(i);
|
|
let nextSibling = document.activeElement.nextElementSibling;
|
|
if (!nextSibling) break;
|
|
nextSibling.focus();
|
|
}
|
|
this._evaluateKeyChars();
|
|
}
|
|
|
|
_evaluateKeyChars() {
|
|
if (this.$inputKeyContainer.querySelectorAll('input:placeholder-shown').length > 0) {
|
|
this._onNotAllCharsFilled();
|
|
}
|
|
else {
|
|
this._onAllCharsFilled();
|
|
|
|
const lastCharFocused = document.activeElement === this.$inputKeyChars[this.$inputKeyChars.length - 1];
|
|
if (lastCharFocused) {
|
|
this._onLastCharFilled();
|
|
}
|
|
}
|
|
}
|
|
|
|
focusLastChar() {
|
|
let lastChar = this.$inputKeyChars[this.$inputKeyChars.length-1];
|
|
lastChar.focus();
|
|
}
|
|
}
|
|
|
|
class PairDeviceDialog extends Dialog {
|
|
constructor() {
|
|
super('pair-device-dialog');
|
|
this.$pairDeviceHeaderBtn = $('pair-device');
|
|
this.$editPairedDevicesHeaderBtn = $('edit-paired-devices');
|
|
this.$footerInstructionsPairedDevices = $$('.discovery-wrapper .badge-room-secret');
|
|
|
|
this.$key = this.$el.querySelector('.key');
|
|
this.$qrCode = this.$el.querySelector('.key-qr-code');
|
|
this.$form = this.$el.querySelector('form');
|
|
this.$closeBtn = this.$el.querySelector('[close]')
|
|
this.$pairSubmitBtn = this.$el.querySelector('button[type="submit"]');
|
|
|
|
this.inputKeyContainer = new InputKeyContainer(
|
|
this.$el.querySelector('.input-key-container'),
|
|
/\d/,
|
|
() => this.$pairSubmitBtn.removeAttribute('disabled'),
|
|
() => this.$pairSubmitBtn.setAttribute('disabled', true),
|
|
() => this._submit()
|
|
);
|
|
|
|
this.$pairDeviceHeaderBtn.addEventListener('click', _ => this._pairDeviceInitiate());
|
|
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
|
this.$closeBtn.addEventListener('click', _ => this._close());
|
|
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
Events.on('ws-disconnected', _ => this.hide());
|
|
Events.on('pair-device-initiated', e => this._onPairDeviceInitiated(e.detail));
|
|
Events.on('pair-device-joined', e => this._onPairDeviceJoined(e.detail.peerId, e.detail.roomSecret));
|
|
Events.on('peers', e => this._onPeers(e.detail.peers, e.detail.roomType, e.detail.roomId));
|
|
Events.on('peer-joined', e => this._onPeerJoined(e.detail.peer, e.detail.roomType, e.detail.roomId));
|
|
Events.on('pair-device-join-key-invalid', _ => this._onPublicRoomJoinKeyInvalid());
|
|
Events.on('pair-device-canceled', e => this._onPairDeviceCanceled(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.$qrCode.addEventListener('click', _ => this._copyPairUrl());
|
|
|
|
this.pairPeer = {};
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (!this.isShown()) return;
|
|
|
|
if (e.code === "Escape") {
|
|
// Timeout to prevent share mode from getting cancelled simultaneously
|
|
setTimeout(() => this._close(), 50);
|
|
}
|
|
}
|
|
|
|
_onPaste(e) {
|
|
e.preventDefault();
|
|
let pastedKey = e.clipboardData
|
|
.getData("Text")
|
|
.replace(/\D/g,'')
|
|
.substring(0, 6);
|
|
this.inputKeyContainer._onPaste(pastedKey);
|
|
}
|
|
|
|
_pairDeviceInitiate() {
|
|
Events.fire('pair-device-initiate');
|
|
}
|
|
|
|
_onPairDeviceInitiated(msg) {
|
|
this.pairKey = msg.pairKey;
|
|
this.roomSecret = msg.roomSecret;
|
|
this._setKeyAndQRCode();
|
|
this.inputKeyContainer._enableChars();
|
|
this.show();
|
|
}
|
|
|
|
_setKeyAndQRCode() {
|
|
this.$key.innerText = `${this.pairKey.substring(0,3)} ${this.pairKey.substring(3,6)}`
|
|
|
|
// Display the QR code for the url
|
|
const qr = new QRCode({
|
|
content: this._getPairUrl(),
|
|
width: 130,
|
|
height: 130,
|
|
padding: 1,
|
|
background: 'white',
|
|
color: 'rgb(18, 18, 18)',
|
|
ecl: "L",
|
|
join: true
|
|
});
|
|
this.$qrCode.innerHTML = qr.svg();
|
|
}
|
|
|
|
_getPairUrl() {
|
|
let url = new URL(location.href);
|
|
url.searchParams.append('pair_key', this.pairKey)
|
|
return url.href;
|
|
}
|
|
|
|
_copyPairUrl() {
|
|
navigator.clipboard.writeText(this._getPairUrl())
|
|
.then(_ => {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.pair-url-copied-to-clipboard"));
|
|
})
|
|
.catch(_ => {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard-error"));
|
|
})
|
|
}
|
|
|
|
_onSubmit(e) {
|
|
e.preventDefault();
|
|
this._submit();
|
|
}
|
|
|
|
_submit() {
|
|
let inputKey = this.inputKeyContainer._getInputKey();
|
|
this._pairDeviceJoin(inputKey);
|
|
}
|
|
|
|
_pairDeviceJoin(pairKey) {
|
|
if (/^\d{6}$/g.test(pairKey)) {
|
|
Events.fire('pair-device-join', pairKey);
|
|
this.inputKeyContainer.focusLastChar();
|
|
}
|
|
}
|
|
|
|
_onPairDeviceJoined(peerId, roomSecret) {
|
|
// abort if peer is another tab on the same browser and remove room-type from gui
|
|
if (BrowserTabsConnector.peerIsSameBrowser(peerId)) {
|
|
this._cleanUp();
|
|
this.hide();
|
|
|
|
Events.fire('room-secrets-deleted', [roomSecret]);
|
|
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-tabs-error"));
|
|
return;
|
|
}
|
|
|
|
// save pairPeer and wait for it to connect to ensure both devices have gotten the roomSecret
|
|
this.pairPeer = {
|
|
"peerId": peerId,
|
|
"roomSecret": roomSecret
|
|
};
|
|
}
|
|
|
|
_onPeers(peers, roomType, roomId) {
|
|
peers.forEach(messagePeer => {
|
|
this._evaluateJoinedPeer(messagePeer, roomType, roomId);
|
|
});
|
|
}
|
|
|
|
_onPeerJoined(peer, roomType, roomId) {
|
|
this._evaluateJoinedPeer(peer, roomType, roomId);
|
|
}
|
|
|
|
_evaluateJoinedPeer(peer, roomType, roomId) {
|
|
const noPairPeerSaved = !Object.keys(this.pairPeer);
|
|
const peerId = peer.id;
|
|
|
|
if (!peerId || !roomType || !roomId || noPairPeerSaved) return;
|
|
|
|
const samePeerId = peerId === this.pairPeer.peerId;
|
|
const sameRoomSecret = roomId === this.pairPeer.roomSecret;
|
|
const typeIsSecret = roomType === "secret";
|
|
|
|
if (!samePeerId || !sameRoomSecret || !typeIsSecret) return;
|
|
|
|
this._onPairPeerJoined(peer, roomId);
|
|
this.pairPeer = {};
|
|
}
|
|
|
|
_onPairPeerJoined(peer, roomSecret) {
|
|
const displayName = peer.name.displayName;
|
|
const deviceName = peer.name.deviceName;
|
|
|
|
PersistentStorage
|
|
.addRoomSecret(roomSecret, displayName, deviceName)
|
|
.then(_ => {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-success"));
|
|
this._evaluateNumberRoomSecrets();
|
|
})
|
|
.finally(() => {
|
|
this._cleanUp();
|
|
this.hide();
|
|
})
|
|
.catch(_ => {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-not-persistent"));
|
|
PersistentStorage.logBrowserNotCapable();
|
|
});
|
|
Events.fire('room-secret-added', {peerId: peer.id, roomSecret: roomSecret})
|
|
}
|
|
|
|
_onPublicRoomJoinKeyInvalid() {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalid"));
|
|
}
|
|
|
|
_close() {
|
|
this._pairDeviceCancel();
|
|
}
|
|
|
|
_pairDeviceCancel() {
|
|
this.hide();
|
|
this._cleanUp();
|
|
Events.fire('pair-device-cancel');
|
|
}
|
|
|
|
_onPairDeviceCanceled(pairKey) {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-key-invalidated", null, {key: pairKey}));
|
|
}
|
|
|
|
_cleanUp() {
|
|
this.roomSecret = null;
|
|
this.pairKey = null;
|
|
this.inputKeyContainer._cleanUp();
|
|
this.pairPeer = {};
|
|
}
|
|
|
|
_onSecretRoomDeleted(roomSecret) {
|
|
PersistentStorage
|
|
.deleteRoomSecret(roomSecret)
|
|
.then(_ => {
|
|
this._evaluateNumberRoomSecrets();
|
|
});
|
|
}
|
|
|
|
_evaluateNumberRoomSecrets() {
|
|
PersistentStorage
|
|
.getAllRoomSecrets()
|
|
.then(roomSecrets => {
|
|
if (roomSecrets.length > 0) {
|
|
this.$editPairedDevicesHeaderBtn.removeAttribute('hidden');
|
|
this.$footerInstructionsPairedDevices.removeAttribute('hidden');
|
|
}
|
|
else {
|
|
this.$editPairedDevicesHeaderBtn.setAttribute('hidden', true);
|
|
this.$footerInstructionsPairedDevices.setAttribute('hidden', true);
|
|
}
|
|
Events.fire('evaluate-footer-badges');
|
|
});
|
|
}
|
|
}
|
|
|
|
class EditPairedDevicesDialog extends Dialog {
|
|
constructor() {
|
|
super('edit-paired-devices-dialog');
|
|
this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');
|
|
this.$footerBadgePairedDevices = $$('.discovery-wrapper .badge-room-secret');
|
|
this.$editPairedDevices = $('edit-paired-devices');
|
|
|
|
this.$editPairedDevices.addEventListener('click', _ => this._onEditPairedDevices());
|
|
this.$footerBadgePairedDevices.addEventListener('click', _ => this._onEditPairedDevices());
|
|
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (!this.isShown()) return;
|
|
|
|
if (e.code === "Escape") {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
async _initDOM() {
|
|
const pairedDeviceRemovedString = Localization.getTranslation("dialogs.paired-device-removed");
|
|
const unpairString = Localization.getTranslation("dialogs.unpair").toUpperCase();
|
|
const autoAcceptString = Localization.getTranslation("dialogs.auto-accept").toLowerCase();
|
|
const roomSecretsEntries = await PersistentStorage.getAllRoomSecretEntries();
|
|
|
|
roomSecretsEntries
|
|
.forEach(roomSecretsEntry => {
|
|
let $pairedDevice = document.createElement('div');
|
|
$pairedDevice.classList.add("paired-device");
|
|
$pairedDevice.setAttribute('placeholder', pairedDeviceRemovedString);
|
|
|
|
$pairedDevice.innerHTML = `
|
|
<div class="display-name">
|
|
<span class="fw">
|
|
${roomSecretsEntry.display_name}
|
|
</span>
|
|
</div>
|
|
<div class="device-name">
|
|
<span class="fw">
|
|
${roomSecretsEntry.device_name}
|
|
</span>
|
|
</div>
|
|
<div class="button-wrapper row fw center wrap">
|
|
<div class="center grow">
|
|
<span class="center wrap">
|
|
${autoAcceptString}
|
|
</span>
|
|
<label class="auto-accept switch pointer m-1">
|
|
<input type="checkbox" ${roomSecretsEntry.auto_accept ? "checked" : ""}>
|
|
<div class="slider round"></div>
|
|
</label>
|
|
</div>
|
|
<button class="btn grow" type="button">${unpairString}</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');
|
|
$pairedDevice.innerText = "";
|
|
});
|
|
})
|
|
|
|
this.$pairedDevicesWrapper.appendChild($pairedDevice)
|
|
})
|
|
}
|
|
|
|
hide() {
|
|
super.hide();
|
|
setTimeout(() => {
|
|
this.$pairedDevicesWrapper.innerHTML = ""
|
|
}, 300);
|
|
}
|
|
|
|
_onEditPairedDevices() {
|
|
this._initDOM()
|
|
.then(_ => {
|
|
this._evaluateOverflowing(this.$pairedDevicesWrapper);
|
|
this.show();
|
|
});
|
|
}
|
|
|
|
_clearRoomSecrets() {
|
|
PersistentStorage
|
|
.getAllRoomSecrets()
|
|
.then(roomSecrets => {
|
|
PersistentStorage
|
|
.clearRoomSecrets()
|
|
.finally(() => {
|
|
Events.fire('room-secrets-deleted', roomSecrets);
|
|
Events.fire('evaluate-number-room-secrets');
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.pairing-cleared"));
|
|
this.hide();
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
class PublicRoomDialog extends Dialog {
|
|
constructor() {
|
|
super('public-room-dialog');
|
|
|
|
this.$key = this.$el.querySelector('.key');
|
|
this.$qrCode = this.$el.querySelector('.key-qr-code');
|
|
this.$form = this.$el.querySelector('form');
|
|
this.$closeBtn = this.$el.querySelector('[close]');
|
|
this.$leaveBtn = this.$el.querySelector('.leave-room');
|
|
this.$joinSubmitBtn = this.$el.querySelector('button[type="submit"]');
|
|
this.$headerBtnJoinPublicRoom = $('join-public-room');
|
|
this.$footerBadgePublicRoomDevices = $$('.discovery-wrapper .badge-room-public-id');
|
|
|
|
|
|
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
|
this.$closeBtn.addEventListener('click', _ => this.hide());
|
|
this.$leaveBtn.addEventListener('click', _ => this._leavePublicRoom())
|
|
|
|
this.$headerBtnJoinPublicRoom.addEventListener('click', _ => this._onHeaderBtnClick());
|
|
this.$footerBadgePublicRoomDevices.addEventListener('click', _ => this._onHeaderBtnClick());
|
|
|
|
this.inputKeyContainer = new InputKeyContainer(
|
|
this.$el.querySelector('.input-key-container'),
|
|
/[a-z|A-Z]/,
|
|
() => this.$joinSubmitBtn.removeAttribute('disabled'),
|
|
() => this.$joinSubmitBtn.setAttribute('disabled', true),
|
|
() => this._submit()
|
|
);
|
|
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
Events.on('public-room-created', e => this._onPublicRoomCreated(e.detail));
|
|
Events.on('peers', e => this._onPeers(e.detail));
|
|
Events.on('peer-joined', e => this._onPeerJoined(e.detail.peer, e.detail.roomId));
|
|
Events.on('public-room-id-invalid', e => this._onPublicRoomIdInvalid(e.detail));
|
|
Events.on('public-room-left', _ => this._onPublicRoomLeft());
|
|
this.$el.addEventListener('paste', e => this._onPaste(e));
|
|
this.$qrCode.addEventListener('click', _ => this._copyShareRoomUrl());
|
|
|
|
Events.on('ws-connected', _ => this._onWsConnected());
|
|
Events.on('translation-loaded', _ => this.setFooterBadge());
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (!this.isShown()) return;
|
|
|
|
if (e.code === "Escape") {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
_onPaste(e) {
|
|
e.preventDefault();
|
|
let pastedKey = e.clipboardData.getData("Text");
|
|
this.inputKeyContainer._onPaste(pastedKey);
|
|
}
|
|
|
|
_onHeaderBtnClick() {
|
|
if (this.roomId) {
|
|
this.show();
|
|
}
|
|
else {
|
|
this._createPublicRoom();
|
|
}
|
|
}
|
|
|
|
_createPublicRoom() {
|
|
Events.fire('create-public-room');
|
|
}
|
|
|
|
_onPublicRoomCreated(roomId) {
|
|
this.roomId = roomId;
|
|
|
|
this._setKeyAndQrCode();
|
|
|
|
this.show();
|
|
|
|
sessionStorage.setItem('public_room_id', roomId);
|
|
}
|
|
|
|
_setKeyAndQrCode() {
|
|
if (!this.roomId) return;
|
|
|
|
this.$key.innerText = this.roomId.toUpperCase();
|
|
|
|
// Display the QR code for the url
|
|
const qr = new QRCode({
|
|
content: this._getShareRoomUrl(),
|
|
width: 130,
|
|
height: 130,
|
|
padding: 1,
|
|
background: 'white',
|
|
color: 'rgb(18, 18, 18)',
|
|
ecl: "L",
|
|
join: true
|
|
});
|
|
this.$qrCode.innerHTML = qr.svg();
|
|
|
|
this.setFooterBadge();
|
|
}
|
|
|
|
setFooterBadge() {
|
|
if (!this.roomId) return;
|
|
|
|
this.$footerBadgePublicRoomDevices.innerText = Localization.getTranslation("footer.public-room-devices", null, {
|
|
roomId: this.roomId.toUpperCase()
|
|
});
|
|
this.$footerBadgePublicRoomDevices.removeAttribute('hidden');
|
|
|
|
Events.fire('evaluate-footer-badges');
|
|
}
|
|
|
|
_getShareRoomUrl() {
|
|
let url = new URL(location.href);
|
|
url.searchParams.append('room_id', this.roomId)
|
|
return url.href;
|
|
}
|
|
|
|
_copyShareRoomUrl() {
|
|
navigator.clipboard.writeText(this._getShareRoomUrl())
|
|
.then(_ => {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.room-url-copied-to-clipboard"));
|
|
})
|
|
.catch(_ => {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard-error"));
|
|
})
|
|
}
|
|
|
|
_onWsConnected() {
|
|
let roomId = sessionStorage.getItem('public_room_id');
|
|
|
|
if (!roomId) return;
|
|
|
|
this.roomId = roomId;
|
|
this._setKeyAndQrCode();
|
|
|
|
this._joinPublicRoom(roomId, true);
|
|
}
|
|
|
|
_onSubmit(e) {
|
|
e.preventDefault();
|
|
this._submit();
|
|
}
|
|
|
|
_submit() {
|
|
let inputKey = this.inputKeyContainer._getInputKey();
|
|
this._joinPublicRoom(inputKey);
|
|
}
|
|
|
|
_joinPublicRoom(roomId, createIfInvalid = false) {
|
|
roomId = roomId.toLowerCase();
|
|
if (/^[a-z]{5}$/g.test(roomId)) {
|
|
this.roomIdJoin = roomId;
|
|
|
|
this.inputKeyContainer.focusLastChar();
|
|
|
|
Events.fire('join-public-room', {
|
|
roomId: roomId,
|
|
createIfInvalid: createIfInvalid
|
|
});
|
|
}
|
|
}
|
|
|
|
_onPeers(message) {
|
|
message.peers.forEach(messagePeer => {
|
|
this._evaluateJoinedPeer(messagePeer.id, message.roomId);
|
|
});
|
|
}
|
|
|
|
_onPeerJoined(peer, roomId) {
|
|
this._evaluateJoinedPeer(peer.id, roomId);
|
|
}
|
|
|
|
_evaluateJoinedPeer(peerId, roomId) {
|
|
const isInitiatedRoomId = roomId === this.roomId;
|
|
const isJoinedRoomId = roomId === this.roomIdJoin;
|
|
|
|
if (!peerId || !roomId || (!isInitiatedRoomId && !isJoinedRoomId)) return;
|
|
|
|
this.hide();
|
|
|
|
sessionStorage.setItem('public_room_id', roomId);
|
|
|
|
if (isJoinedRoomId) {
|
|
this.roomId = roomId;
|
|
this.roomIdJoin = false;
|
|
this._setKeyAndQrCode();
|
|
}
|
|
}
|
|
|
|
_onPublicRoomIdInvalid(roomId) {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.public-room-id-invalid"));
|
|
if (roomId === sessionStorage.getItem('public_room_id')) {
|
|
sessionStorage.removeItem('public_room_id');
|
|
}
|
|
}
|
|
|
|
_leavePublicRoom() {
|
|
Events.fire('leave-public-room', this.roomId);
|
|
}
|
|
|
|
_onPublicRoomLeft() {
|
|
let publicRoomId = this.roomId.toUpperCase();
|
|
this.hide();
|
|
this._cleanUp();
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.public-room-left", null, {publicRoomId: publicRoomId}));
|
|
}
|
|
|
|
show() {
|
|
this.inputKeyContainer._enableChars();
|
|
super.show();
|
|
}
|
|
|
|
hide() {
|
|
this.inputKeyContainer._cleanUp();
|
|
super.hide();
|
|
}
|
|
|
|
_cleanUp() {
|
|
this.roomId = null;
|
|
this.inputKeyContainer._cleanUp();
|
|
sessionStorage.removeItem('public_room_id');
|
|
this.$footerBadgePublicRoomDevices.setAttribute('hidden', true);
|
|
Events.fire('evaluate-footer-badges');
|
|
}
|
|
}
|
|
|
|
class SendTextDialog extends Dialog {
|
|
constructor() {
|
|
super('send-text-dialog');
|
|
|
|
this.$text = this.$el.querySelector('.textarea');
|
|
this.$peerDisplayName = this.$el.querySelector('.display-name');
|
|
this.$form = this.$el.querySelector('form');
|
|
this.$submit = this.$el.querySelector('button[type="submit"]');
|
|
this.$form.addEventListener('submit', e => this._onSubmit(e));
|
|
this.$text.addEventListener('input', _ => this._onInput());
|
|
|
|
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (!this.isShown()) return;
|
|
|
|
if (e.code === "Escape") {
|
|
this.hide();
|
|
}
|
|
else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
if (this._textEmpty()) return;
|
|
|
|
this._send();
|
|
}
|
|
}
|
|
|
|
_textEmpty() {
|
|
return !this.$text.innerText || this.$text.innerText === "\n";
|
|
}
|
|
|
|
_onInput() {
|
|
if (this._textEmpty()) {
|
|
this.$submit.setAttribute('disabled', true);
|
|
// remove remaining whitespace on Firefox on text deletion
|
|
this.$text.innerText = "";
|
|
}
|
|
else {
|
|
this.$submit.removeAttribute('disabled');
|
|
}
|
|
this._evaluateOverflowing(this.$text);
|
|
}
|
|
|
|
_onRecipient(peerId, deviceName) {
|
|
this.correspondingPeerId = peerId;
|
|
this.$peerDisplayName.innerText = deviceName;
|
|
this.$peerDisplayName.classList.remove(...PeerUI._badgeClassNames);
|
|
this.$peerDisplayName.classList.add($(peerId).ui._badgeClassName());
|
|
|
|
this.show();
|
|
|
|
const range = document.createRange();
|
|
const sel = window.getSelection();
|
|
|
|
range.selectNodeContents(this.$text);
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
}
|
|
|
|
_onSubmit(e) {
|
|
e.preventDefault();
|
|
this._send();
|
|
}
|
|
|
|
_send() {
|
|
Events.fire('send-text', {
|
|
to: this.correspondingPeerId,
|
|
text: this.$text.innerText
|
|
});
|
|
this.hide();
|
|
setTimeout(() => this.$text.innerText = "", 300);
|
|
}
|
|
}
|
|
|
|
class ReceiveTextDialog extends Dialog {
|
|
constructor() {
|
|
super('receive-text-dialog');
|
|
Events.on('text-received', e => this._onText(e.detail.text, e.detail.peerId));
|
|
this.$text = this.$el.querySelector('#text');
|
|
this.$copy = this.$el.querySelector('#copy');
|
|
this.$close = this.$el.querySelector('#close');
|
|
|
|
this.$copy.addEventListener('click', _ => this._onCopy());
|
|
this.$close.addEventListener('click', _ => this.hide());
|
|
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
|
|
this.$displayName = this.$el.querySelector('.display-name');
|
|
this._receiveTextQueue = [];
|
|
}
|
|
|
|
selectionEmpty() {
|
|
return !window.getSelection().toString()
|
|
}
|
|
|
|
async _onKeyDown(e) {
|
|
if (!this.isShown()) return
|
|
|
|
if (e.code === "KeyC" && (e.ctrlKey || e.metaKey) && this.selectionEmpty()) {
|
|
await this._onCopy()
|
|
}
|
|
else if (e.code === "Escape") {
|
|
this.hide();
|
|
}
|
|
}
|
|
|
|
_onText(text, peerId) {
|
|
audioPlayer.playBlop();
|
|
this._receiveTextQueue.push({text: text, peerId: peerId});
|
|
this._setDocumentTitleMessages();
|
|
if (this.isShown()) return;
|
|
this._dequeueRequests();
|
|
}
|
|
|
|
_dequeueRequests() {
|
|
if (!this._receiveTextQueue.length) return;
|
|
let {text, peerId} = this._receiveTextQueue.shift();
|
|
this._showReceiveTextDialog(text, peerId);
|
|
}
|
|
|
|
_showReceiveTextDialog(text, peerId) {
|
|
this.$displayName.innerText = $(peerId).ui._displayName();
|
|
this.$displayName.classList.remove(...PeerUI._badgeClassNames);
|
|
this.$displayName.classList.add($(peerId).ui._badgeClassName());
|
|
|
|
this.$text.innerText = text;
|
|
this.$text.classList.remove('text-center');
|
|
|
|
// Beautify text if text is short
|
|
if (text.length < 2000) {
|
|
// replace URLs with actual links
|
|
this.$text.innerHTML = this.$text.innerHTML
|
|
.replace(/(^|<br>|\s|")((https?:\/\/|www.)(([a-z]|[A-Z]|[0-9]|[\-_~:\/?#\[\]@!$&'()*+,;=%]){2,}\.)(([a-z]|[A-Z]|[0-9]|[\-_~:\/?#\[\]@!$&'()*+,;=%.]){2,}))/g,
|
|
(match, whitespace, url) => {
|
|
let link = url;
|
|
|
|
// prefix www.example.com with http protocol to prevent it from being a relative link
|
|
if (link.startsWith('www')) {
|
|
link = "http://" + link
|
|
}
|
|
|
|
// Check if link is valid
|
|
if (isUrlValid(link)) {
|
|
return `${whitespace}<a href="${link}" target="_blank">${url}</a>`;
|
|
}
|
|
else {
|
|
return match;
|
|
}
|
|
});
|
|
}
|
|
|
|
this._evaluateOverflowing(this.$text);
|
|
|
|
this._setDocumentTitleMessages();
|
|
|
|
changeFavicon("images/favicon-96x96-notification.png");
|
|
this.show();
|
|
}
|
|
|
|
_setDocumentTitleMessages() {
|
|
document.title = !this._receiveTextQueue.length
|
|
? `${ Localization.getTranslation("document-titles.message-received") } - PairDrop`
|
|
: `${ Localization.getTranslation("document-titles.message-received-plural", null, {count: this._receiveTextQueue.length + 1}) } - PairDrop`;
|
|
}
|
|
|
|
async _onCopy() {
|
|
const sanitizedText = this.$text.innerText.replace(/\u00A0/gm, ' ');
|
|
navigator.clipboard
|
|
.writeText(sanitizedText)
|
|
.then(_ => {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard"));
|
|
this.hide();
|
|
})
|
|
.catch(_ => {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.copied-to-clipboard-error"));
|
|
});
|
|
}
|
|
|
|
hide() {
|
|
super.hide();
|
|
setTimeout(() => {
|
|
this._dequeueRequests();
|
|
this.$text.innerHTML = "";
|
|
}, 500);
|
|
}
|
|
}
|
|
|
|
class ShareTextDialog extends Dialog {
|
|
constructor() {
|
|
super('share-text-dialog');
|
|
|
|
this.$text = this.$el.querySelector('.textarea');
|
|
this.$approveMsgBtn = this.$el.querySelector('button[type="submit"]');
|
|
this.$checkbox = this.$el.querySelector('input[type="checkbox"]')
|
|
|
|
this.$approveMsgBtn.addEventListener('click', _ => this._approveShareText());
|
|
|
|
// Only show this per default if user sets checkmark
|
|
this.$checkbox.checked = localStorage.getItem('approve-share-text')
|
|
? ShareTextDialog.isApproveShareTextSet()
|
|
: false;
|
|
|
|
this._setCheckboxValueToLocalStorage();
|
|
|
|
this.$checkbox.addEventListener('change', _ => this._setCheckboxValueToLocalStorage());
|
|
Events.on('share-text-dialog', e => this._onShareText(e.detail));
|
|
Events.on('keydown', e => this._onKeyDown(e));
|
|
this.$text.addEventListener('input', _ => this._evaluateEmptyText());
|
|
}
|
|
|
|
static isApproveShareTextSet() {
|
|
return localStorage.getItem('approve-share-text') === "true";
|
|
}
|
|
|
|
_setCheckboxValueToLocalStorage() {
|
|
localStorage.setItem('approve-share-text', this.$checkbox.checked ? "true" : "false");
|
|
}
|
|
|
|
_onKeyDown(e) {
|
|
if (!this.isShown()) return;
|
|
|
|
if (e.code === "Escape") {
|
|
this._approveShareText();
|
|
}
|
|
else if (e.code === "Enter" && (e.ctrlKey || e.metaKey)) {
|
|
if (this._textEmpty()) return;
|
|
|
|
this._approveShareText();
|
|
}
|
|
}
|
|
|
|
_textEmpty() {
|
|
return !this.$text.innerText || this.$text.innerText === "\n";
|
|
}
|
|
|
|
_evaluateEmptyText() {
|
|
if (this._textEmpty()) {
|
|
this.$approveMsgBtn.setAttribute('disabled', true);
|
|
// remove remaining whitespace on Firefox on text deletion
|
|
this.$text.innerText = "";
|
|
}
|
|
else {
|
|
this.$approveMsgBtn.removeAttribute('disabled');
|
|
}
|
|
this._evaluateOverflowing(this.$text);
|
|
}
|
|
|
|
_onShareText(text) {
|
|
this.$text.innerText = text;
|
|
this._evaluateEmptyText();
|
|
this.show();
|
|
}
|
|
|
|
_approveShareText() {
|
|
Events.fire('activate-share-mode', {text: this.$text.innerText});
|
|
this.hide();
|
|
}
|
|
|
|
hide() {
|
|
super.hide();
|
|
setTimeout(() => this.$text.innerText = "", 500);
|
|
}
|
|
}
|
|
|
|
class Base64Dialog extends Dialog {
|
|
|
|
constructor() {
|
|
super('base64-paste-dialog');
|
|
|
|
this.$title = this.$el.querySelector('.dialog-title');
|
|
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
|
|
this.$fallbackTextarea = this.$el.querySelector('.textarea');
|
|
}
|
|
|
|
async evaluateBase64Text(base64Text, hash) {
|
|
this.$title.innerText = Localization.getTranslation('dialogs.base64-title-text');
|
|
|
|
if (base64Text === 'paste') {
|
|
// ?base64text=paste
|
|
// base64 encoded string is ready to be pasted from clipboard
|
|
this.preparePasting('text');
|
|
this.show();
|
|
}
|
|
else if (base64Text === 'hash') {
|
|
// ?base64text=hash#BASE64ENCODED
|
|
// base64 encoded text is url hash which cannot be seen by the server and is faster (recommended)
|
|
this.show();
|
|
await this.processBase64Text(hash);
|
|
}
|
|
else {
|
|
// ?base64text=BASE64ENCODED
|
|
// base64 encoded text is part of the url param. Seen by server and slow (not recommended)
|
|
this.show();
|
|
await this.processBase64Text(base64Text);
|
|
}
|
|
}
|
|
|
|
async evaluateBase64Zip(base64Zip, hash) {
|
|
this.$title.innerText = Localization.getTranslation('dialogs.base64-title-files');
|
|
|
|
if (base64Zip === 'paste') {
|
|
// ?base64zip=paste || ?base64zip=true
|
|
this.preparePasting('files');
|
|
this.show();
|
|
}
|
|
else if (base64Zip === 'hash') {
|
|
// ?base64zip=hash#BASE64ENCODED
|
|
// base64 encoded zip file is url hash which cannot be seen by the server
|
|
await this.processBase64Zip(hash);
|
|
}
|
|
}
|
|
|
|
_setPasteBtnToProcessing() {
|
|
this.$pasteBtn.style.pointerEvents = "none";
|
|
this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-processing");
|
|
}
|
|
|
|
preparePasting(type) {
|
|
const translateType = type === 'text'
|
|
? Localization.getTranslation("dialogs.base64-text")
|
|
: Localization.getTranslation("dialogs.base64-files");
|
|
|
|
if (navigator.clipboard.readText) {
|
|
this.$pasteBtn.innerText = Localization.getTranslation("dialogs.base64-tap-to-paste", null, {type: translateType});
|
|
this._clickCallback = _ => this.processClipboard(type);
|
|
this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
|
|
}
|
|
else {
|
|
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
|
|
this.$pasteBtn.setAttribute('hidden', true);
|
|
this.$fallbackTextarea.setAttribute('placeholder', Localization.getTranslation("dialogs.base64-paste-to-send", null, {type: translateType}));
|
|
this.$fallbackTextarea.removeAttribute('hidden');
|
|
this._inputCallback = _ => this.processInput(type);
|
|
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
|
|
this.$fallbackTextarea.focus();
|
|
}
|
|
}
|
|
|
|
async processInput(type) {
|
|
const base64 = this.$fallbackTextarea.textContent;
|
|
this.$fallbackTextarea.textContent = '';
|
|
await this.processPastedBase64(type, base64);
|
|
}
|
|
|
|
async processClipboard(type) {
|
|
const base64 = await navigator.clipboard.readText();
|
|
await this.processPastedBase64(type, base64);
|
|
}
|
|
|
|
async processPastedBase64(type, base64) {
|
|
try {
|
|
if (type === 'text') {
|
|
await this.processBase64Text(base64);
|
|
}
|
|
else {
|
|
await this.processBase64Zip(base64);
|
|
}
|
|
}
|
|
catch(e) {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect"));
|
|
console.log("Clipboard content is incorrect.")
|
|
}
|
|
this.hide();
|
|
}
|
|
|
|
async processBase64Text(base64){
|
|
this._setPasteBtnToProcessing();
|
|
|
|
try {
|
|
const decodedText = await decodeBase64Text(base64);
|
|
if (ShareTextDialog.isApproveShareTextSet()) {
|
|
Events.fire('share-text-dialog', decodedText);
|
|
}
|
|
else {
|
|
Events.fire('activate-share-mode', {text: decodedText});
|
|
}
|
|
}
|
|
catch (e) {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
|
|
console.log("Text content incorrect.");
|
|
}
|
|
|
|
this.hide();
|
|
}
|
|
|
|
async processBase64Zip(base64) {
|
|
this._setPasteBtnToProcessing();
|
|
|
|
try {
|
|
const decodedFiles = await decodeBase64Files(base64);
|
|
Events.fire('activate-share-mode', {files: decodedFiles});
|
|
}
|
|
catch (e) {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect"));
|
|
console.log("File content incorrect.");
|
|
}
|
|
|
|
this.hide();
|
|
}
|
|
|
|
hide() {
|
|
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
|
|
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
|
|
this.$fallbackTextarea.setAttribute('disabled', true);
|
|
this.$fallbackTextarea.blur();
|
|
super.hide();
|
|
}
|
|
}
|
|
|
|
class AboutUI {
|
|
constructor() {
|
|
this.$donationBtn = $('donation-btn');
|
|
this.$twitterBtn = $('twitter-btn');
|
|
this.$mastodonBtn = $('mastodon-btn');
|
|
this.$blueskyBtn = $('bluesky-btn');
|
|
this.$customBtn = $('custom-btn');
|
|
this.$privacypolicyBtn = $('privacypolicy-btn');
|
|
Events.on('config', e => this._onConfig(e.detail.buttons));
|
|
}
|
|
|
|
async _onConfig(btnConfig) {
|
|
await this._evaluateBtnConfig(this.$donationBtn, btnConfig.donation_button);
|
|
await this._evaluateBtnConfig(this.$twitterBtn, btnConfig.twitter_button);
|
|
await this._evaluateBtnConfig(this.$mastodonBtn, btnConfig.mastodon_button);
|
|
await this._evaluateBtnConfig(this.$blueskyBtn, btnConfig.bluesky_button);
|
|
await this._evaluateBtnConfig(this.$customBtn, btnConfig.custom_button);
|
|
await this._evaluateBtnConfig(this.$privacypolicyBtn, btnConfig.privacypolicy_button);
|
|
}
|
|
|
|
async _evaluateBtnConfig($btn, config) {
|
|
// if config is not set leave everything as default
|
|
if (!Object.keys(config).length) return;
|
|
|
|
if (config.active === "false") {
|
|
$btn.setAttribute('hidden', true);
|
|
} else {
|
|
if (config.link) {
|
|
$btn.setAttribute('href', config.link);
|
|
}
|
|
if (config.title) {
|
|
$btn.setAttribute('title', config.title);
|
|
// prevent overwriting of custom title when setting different language
|
|
$btn.removeAttribute('data-i18n-key');
|
|
$btn.removeAttribute('data-i18n-attrs');
|
|
}
|
|
if (config.icon) {
|
|
$btn.setAttribute('title', config.title);
|
|
// prevent overwriting of custom title when setting different language
|
|
$btn.removeAttribute('data-i18n-key');
|
|
$btn.removeAttribute('data-i18n-attrs');
|
|
}
|
|
$btn.removeAttribute('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
class Toast extends Dialog {
|
|
constructor() {
|
|
super('toast');
|
|
this.$closeBtn = this.$el.querySelector('.icon-button');
|
|
this.$text = this.$el.querySelector('span');
|
|
|
|
this.$closeBtn.addEventListener('click', _ => this.hide());
|
|
Events.on('notify-user', e => this._onNotify(e.detail));
|
|
Events.on('share-mode-changed', _ => this.hide());
|
|
}
|
|
|
|
_onNotify(message) {
|
|
if (this.hideTimeout) clearTimeout(this.hideTimeout);
|
|
this.$text.innerText = typeof message === "object" ? message.message : message;
|
|
this.show();
|
|
|
|
if (typeof message === "object" && message.persistent) return;
|
|
|
|
this.hideTimeout = setTimeout(() => this.hide(), 5000);
|
|
}
|
|
|
|
hide() {
|
|
if (this.hideTimeout) clearTimeout(this.hideTimeout);
|
|
super.hide();
|
|
}
|
|
}
|
|
|
|
class Notifications {
|
|
|
|
constructor() {
|
|
// Check if the browser supports notifications
|
|
if (!('Notification' in window)) return;
|
|
|
|
this.$headerNotificationButton = $('notification');
|
|
this.$downloadBtn = $('download-btn');
|
|
|
|
this.$headerNotificationButton.addEventListener('click', _ => this._requestPermission());
|
|
|
|
|
|
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-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
|
|
}
|
|
|
|
async _requestPermission() {
|
|
await Notification.
|
|
requestPermission(permission => {
|
|
if (permission !== 'granted') {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.notifications-permissions-error"));
|
|
return;
|
|
}
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.notifications-enabled"));
|
|
this.$headerNotificationButton.setAttribute('hidden', true);
|
|
});
|
|
}
|
|
|
|
_notify(title, body) {
|
|
const config = {
|
|
body: body,
|
|
icon: '/images/logo_transparent_128x128.png',
|
|
}
|
|
let notification;
|
|
try {
|
|
notification = new Notification(title, config);
|
|
} catch (e) {
|
|
// Android doesn't support "new Notification" if service worker is installed
|
|
if (!serviceWorker || !serviceWorker.showNotification) return;
|
|
notification = serviceWorker.showNotification(title, config);
|
|
}
|
|
|
|
// Notification is persistent on Android. We have to close it manually
|
|
const visibilitychangeHandler = () => {
|
|
if (document.visibilityState === 'visible') {
|
|
notification.close();
|
|
Events.off('visibilitychange', visibilitychangeHandler);
|
|
}
|
|
};
|
|
Events.on('visibilitychange', visibilitychangeHandler);
|
|
|
|
return notification;
|
|
}
|
|
|
|
_messageNotification(message, peerId) {
|
|
if (document.visibilityState !== 'visible') {
|
|
const peerDisplayName = $(peerId).ui._displayName();
|
|
if (/^((https?:\/\/|www)[abcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)$/.test(message.toLowerCase())) {
|
|
const notification = this._notify(Localization.getTranslation("notifications.link-received", null, {name: peerDisplayName}), message);
|
|
this._bind(notification, _ => window.open(message, '_blank', "noreferrer"));
|
|
}
|
|
else {
|
|
const notification = this._notify(Localization.getTranslation("notifications.message-received", null, {name: peerDisplayName}), message);
|
|
this._bind(notification, _ => this._copyText(message, notification));
|
|
}
|
|
}
|
|
}
|
|
|
|
_downloadNotification(files) {
|
|
if (document.visibilityState !== 'visible') {
|
|
let imagesOnly = files.every(file => file.type.split('/')[0] === 'image');
|
|
let title;
|
|
|
|
if (files.length === 1) {
|
|
title = `${files[0].name}`;
|
|
}
|
|
else {
|
|
let fileOther;
|
|
if (files.length === 2) {
|
|
fileOther = imagesOnly
|
|
? Localization.getTranslation("dialogs.file-other-description-image")
|
|
: Localization.getTranslation("dialogs.file-other-description-file");
|
|
}
|
|
else {
|
|
fileOther = imagesOnly
|
|
? Localization.getTranslation("dialogs.file-other-description-image-plural", null, {count: files.length - 1})
|
|
: Localization.getTranslation("dialogs.file-other-description-file-plural", null, {count: files.length - 1});
|
|
}
|
|
title = `${files[0].name} ${fileOther}`
|
|
}
|
|
const notification = this._notify(title, Localization.getTranslation("notifications.click-to-download"));
|
|
this._bind(notification, _ => this._download(notification));
|
|
}
|
|
}
|
|
|
|
_requestNotification(request, peerId) {
|
|
if (document.visibilityState !== 'visible') {
|
|
let imagesOnly = request.header.every(header => header.mime.split('/')[0] === 'image');
|
|
let displayName = $(peerId).querySelector('.name').textContent;
|
|
|
|
let descriptor;
|
|
if (request.header.length === 1) {
|
|
descriptor = imagesOnly
|
|
? Localization.getTranslation("dialogs.title-image")
|
|
: Localization.getTranslation("dialogs.title-file");
|
|
}
|
|
else {
|
|
descriptor = imagesOnly
|
|
? Localization.getTranslation("dialogs.title-image-plural")
|
|
: Localization.getTranslation("dialogs.title-file-plural");
|
|
}
|
|
|
|
let title = Localization
|
|
.getTranslation("notifications.request-title", null, {
|
|
name: displayName,
|
|
count: request.header.length,
|
|
descriptor: descriptor.toLowerCase()
|
|
});
|
|
|
|
const notification = this._notify(title, Localization.getTranslation("notifications.click-to-show"));
|
|
}
|
|
}
|
|
|
|
_download(notification) {
|
|
this.$downloadBtn.click();
|
|
notification.close();
|
|
}
|
|
|
|
async _copyText(message, notification) {
|
|
if (await navigator.clipboard.writeText(message)) {
|
|
notification.close();
|
|
this._notify(Localization.getTranslation("notifications.copied-text"));
|
|
}
|
|
else {
|
|
this._notify(Localization.getTranslation("notifications.copied-text-error"));
|
|
}
|
|
}
|
|
|
|
_bind(notification, handler) {
|
|
if (notification.then) {
|
|
notification.then(_ => {
|
|
serviceWorker
|
|
.getNotifications()
|
|
.then(_ => {
|
|
serviceWorker.addEventListener('notificationclick', handler);
|
|
})
|
|
});
|
|
}
|
|
else {
|
|
notification.onclick = handler;
|
|
}
|
|
}
|
|
}
|
|
|
|
class NetworkStatusUI {
|
|
|
|
constructor() {
|
|
Events.on('offline', _ => this._showOfflineMessage());
|
|
Events.on('online', _ => this._showOnlineMessage());
|
|
if (!navigator.onLine) this._showOfflineMessage();
|
|
}
|
|
|
|
_showOfflineMessage() {
|
|
Events.fire('notify-user', {
|
|
message: Localization.getTranslation("notifications.offline"),
|
|
persistent: true
|
|
});
|
|
}
|
|
|
|
_showOnlineMessage() {
|
|
Events.fire('notify-user', Localization.getTranslation("notifications.online"));
|
|
}
|
|
}
|
|
|
|
class WebShareTargetUI {
|
|
|
|
async evaluateShareTarget(shareTargetType, title, text, url) {
|
|
if (shareTargetType === "text") {
|
|
let shareTargetText;
|
|
if (url) {
|
|
shareTargetText = url; // we share only the link - no text.
|
|
}
|
|
else if (title && text) {
|
|
shareTargetText = title + '\r\n' + text;
|
|
}
|
|
else {
|
|
shareTargetText = title + text;
|
|
}
|
|
|
|
if (ShareTextDialog.isApproveShareTextSet()) {
|
|
Events.fire('share-text-dialog', shareTargetText);
|
|
}
|
|
else {
|
|
Events.fire('activate-share-mode', {text: shareTargetText});
|
|
}
|
|
}
|
|
else if (shareTargetType === "files") {
|
|
let openRequest = window.indexedDB.open('pairdrop_store')
|
|
openRequest.onsuccess = e => {
|
|
const db = e.target.result;
|
|
const tx = db.transaction('share_target_files', 'readwrite');
|
|
const store = tx.objectStore('share_target_files');
|
|
const request = store.getAll();
|
|
request.onsuccess = _ => {
|
|
const fileObjects = request.result;
|
|
|
|
let filesReceived = [];
|
|
for (let i = 0; i < fileObjects.length; i++) {
|
|
filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name));
|
|
}
|
|
|
|
const clearRequest = store.clear()
|
|
clearRequest.onsuccess = _ => db.close();
|
|
|
|
Events.fire('activate-share-mode', {files: filesReceived})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep for legacy reasons even though this is removed from new PWA installations
|
|
class WebFileHandlersUI {
|
|
async evaluateLaunchQueue() {
|
|
if (!"launchQueue" in window) return;
|
|
|
|
launchQueue.setConsumer(async launchParams => {
|
|
console.log("Launched with: ", launchParams);
|
|
|
|
if (!launchParams.files.length) return;
|
|
|
|
let files = [];
|
|
|
|
for (let i = 0; i < launchParams.files.length; i++) {
|
|
if (i !== 0 && await launchParams.files[i].isSameEntry(launchParams.files[i-1])) continue;
|
|
|
|
const file = await launchParams.files[i].getFile();
|
|
files.push(file);
|
|
}
|
|
|
|
Events.fire('activate-share-mode', {files: files})
|
|
});
|
|
}
|
|
}
|
|
|
|
class NoSleepUI {
|
|
constructor() {
|
|
NoSleepUI._nosleep = new NoSleep();
|
|
}
|
|
|
|
static enable() {
|
|
if (!NoSleepUI._interval) {
|
|
NoSleepUI._nosleep.enable();
|
|
// Disable after 10s if all peers are idle
|
|
NoSleepUI._interval = setInterval(() => NoSleepUI.disableIfPeersIdle(), 10000);
|
|
}
|
|
}
|
|
|
|
static disableIfPeersIdle() {
|
|
if ($$('x-peer[status]') === null) {
|
|
clearInterval(NoSleepUI._interval);
|
|
NoSleepUI._nosleep.disable();
|
|
}
|
|
}
|
|
}
|