mirror of
https://github.com/schlagmichdoch/PairDrop.git
synced 2025-04-20 15:06:15 -04:00
2718 lines
94 KiB
JavaScript
2718 lines
94 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.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();
|
||
}
|
||
}
|
||
}
|