PairDrop/public/scripts/ui.js

2718 lines
94 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.peers = {};
this.shareMode = {};
this.shareMode.active = false;
this.shareMode.descriptor = "";
this.shareMode.files = [];
this.shareMode.text = "";
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
Events.on('peer-added', _ => this._evaluateOverflowingPeers());
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash));
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));
this.$shareModeCancelBtn.addEventListener('click', _ => this._deactivateShareMode());
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
Events.on('ws-config', e => this._evaluateRtcSupport(e.detail))
}
_evaluateRtcSupport(wsConfig) {
if (wsConfig.wsFallback) {
this.$wsFallbackWarning.hidden = false;
}
else {
this.$wsFallbackWarning.hidden = true;
if (!window.isRtcSupported) {
alert(Localization.getTranslation("instructions.webrtc-requirement"));
}
}
}
_changePeerDisplayName(peerId, displayName) {
this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
this._redrawPeerRoomTypes(peerId);
}
_onPeerDisplayNameChanged(e) {
if (!e.detail.displayName) return;
this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);
}
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(msg) {
this._joinPeer(msg.peer, msg.roomType, msg.roomId);
}
_joinPeer(peer, roomType, roomId) {
const existingPeer = this.peers[peer.id];
if (existingPeer) {
// peer already exists. Abort but add roomType to GUI
existingPeer._roomIds[roomType] = roomId;
this._redrawPeerRoomTypes(peer.id);
return;
}
peer._isSameBrowser = () => BrowserTabsConnector.peerIsSameBrowser(peer.id);
peer._roomIds = {};
peer._roomIds[roomType] = roomId;
this.peers[peer.id] = peer;
}
_onPeerConnected(peerId, connectionHash) {
if (!this.peers[peerId] || $(peerId)) return;
const peer = this.peers[peerId];
new PeerUI(peer, connectionHash, {
active: this.shareMode.active,
descriptor: this.shareMode.descriptor,
});
}
_redrawPeerRoomTypes(peerId) {
const peer = this.peers[peerId];
const peerNode = $(peerId);
if (!peer || !peerNode) return;
peerNode.classList.remove('type-ip', 'type-secret', 'type-public-id', 'type-same-browser');
if (peer._isSameBrowser()) {
peerNode.classList.add(`type-same-browser`);
}
Object.keys(peer._roomIds).forEach(roomType => peerNode.classList.add(`type-${roomType}`));
}
_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 $peer = $(peerId);
if (!$peer) return;
$peer.remove();
this._evaluateOverflowingPeers();
}
_onRoomTypeRemoved(peerId, roomType) {
const peer = this.peers[peerId];
if (!peer) return;
delete peer._roomIds[roomType];
this._redrawPeerRoomTypes(peerId)
}
_onSetProgress(progress) {
const $peer = $(progress.peerId);
if (!$peer) return;
$peer.ui.setProgress(progress.progress, progress.status)
}
_onDrop(e) {
if (this.shareMode.active || Dialog.anyDialogShown()) return;
e.preventDefault();
this._onDragEnd();
if ($$('x-peer') || !$$('x-peer').contains(e.target)) return; // dropped on peer
const files = e.dataTransfer.files;
const text = e.dataTransfer.getData("text");
if (files.length > 0) {
Events.fire('activate-share-mode', {
files: files
});
}
else if(text.length > 0) {
Events.fire('activate-share-mode', {
text: text
});
}
}
_onDragOver(e) {
if (this.shareMode.active || Dialog.anyDialogShown()) return;
e.preventDefault();
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"];
static _shareMode = {
active: false,
descriptor: ""
};
constructor(peer, connectionHash, shareMode) {
this.$xInstructions = $$('x-instructions');
this.$xPeers = $$('x-peers');
this._peer = peer;
this._connectionHash =
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
// This is needed if the ShareMode is started BEFORE the PeerUI is drawn.
PeerUI._shareMode = shareMode;
this._initDom();
this.$xPeers.appendChild(this.$el);
Events.fire('peer-added');
// ShareMode
Events.on('share-mode-changed', e => this._onShareModeChanged(e.detail.active, e.detail.descriptor));
}
html() {
let title= PeerUI._shareMode.active
? Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: PeerUI._shareMode.descriptor})
: 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();
this.$label = this.$el.querySelector('label');
this.$input = this.$el.querySelector('input');
}
addTypesToClassList() {
if (this._peer._isSameBrowser()) {
this.$el.classList.add(`type-same-browser`);
}
Object.keys(this._peer._roomIds).forEach(roomType => this.$el.classList.add(`type-${roomType}`));
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer');
}
_initDom() {
this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id;
this.$el.ui = this;
this.$el.classList.add('center');
this.addTypesToClassList();
this.html();
this._createCallbacks();
this._evaluateShareMode();
this._bindListeners();
}
_onShareModeChanged(active = false, descriptor = "") {
// This is needed if the ShareMode is started AFTER the PeerUI is drawn.
PeerUI._shareMode.active = active;
PeerUI._shareMode.descriptor = descriptor;
this._evaluateShareMode();
this._bindListeners();
}
_evaluateShareMode() {
let title;
if (!PeerUI._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: PeerUI._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(!PeerUI._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
});
}
_displayName() {
return this._peer.name.displayName;
}
_deviceName() {
return this._peer.name.deviceName;
}
_badgeClassName() {
const roomTypes = Object.keys(this._peer._roomIds);
return roomTypes.includes('secret')
? 'badge-room-secret'
: roomTypes.includes('ip')
? 'badge-room-ip'
: '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;
if (files.length === 0) return;
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) {
if (status !== this.currentStatus) {
let statusName = {
"prepare": Localization.getTranslation("peer-ui.preparing"),
"transfer": Localization.getTranslation("peer-ui.transferring"),
"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;
}
}
else {
this.$el.removeAttribute('status');
this.$el.querySelector('.status').innerHTML = '';
progress = 0;
this.currentStatus = null;
}
const degrees = `rotate(${360 * progress}deg)`;
$progress.style.setProperty('--progress', degrees);
}
_onDrop(e) {
if (PeerUI._shareMode.active || Dialog.anyDialogShown()) return;
e.preventDefault();
this._onDragEnd();
const peerId = this._peer.id;
const files = e.dataTransfer.files;
const text = e.dataTransfer.getData("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
});
}
}
_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 = 1024 MB = 1024^2 KB = 1024^3 B
// 1024^2 = 104876; 1024^3 = 1073741824
if (bytes >= 1073741824) {
return Math.round(10 * bytes / 1073741824) / 10 + ' GB';
}
else if (bytes >= 1048576) {
return Math.round(bytes / 1048576) + ' MB';
}
else if (bytes > 1024) {
return Math.round(bytes / 1024) + ' 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
});
window.blop.play();
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));
Events.on('peer-joined', e => this._onPeerJoined(e.detail));
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(message) {
message.peers.forEach(messagePeer => {
this._evaluateJoinedPeer(messagePeer.id, message.roomType, message.roomId);
});
}
_onPeerJoined(message) {
this._evaluateJoinedPeer(message.peer.id, message.roomType, message.roomId);
}
_evaluateJoinedPeer(peerId, roomType, roomId) {
const noPairPeerSaved = !Object.keys(this.pairPeer);
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(peerId, roomId);
this.pairPeer = {};
}
_onPairPeerJoined(peerId, roomSecret) {
// if devices are paired that are already connected we must save the names at this point
const $peer = $(peerId);
let displayName, deviceName;
if ($peer) {
displayName = $peer.ui._peer.name.displayName;
deviceName = $peer.ui._peer.name.deviceName;
}
PersistentStorage
.addRoomSecret(roomSecret, displayName, deviceName)
.then(_ => {
Events.fire('notify-user', 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();
});
}
_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');
$('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices());
this.$footerBadgePairedDevices.addEventListener('click', _ => this._onEditPairedDevices());
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e));
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();
})
});
}
_onPeerDisplayNameChanged(e) {
const peerId = e.detail.peerId;
const peerNode = $(peerId);
if (!peerNode) return;
const peer = peerNode.ui._peer;
if (!peer || !peer._roomIds["secret"]) return;
PersistentStorage
.updateRoomSecretNames(peer._roomIds["secret"], peer.name.displayName, peer.name.deviceName)
.then(roomSecretEntry => {
console.log(`Successfully updated DisplayName and DeviceName for roomSecretEntry ${roomSecretEntry.key}`);
})
}
}
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));
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(message) {
this._evaluateJoinedPeer(message.peer.id, message.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());
this.$text.addEventListener('paste', e => this._onPaste(e));
this.$text.addEventListener('drop', e => this._onDrop(e));
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();
}
}
async _onDrop(e) {
e.preventDefault()
const text = e.dataTransfer.getData("text");
const selection = window.getSelection();
if (selection.rangeCount) {
selection.deleteFromDocument();
selection.getRangeAt(0).insertNode(document.createTextNode(text));
}
this._onInput();
}
async _onPaste(e) {
e.preventDefault()
const text = (e.clipboardData || window.clipboardData).getData('text');
const selection = window.getSelection();
if (selection.rangeCount) {
selection.deleteFromDocument();
const textNode = document.createTextNode(text);
const range = document.createRange();
range.setStart(textNode, textNode.length);
range.collapse(true);
selection.getRangeAt(0).insertNode(textNode);
selection.removeAllRanges();
selection.addRange(range);
}
this._onInput();
}
_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 = [];
this._hideTimeout = null;
}
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) {
window.blop.play();
this._receiveTextQueue.push({text: text, peerId: peerId});
this._setDocumentTitleMessages();
changeFavicon("images/favicon-96x96-notification.png");
if (this.isShown() || this._hideTimeout) return;
this._dequeueRequests();
}
_dequeueRequests() {
this._setDocumentTitleMessages();
changeFavicon("images/favicon-96x96-notification.png");
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;
// Beautify text if text is not too long
if (this.$text.innerText.length <= 300000) {
// Hacky workaround to replace URLs with link nodes in all cases
// 1. Use text variable, find all valid URLs via regex and replace URLs with placeholder
// 2. Use html variable, find placeholders with regex and replace them with link nodes
let $textShadow = document.createElement('div');
$textShadow.innerText = text;
let linkNodes = {};
let searchHTML = $textShadow.innerHTML;
const p = "@";
const pRgx = new RegExp(`${p}\\d+`, 'g');
let occP = searchHTML.match(pRgx) || [];
let m = 0;
const allowedDomainChars = "a-zA-Z0-9áàäčçđéèêŋńñóòôöšŧüžæøåëìíîïðùúýþćěłřśţźǎǐǒǔǥǧǩǯəʒâûœÿãõāēīōūăąĉċďĕėęĝğġģĥħĩĭįıĵķĸĺļľņňŏőŕŗŝşťũŭůűųŵŷżאבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ";
const urlRgx = new RegExp(`(^|\\n|\\s|["><\\-_~:\\/?#\\[\\]@!$&'()*+,;=%.])(((https?:\\/\\/)?(?:[${allowedDomainChars}](?:[${allowedDomainChars}-]{0,61}[${allowedDomainChars}])?\\.)+[${allowedDomainChars}][${allowedDomainChars}-]{0,61}[${allowedDomainChars}])(:?\\d*)\\/?([${allowedDomainChars}_\\/\\-#.]*)(\\?([${allowedDomainChars}\\-_~:\\/?#\\[\\]@!$&'()*+,;=%.]*))?)`, 'g');
$textShadow.innerText = text.replace(urlRgx,
(match, whitespaceOrSpecial, url, g3, scheme) => {
let link = url;
// prefix www.example.com with http protocol to prevent it from being a relative link
if (!scheme && link.startsWith('www')) {
link = "http://" + link
}
if (isUrlValid(link)) {
// link is valid -> replace with link node placeholder
// find linkNodePlaceholder that is not yet present in text node
m++;
while (occP.includes(`${p}${m}`)) {
m++;
}
let linkNodePlaceholder = `${p}${m}`;
// add linkNodePlaceholder to text node and save a reference to linkNodes object
linkNodes[linkNodePlaceholder] = `<a href="${link}" target="_blank">${url}</a>`;
return `${whitespaceOrSpecial}${linkNodePlaceholder}`;
}
// link is not valid -> do not replace
return match;
});
this.$text.innerHTML = $textShadow.innerHTML.replace(pRgx,
(m) => {
let urlNode = linkNodes[m];
return urlNode ? urlNode : m;
});
}
this._evaluateOverflowing(this.$text);
this.show();
}
_setDocumentTitleMessages() {
document.title = this._receiveTextQueue.length <= 1
? `${ 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();
// If queue is empty -> clear text field | else -> open next message
this._hideTimeout = setTimeout(() => {
if (!this._receiveTextQueue.length) {
this.$text.innerHTML = "";
}
else {
this._dequeueRequests();
}
this._hideTimeout = null;
}, 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 (!this._interval) {
NoSleepUI._nosleep.enable();
NoSleepUI._interval = setInterval(() => NoSleepUI.disable(), 10000);
}
}
static disable() {
if ($$('x-peer[status]') === null) {
clearInterval(NoSleepUI._interval);
NoSleepUI._nosleep.disable();
}
}
}