Implement new status 'connecting', automatic reconnect on disconnect and auto resume of transfer + sending of queued messages. (fixes #260 and #247)

This commit is contained in:
schlagmichdoch 2024-02-04 18:02:10 +01:00
parent b36105b1cf
commit f22abca783
3 changed files with 589 additions and 316 deletions

View file

@ -176,9 +176,11 @@
"click-to-send-share-mode": "Click to send {{descriptor}}", "click-to-send-share-mode": "Click to send {{descriptor}}",
"click-to-send": "Click to send files or right click to send a message", "click-to-send": "Click to send files or right click to send a message",
"connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices", "connection-hash": "To verify the security of the end-to-end encryption, compare this security number on both devices",
"connecting": "Connecting…",
"preparing": "Preparing…", "preparing": "Preparing…",
"waiting": "Waiting…", "waiting": "Waiting…",
"processing": "Processing…", "processing": "Processing…",
"transferring": "Transferring…" "transferring": "Transferring…",
"receiving": "Receiving…"
} }
} }

View file

@ -88,7 +88,9 @@ class ServerConnection {
_onOpen() { _onOpen() {
console.log('WS: server connected'); console.log('WS: server connected');
Events.fire('ws-connected'); Events.fire('ws-connected');
if (this._isReconnect) Events.fire('notify-user', Localization.getTranslation("notifications.connected")); if (this._isReconnect) {
Events.fire('notify-user', Localization.getTranslation("notifications.connected"));
}
} }
_onPairDeviceInitiate() { _onPairDeviceInitiate() {
@ -101,6 +103,7 @@ class ServerConnection {
_onPairDeviceJoin(pairKey) { _onPairDeviceJoin(pairKey) {
if (!this._isConnected()) { if (!this._isConnected()) {
// Todo: instead use pending outbound ws queue
setTimeout(() => this._onPairDeviceJoin(pairKey), 1000); setTimeout(() => this._onPairDeviceJoin(pairKey), 1000);
return; return;
} }
@ -336,6 +339,10 @@ class Peer {
this._evaluateAutoAccept(); this._evaluateAutoAccept();
} }
_setIsCaller(isCaller) {
this._isCaller = isCaller;
}
sendJSON(message) { sendJSON(message) {
this._send(JSON.stringify(message)); this._send(JSON.stringify(message));
} }
@ -433,6 +440,14 @@ class Peer {
: false; : false;
} }
_onPeerConnected() {
if (this._digester) {
// Reconnection during receiving of file. Send request for restart
const offset = this._digester._bytesReceived;
this._requestResendFromOffset(offset);
}
}
async requestFileTransfer(files) { async requestFileTransfer(files) {
let header = []; let header = [];
let totalSize = 0; let totalSize = 0;
@ -472,8 +487,8 @@ class Peer {
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'}) Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'wait'})
} }
async sendFiles() { sendFiles() {
for (let i=0; i<this._filesRequested.length; i++) { for (let i = 0; i < this._filesRequested.length; i++) {
this._filesQueue.push(this._filesRequested[i]); this._filesQueue.push(this._filesRequested[i]);
} }
this._filesRequested = null this._filesRequested = null
@ -487,7 +502,7 @@ class Peer {
this._sendFile(file); this._sendFile(file);
} }
async _sendFile(file) { _sendFile(file) {
this.sendJSON({ this.sendJSON({
type: 'header', type: 'header',
size: file.size, size: file.size,
@ -508,11 +523,21 @@ class Peer {
this.sendJSON({ type: 'partition-received', offset: offset }); this.sendJSON({ type: 'partition-received', offset: offset });
} }
_requestResendFromOffset(offset) {
this.sendJSON({ type: 'request-resend-from-offset', offset: offset });
}
_sendNextPartition() { _sendNextPartition() {
if (!this._chunker || this._chunker.isFileEnd()) return; if (!this._chunker || this._chunker.isFileEnd()) return;
this._chunker.nextPartition(); this._chunker.nextPartition();
} }
_onRequestResendFromOffset(offset) {
console.log("Restart requested from offset:", offset)
if (!this._chunker) return;
this._chunker._restartFromOffset(offset);
}
_sendProgress(progress) { _sendProgress(progress) {
this.sendJSON({ type: 'progress', progress: progress }); this.sendJSON({ type: 'progress', progress: progress });
} }
@ -522,25 +547,35 @@ class Peer {
this._onChunkReceived(message); this._onChunkReceived(message);
return; return;
} }
const messageJSON = JSON.parse(message);
switch (messageJSON.type) { try {
message = JSON.parse(message);
} catch (e) {
console.warn("Peer: Received JSON is malformed");
return;
}
switch (message.type) {
case 'request': case 'request':
this._onFilesTransferRequest(messageJSON); this._onFilesTransferRequest(message);
break; break;
case 'header': case 'header':
this._onFileHeader(messageJSON); this._onFileHeader(message);
break; break;
case 'partition': case 'partition':
this._onReceivedPartitionEnd(messageJSON); this._onReceivedPartitionEnd(message);
break; break;
case 'partition-received': case 'partition-received':
this._sendNextPartition(); this._sendNextPartition();
break; break;
case 'progress': case 'progress':
this._onDownloadProgress(messageJSON.progress); this._onProgress(message.progress);
break;
case 'request-resend-from-offset':
this._onRequestResendFromOffset(message.offset);
break; break;
case 'files-transfer-response': case 'files-transfer-response':
this._onFileTransferRequestResponded(messageJSON); this._onFileTransferRequestResponded(message);
break; break;
case 'file-transfer-complete': case 'file-transfer-complete':
this._onFileTransferCompleted(); this._onFileTransferCompleted();
@ -549,10 +584,10 @@ class Peer {
this._onMessageTransferCompleted(); this._onMessageTransferCompleted();
break; break;
case 'text': case 'text':
this._onTextReceived(messageJSON); this._onTextReceived(message);
break; break;
case 'display-name-changed': case 'display-name-changed':
this._onDisplayNameChanged(messageJSON); this._onDisplayNameChanged(message);
break; break;
} }
} }
@ -620,21 +655,28 @@ class Peer {
if(!this._digester || !(chunk.byteLength || chunk.size)) return; if(!this._digester || !(chunk.byteLength || chunk.size)) return;
this._digester.unchunk(chunk); this._digester.unchunk(chunk);
const progress = this._digester.progress; const progress = this._digester.progress;
if (progress > 1) { if (progress > 1) {
this._abortTransfer(); this._abortTransfer();
return;
} }
this._onDownloadProgress(progress); if (progress === 1) {
this._digester = null;
}
Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'receive'});
// occasionally notify sender about our progress // occasionally notify sender about our progress
if (progress - this._lastProgress < 0.005 && progress !== 1) return; if (progress - this._lastProgress >= 0.005 || progress === 1) {
this._lastProgress = progress; this._lastProgress = progress;
this._sendProgress(progress); this._sendProgress(progress);
}
} }
_onDownloadProgress(progress) { _onProgress(progress) {
Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'}); Events.fire('set-progress', {peerId: this._peerId, progress: progress, status: 'transfer'});
} }
@ -654,25 +696,33 @@ class Peer {
Events.fire('file-received', fileBlob); Events.fire('file-received', fileBlob);
this._filesReceived.push(fileBlob); this._filesReceived.push(fileBlob);
if (!this._requestAccepted.header.length) {
this._busy = false; if (this._requestAccepted.header.length) return;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
Events.fire('files-received', {peerId: this._peerId, files: this._filesReceived, imagesOnly: this._requestAccepted.imagesOnly, totalSize: this._requestAccepted.totalSize}); // We are done receiving
this._filesReceived = []; this._busy = false;
this._requestAccepted = null; Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
} Events.fire('files-received', {
peerId: this._peerId,
files: this._filesReceived,
imagesOnly: this._requestAccepted.imagesOnly,
totalSize: this._requestAccepted.totalSize
});
this._filesReceived = [];
this._requestAccepted = null;
} }
_onFileTransferCompleted() { _onFileTransferCompleted() {
this._chunker = null; this._chunker = null;
if (!this._filesQueue.length) { if (this._filesQueue.length) {
this._busy = false;
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
}
else {
this._dequeueFile(); this._dequeueFile();
return;
} }
// No more files in queue. Transfer is complete
this._busy = false;
Events.fire('notify-user', Localization.getTranslation("notifications.file-transfer-completed"));
Events.fire('files-sent'); // used by 'Snapdrop & PairDrop for Android' app
} }
_onFileTransferRequestResponded(message) { _onFileTransferRequestResponded(message) {
@ -725,99 +775,293 @@ class RTCPeer extends Peer {
super(serverConnection, isCaller, peerId, roomType, roomId); super(serverConnection, isCaller, peerId, roomType, roomId);
this.rtcSupported = true; this.rtcSupported = true;
this.rtcConfig = rtcConfig this.rtcConfig = rtcConfig;
this.pendingInboundMessages = [];
this.pendingOutboundMessages = [];
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide());
if (!this._isCaller) return; // we will listen for a caller
this._connect(); this._connect();
} }
_connect() { _isConnected() {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(); return this._conn && this._conn.connectionState === 'connected';
}
if (this._isCaller) { _isConnecting() {
this._openChannel(); return this._conn
} && (
else { this._conn.connectionState === 'new'
this._conn.ondatachannel = e => this._onChannelOpened(e); || this._conn.connectionState === 'connecting'
} );
}
_isChannelOpen() {
return this._channel && this._channel.readyState === 'open';
}
_isChannelConnecting() {
return this._channel && this._channel.readyState === 'connecting';
}
_isStable() {
return this._isChannelOpen() && this._isConnected();
}
_connect() {
if (this._isStable()) return;
Events.fire('peer-connecting', this._peerId);
this._openConnection();
// TOdo: one channel for messages - one for data?
this._openChannel();
} }
_openConnection() { _openConnection() {
this._conn = new RTCPeerConnection(this.rtcConfig); const conn = new RTCPeerConnection(this.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e); conn.onnegotiationneeded = _ => this._onNegotiationNeeded();
this._conn.onicecandidateerror = e => this._onError(e); conn.onsignalingstatechange = _ => this._onSignalingStateChanged();
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange(); conn.oniceconnectionstatechange = _ => this._onIceConnectionStateChange();
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e); conn.onicegatheringstatechange = _ => this._onIceGatheringStateChanged();
conn.onconnectionstatechange = _ => this._onConnectionStateChange();
conn.onicecandidate = e => this._onIceCandidate(e);
conn.onicecandidateerror = e => this._onIceCandidateError(e);
this._conn = conn;
this._evaluatePendingInboundMessages()
.then((count) => {
if (count) {
console.log("Pending inbound messages evaluated.");
}
});
} }
_openChannel() { async _onNegotiationNeeded() {
if (!this._conn) return; console.log('RTC: Negotiation needed');
const channel = this._conn.createDataChannel('data-channel', { if (this._isCaller) {
ordered: true, // Creating offer if required
reliable: true // Obsolete. See https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/reliable console.log('RTC: Creating offer');
}); const description = await this._conn.createOffer();
channel.onopen = e => this._onChannelOpened(e); await this._handleLocalDescription(description);
channel.onerror = e => this._onError(e); }
this._conn
.createOffer()
.then(d => this._onDescription(d))
.catch(e => this._onError(e));
} }
_onDescription(description) { _onSignalingStateChanged() {
// description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400'); console.log('RTC: Signaling state changed:', this._conn.signalingState);
this._conn }
.setLocalDescription(description)
.then(_ => this._sendSignal({ sdp: description })) _onIceConnectionStateChange() {
.catch(e => this._onError(e)); console.log('RTC: ICE connection state changed:', this._conn.iceConnectionState);
}
_onIceGatheringStateChanged() {
console.log('RTC: ICE gathering state changed:', this._conn.iceConnectionState);
}
_onConnectionStateChange() {
console.log('RTC: Connection state changed:', this._conn.connectionState);
switch (this._conn.connectionState) {
case 'disconnected':
this._refresh();
break;
case 'failed':
console.warn('RTC connection failed');
// TOdo: implement ws fallback as real fallback
this._refresh();
}
} }
_onIceCandidate(event) { _onIceCandidate(event) {
if (!event.candidate) return; this._handleLocalCandidate(event.candidate);
this._sendSignal({ ice: event.candidate });
} }
onServerMessage(message) { _onIceCandidateError(error) {
if (!this._conn) this._connect(); console.error(error);
if (message.sdp) {
this._conn
.setRemoteDescription(message.sdp)
.then(_ => {
if (message.sdp.type === 'offer') {
return this._conn
.createAnswer()
.then(d => this._onDescription(d));
}
})
.catch(e => this._onError(e));
}
else if (message.ice) {
this._conn
.addIceCandidate(new RTCIceCandidate(message.ice))
.catch(e => this._onError(e));
}
} }
_onChannelOpened(event) { _openChannel() {
console.log('RTC: channel opened with', this._peerId); const channel = this._conn.createDataChannel('data-channel', {
const channel = event.channel || event.target; ordered: true,
negotiated: true,
id: 0
});
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onopen = _ => this._onChannelOpened();
channel.onclose = _ => this._onChannelClosed(); channel.onclose = _ => this._onChannelClosed();
channel.onerror = e => this._onChannelError(e);
channel.onmessage = e => this._onMessage(e.data);
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide());
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
} }
_onMessage(message) { _onChannelOpened() {
if (typeof message === 'string') { console.log('RTC: Channel opened with', this._peerId);
console.log('RTC:', JSON.parse(message)); console.debug(this.getConnectionHash())
console.debug(this._conn)
console.debug(this._channel)
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
super._onPeerConnected();
while (this._isChannelOpen() && this.pendingOutboundMessages.length > 0) {
this._sendViaChannel(this.pendingOutboundMessages.shift());
} }
super._onMessage(message); }
_onChannelClosed() {
console.log('RTC: Channel closed', this._peerId);
this._refresh();
}
_onChannelError(error) {
console.error(error);
}
async _handleLocalDescription(localDescription) {
await this._conn.setLocalDescription(localDescription);
console.log("RTC: Sending local description");
this._sendSignal({ signalType: 'description', description: localDescription });
}
async _handleRemoteDescription(remoteDescription) {
console.log("RTC: Received remote description");
await this._conn.setRemoteDescription(remoteDescription);
if (!this._isCaller) {
// Creating answer if required
console.log('RTC: Creating answer');
const localDescription = await this._conn.createAnswer();
await this._handleLocalDescription(localDescription);
}
}
_handleLocalCandidate(candidate) {
console.log("RTC: Sending local candidate");
this._sendSignal({ signalType: 'candidate', candidate: candidate });
if (candidate === null) {
this.localIceCandidatesSent = true;
}
}
async _handleRemoteCandidate(candidate) {
console.log("RTC: Received remote candidate");
if (candidate !== null) {
await this._conn.addIceCandidate(candidate);
}
else {
this.remoteIceCandidatesReceived = true;
}
}
async _evaluatePendingInboundMessages() {
let inboundMessagesEvaluatedCount = 0;
while (this.pendingInboundMessages.length > 0) {
const message = this.pendingInboundMessages.shift();
console.log("Evaluate pending inbound message:", message);
await this.onServerMessage(message);
inboundMessagesEvaluatedCount++;
}
return inboundMessagesEvaluatedCount;
}
async onServerMessage(message) {
if (this._conn === null) {
console.debug("Not ready yet. Pending needed indeed?")
this.pendingInboundMessages.push(message);
return;
}
switch (message.signalType) {
case 'description':
await this._handleRemoteDescription(message.description);
break;
case 'candidate':
await this._handleRemoteCandidate(message.candidate);
break;
default:
console.warn(this.name, 'Unknown message type:', message.type);
break;
}
}
_disconnect() {
Events.fire('peer-disconnected', this._peerId);
}
_refresh() {
Events.fire('peer-connecting', this._peerId);
this._closeChannelAndConnection();
this._connect(); // reopen the channel
}
_closeChannelAndConnection() {
if (this._channel) {
this._channel.onopen = null;
this._channel.onclose = null;
this._channel.onerror = null;
this._channel.onmessage = null;
this._channel.close();
this._channel = null;
}
if (this._conn) {
this._conn.onnegotiationneeded = null;
this._conn.onsignalingstatechange = null;
this._conn.oniceconnectionstatechange = null;
this._conn.onicegatheringstatechange = null;
this._conn.onconnectionstatechange = null;
this._conn.onicecandidate = null;
this._conn.onicecandidateerror = null;
this._conn.close();
this._conn = null;
}
this.localIceCandidatesSent = false;
this.remoteIceCandidatesReceived = false;
}
_onBeforeUnload(e) {
if (this._busy) {
e.preventDefault();
return Localization.getTranslation("notifications.unfinished-transfers-warning");
}
}
_onPageHide() {
this._disconnect();
}
_send(message) {
// Todo: if channel or connection is closed or disconnected: do not send
// put messages in queue and send after reconnection.
// this._pendingMessages[];
if (!this._isStable() || this.pendingOutboundMessages.length > 0) {
// queue messages if not connected OR if connected AND queue is not empty
this.pendingOutboundMessages.push(message);
return;
}
this._sendViaChannel(message);
}
_sendViaChannel(message) {
this._channel.send(message);
}
_sendSignal(message) {
message.type = 'signal';
message.to = this._peerId;
message.roomType = this._getRoomTypes()[0];
message.roomId = this._roomIds[this._getRoomTypes()[0]];
this._server.send(message);
}
sendDisplayName(displayName) {
super.sendDisplayName(displayName);
} }
getConnectionHash() { getConnectionHash() {
@ -886,54 +1130,12 @@ class RTCPeer extends Peer {
} }
} }
_onIceConnectionStateChange() { _onMessage(message) {
switch (this._conn.iceConnectionState) { if (typeof message === 'string') {
case 'failed': // Todo: Test speed increase without prints? --> print only on debug mode via URL argument `?debug_mode=true`
this._onError('ICE Gathering failed'); console.log('RTC:', JSON.parse(message));
break;
default:
console.log('ICE Gathering', this._conn.iceConnectionState);
} }
} super._onMessage(message);
_onError(error) {
console.error(error);
}
_send(message) {
if (!this._channel) this.refresh();
this._channel.send(message);
}
_sendSignal(signal) {
signal.type = 'signal';
signal.to = this._peerId;
signal.roomType = this._getRoomTypes()[0];
signal.roomId = this._roomIds[this._getRoomTypes()[0]];
this._server.send(signal);
}
refresh() {
// check if channel is open. otherwise create one
if (this._isConnected() || this._isConnecting()) return;
// only reconnect if peer is caller
if (!this._isCaller) return;
this._connect();
}
_isConnected() {
return this._channel && this._channel.readyState === 'open';
}
_isConnecting() {
return this._channel && this._channel.readyState === 'connecting';
}
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
} }
} }
@ -1020,9 +1222,7 @@ class PeersManager {
this.peers[peerId].onServerMessage(message); this.peers[peerId].onServerMessage(message);
} }
_refreshPeer(peerId, roomType, roomId) { _refreshPeer(isCaller, peerId, roomType, roomId) {
if (!this._peerExists(peerId)) return false;
const peer = this.peers[peerId]; const peer = this.peers[peerId];
const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType; const roomTypesDiffer = Object.keys(peer._roomIds)[0] !== roomType;
const roomIdsDiffer = peer._roomIds[roomType] !== roomId; const roomIdsDiffer = peer._roomIds[roomType] !== roomId;
@ -1036,17 +1236,22 @@ class PeersManager {
return true; return true;
} }
peer.refresh(); // reconnect peer - caller/waiter might be switched
peer._setIsCaller(isCaller);
peer._refresh();
return true; return true;
} }
_createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) { _createOrRefreshPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
if (this._peerExists(peerId)) { if (this._peerExists(peerId)) {
this._refreshPeer(peerId, roomType, roomId); this._refreshPeer(isCaller, peerId, roomType, roomId);
return; } else {
this.createPeer(isCaller, peerId, roomType, roomId, rtcSupported);
} }
}
createPeer(isCaller, peerId, roomType, roomId, rtcSupported) {
if (window.isRtcSupported && rtcSupported) { if (window.isRtcSupported && rtcSupported) {
this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId, this._wsConfig.rtcConfig); this.peers[peerId] = new RTCPeer(this._server, isCaller, peerId, roomType, roomId, this._wsConfig.rtcConfig);
} }
@ -1091,7 +1296,7 @@ class PeersManager {
} }
_onPeerLeft(message) { _onPeerLeft(message) {
if (this._peerExists(message.peerId) && this._webRtcSupported(message.peerId)) { if (this._peerExists(message.peerId) && !this._webRtcSupported(message.peerId)) {
console.log('WSPeer left:', message.peerId); console.log('WSPeer left:', message.peerId);
} }
if (message.disconnect === true) { if (message.disconnect === true) {
@ -1136,11 +1341,10 @@ class PeersManager {
_onPeerDisconnected(peerId) { _onPeerDisconnected(peerId) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
delete this.peers[peerId]; delete this.peers[peerId];
if (!peer || !peer._conn) return;
if (peer._channel) peer._channel.onclose = null; if (!peer) return;
peer._conn.close();
peer._busy = false; peer._closeChannelAndConnection();
peer._roomIds = {};
} }
_onRoomSecretsDeleted(roomSecrets) { _onRoomSecretsDeleted(roomSecrets) {
@ -1268,6 +1472,11 @@ class FileChunker {
this._readChunk(); this._readChunk();
} }
_restartFromOffset(offset) {
this._offset = offset;
this.nextPartition();
}
repeatPartition() { repeatPartition() {
this._offset -= this._partitionSize; this._offset -= this._partitionSize;
this.nextPartition(); this.nextPartition();

View file

@ -16,7 +16,7 @@ class PeersUI {
this.$shareModeCancelBtn = $$('.shr-panel .cancel-btn'); this.$shareModeCancelBtn = $$('.shr-panel .cancel-btn');
this.$shareModeEditBtn = $$('.shr-panel .edit-btn'); this.$shareModeEditBtn = $$('.shr-panel .edit-btn');
this.peers = {}; this.peerUIs = {};
this.shareMode = {}; this.shareMode = {};
this.shareMode.active = false; this.shareMode.active = false;
@ -24,9 +24,9 @@ class PeersUI {
this.shareMode.files = []; this.shareMode.files = [];
this.shareMode.text = ""; this.shareMode.text = "";
Events.on('peer-joined', e => this._onPeerJoined(e.detail)); Events.on('peer-joined', e => this._onPeerJoined(e.detail.peer, e.detail.roomType, e.detail.roomId));
Events.on('peer-added', _ => this._evaluateOverflowingPeers());
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash)); Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId, e.detail.connectionHash));
Events.on('peer-connecting', e => this._onPeerConnecting(e.detail));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail)); Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('peers', e => this._onPeers(e.detail)); Events.on('peers', e => this._onPeers(e.detail));
Events.on('set-progress', e => this._onSetProgress(e.detail)); Events.on('set-progress', e => this._onSetProgress(e.detail));
@ -47,17 +47,17 @@ class PeersUI {
this.$shareModeCancelBtn.addEventListener('click', _ => this._deactivateShareMode()); this.$shareModeCancelBtn.addEventListener('click', _ => this._deactivateShareMode());
Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e)); Events.on('peer-display-name-changed', e => this._onPeerDisplayNameChanged(e.detail.peerId, e.detail.displayName));
Events.on('ws-config', e => this._evaluateRtcSupport(e.detail)) Events.on('ws-config', e => this._evaluateRtcSupport(e.detail))
} }
_evaluateRtcSupport(wsConfig) { _evaluateRtcSupport(wsConfig) {
if (wsConfig.wsFallback) { if (wsConfig.wsFallback) {
this.$wsFallbackWarning.hidden = false; this.$wsFallbackWarning.removeAttribute("hidden");
} }
else { else {
this.$wsFallbackWarning.hidden = true; this.$wsFallbackWarning.setAttribute("hidden", true);
if (!window.isRtcSupported) { if (!window.isRtcSupported) {
alert(Localization.getTranslation("instructions.webrtc-requirement")); alert(Localization.getTranslation("instructions.webrtc-requirement"));
} }
@ -65,15 +65,17 @@ class PeersUI {
} }
_changePeerDisplayName(peerId, displayName) { _changePeerDisplayName(peerId, displayName) {
this.peers[peerId].name.displayName = displayName; const peerUI = this.peerUIs[peerId];
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName; if (!peerUI) return;
this._redrawPeerRoomTypes(peerId);
peerUI._setDisplayName(displayName);
} }
_onPeerDisplayNameChanged(e) { _onPeerDisplayNameChanged(peerId, displayName) {
if (!e.detail.displayName) return; if (!peerId || !displayName) return;
this._changePeerDisplayName(e.detail.peerId, e.detail.displayName);
this._changePeerDisplayName(peerId, displayName);
} }
async _onKeyDown(e) { async _onKeyDown(e) {
@ -89,50 +91,48 @@ class PeersUI {
} }
} }
_onPeerJoined(msg) { _onPeerJoined(peer, roomType, roomId) {
this._joinPeer(msg.peer, msg.roomType, msg.roomId); this._joinPeer(peer, roomType, roomId);
} }
_joinPeer(peer, roomType, roomId) { _joinPeer(peer, roomType, roomId) {
const existingPeer = this.peers[peer.id]; const existingPeerUI = this.peerUIs[peer.id];
if (existingPeer) { if (existingPeerUI) {
// peer already exists. Abort but add roomType to GUI // peerUI already exists. Abort but add roomType to GUI
existingPeer._roomIds[roomType] = roomId; existingPeerUI._addRoomId(roomType, roomId);
this._redrawPeerRoomTypes(peer.id);
return; return;
} }
peer._isSameBrowser = () => BrowserTabsConnector.peerIsSameBrowser(peer.id); const peerUI = new PeerUI(peer, roomType, roomId, {
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, active: this.shareMode.active,
descriptor: this.shareMode.descriptor, descriptor: this.shareMode.descriptor,
}); });
this.peerUIs[peer.id] = peerUI;
} }
_redrawPeerRoomTypes(peerId) { _onPeerConnected(peerId, connectionHash) {
const peer = this.peers[peerId]; const peerUI = this.peerUIs[peerId];
const peerNode = $(peerId);
if (!peer || !peerNode) return; if (!peerUI) return;
peerNode.classList.remove('type-ip', 'type-secret', 'type-public-id', 'type-same-browser'); peerUI._peerConnected(true, connectionHash);
if (peer._isSameBrowser()) { this._addPeerUIIfMissing(peerUI);
peerNode.classList.add(`type-same-browser`); }
}
Object.keys(peer._roomIds).forEach(roomType => peerNode.classList.add(`type-${roomType}`)); _addPeerUIIfMissing(peerUI) {
if (this.$xPeers.contains(peerUI.$el)) return;
this.$xPeers.appendChild(peerUI.$el);
this._evaluateOverflowingPeers();
}
_onPeerConnecting(peerId) {
const peerUI = this.peerUIs[peerId];
if (!peerUI) return;
peerUI._peerConnected(false);
} }
_evaluateOverflowingPeers() { _evaluateOverflowingPeers() {
@ -149,26 +149,31 @@ class PeersUI {
} }
_onPeerDisconnected(peerId) { _onPeerDisconnected(peerId) {
const $peer = $(peerId); const peerUI = this.peerUIs[peerId];
if (!$peer) return;
$peer.remove(); if (!peerUI) return;
peerUI._removeDom();
delete this.peerUIs[peerId];
this._evaluateOverflowingPeers(); this._evaluateOverflowingPeers();
} }
_onRoomTypeRemoved(peerId, roomType) { _onRoomTypeRemoved(peerId, roomType) {
const peer = this.peers[peerId]; const peerUI = this.peerUIs[peerId];
if (!peer) return; if (!peerUI) return;
delete peer._roomIds[roomType]; peerUI._removeRoomId(roomType);
this._redrawPeerRoomTypes(peerId)
} }
_onSetProgress(progress) { _onSetProgress(progress) {
const $peer = $(progress.peerId); const peerUI = this.peerUIs[progress.peerId];
if (!$peer) return;
$peer.ui.setProgress(progress.progress, progress.status) if (!peerUI) return;
peerUI.setProgress(progress.progress, progress.status);
} }
_onDrop(e) { _onDrop(e) {
@ -392,35 +397,52 @@ class PeersUI {
class PeerUI { class PeerUI {
static _badgeClassNames = ["badge-room-ip", "badge-room-secret", "badge-room-public-id"]; static _badgeClassNames = ["badge-room-ip", "badge-room-secret", "badge-room-public-id"];
static _shareMode = {
active: false,
descriptor: ""
};
constructor(peer, connectionHash, shareMode) { constructor(peer, roomType, roomId, shareMode = {active: false, descriptor: ""}) {
this.$xInstructions = $$('x-instructions'); this.$xInstructions = $$('x-instructions');
this.$xPeers = $$('x-peers');
this._peer = peer; this._peer = peer;
this._connectionHash = this._connectionHash = "";
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`; this._connected = false;
// This is needed if the ShareMode is started BEFORE the PeerUI is drawn. this._roomIds = {}
PeerUI._shareMode = shareMode; this._roomIds[roomType] = roomId;
this._shareMode = shareMode;
this._createCallbacks();
this._initDom(); this._initDom();
this.$xPeers.appendChild(this.$el);
Events.fire('peer-added');
// ShareMode // ShareMode
Events.on('share-mode-changed', e => this._onShareModeChanged(e.detail.active, e.detail.descriptor)); Events.on('share-mode-changed', e => this._onShareModeChanged(e.detail.active, e.detail.descriptor));
} }
_initDom() {
this.$el = document.createElement('x-peer');
this.$el.id = this._peer.id;
this.$el.ui = this;
this.$el.classList.add('center');
this.html();
this.$label = this.$el.querySelector('label');
this.$input = this.$el.querySelector('input');
this.$displayName = this.$el.querySelector('.name');
this.updateTypesClassList();
this.setStatus("connect");
this._evaluateShareMode();
this._bindListeners();
}
_removeDom() {
this.$el.remove();
}
html() { html() {
let title= PeerUI._shareMode.active let title= Localization.getTranslation("peer-ui.click-to-send");
? Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: PeerUI._shareMode.descriptor})
: Localization.getTranslation("peer-ui.click-to-send");
this.$el.innerHTML = ` this.$el.innerHTML = `
<label class="column center pointer" title="${title}"> <label class="column center pointer" title="${title}">
@ -449,41 +471,37 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName(); this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$label = this.$el.querySelector('label');
this.$input = this.$el.querySelector('input');
} }
addTypesToClassList() { updateTypesClassList() {
if (this._peer._isSameBrowser()) { // Remove all classes
this.$el.classList.remove('type-ip', 'type-secret', 'type-public-id', 'type-same-browser', 'ws-peer');
// Add classes accordingly
Object.keys(this._roomIds).forEach(roomType => this.$el.classList.add(`type-${roomType}`));
if (BrowserTabsConnector.peerIsSameBrowser(this._peer.id)) {
this.$el.classList.add(`type-same-browser`); 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');
if (!this._peer.rtcSupported || !window.isRtcSupported) this.$el.classList.add('ws-peer'); }
} }
_initDom() { _addRoomId(roomType, roomId) {
this.$el = document.createElement('x-peer'); this._roomIds[roomType] = roomId;
this.$el.id = this._peer.id; this.updateTypesClassList();
this.$el.ui = this; }
this.$el.classList.add('center');
this.addTypesToClassList(); _removeRoomId(roomType) {
delete this._roomIds[roomType];
this.html(); this.updateTypesClassList();
this._createCallbacks();
this._evaluateShareMode();
this._bindListeners();
} }
_onShareModeChanged(active = false, descriptor = "") { _onShareModeChanged(active = false, descriptor = "") {
// This is needed if the ShareMode is started AFTER the PeerUI is drawn. this._shareMode.active = active;
PeerUI._shareMode.active = active; this._shareMode.descriptor = descriptor;
PeerUI._shareMode.descriptor = descriptor;
this._evaluateShareMode(); this._evaluateShareMode();
this._bindListeners(); this._bindListeners();
@ -491,12 +509,12 @@ class PeerUI {
_evaluateShareMode() { _evaluateShareMode() {
let title; let title;
if (!PeerUI._shareMode.active) { if (!this._shareMode.active) {
title = Localization.getTranslation("peer-ui.click-to-send"); title = Localization.getTranslation("peer-ui.click-to-send");
this.$input.removeAttribute('disabled'); this.$input.removeAttribute('disabled');
} }
else { else {
title = Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: PeerUI._shareMode.descriptor}); title = Localization.getTranslation("peer-ui.click-to-send-share-mode", null, {descriptor: this._shareMode.descriptor});
this.$input.setAttribute('disabled', true); this.$input.setAttribute('disabled', true);
} }
this.$label.setAttribute('title', title); this.$label.setAttribute('title', title);
@ -517,7 +535,7 @@ class PeerUI {
} }
_bindListeners() { _bindListeners() {
if(!PeerUI._shareMode.active) { if(!this._shareMode.active) {
// Remove Events Share mode // Remove Events Share mode
this.$el.removeEventListener('pointerdown', this._callbackPointerDown); this.$el.removeEventListener('pointerdown', this._callbackPointerDown);
@ -559,6 +577,37 @@ class PeerUI {
}); });
} }
_peerConnected(connected = true, connectionHash = "") {
if (connected) {
this._connected = true;
// on reconnect
this.setStatus(this.oldStatus);
this.oldStatus = null;
this._connectionHash = connectionHash;
}
else {
this._connected = false;
if (!this.oldStatus && this.currentStatus !== "connect") {
// save old status when reconnecting
this.oldStatus = this.currentStatus;
}
this.setStatus("connect");
this._connectionHash = "";
}
}
getConnectionHashWithSpaces() {
if (this._connectionHash.length !== 16) {
return ""
}
return `${this._connectionHash.substring(0, 4)} ${this._connectionHash.substring(4, 8)} ${this._connectionHash.substring(8, 12)} ${this._connectionHash.substring(12, 16)}`;
}
_displayName() { _displayName() {
return this._peer.name.displayName; return this._peer.name.displayName;
} }
@ -567,13 +616,26 @@ class PeerUI {
return this._peer.name.deviceName; return this._peer.name.deviceName;
} }
_setDisplayName(displayName) {
this._peer.name.displayName = displayName;
this.$displayName.textContent = displayName;
}
_roomTypes() {
return Object.keys(this._roomIds);
}
_badgeClassName() { _badgeClassName() {
const roomTypes = Object.keys(this._peer._roomIds); const roomTypes = this._roomTypes();
return roomTypes.includes('secret') if (roomTypes.includes('secret')) {
? 'badge-room-secret' return 'badge-room-secret';
: roomTypes.includes('ip') }
? 'badge-room-ip' else if (roomTypes.includes('ip')) {
: 'badge-room-public-id'; return 'badge-room-ip';
}
else {
return 'badge-room-public-id';
}
} }
_icon() { _icon() {
@ -590,6 +652,7 @@ class PeerUI {
_onFilesSelected(e) { _onFilesSelected(e) {
const $input = e.target; const $input = e.target;
const files = $input.files; const files = $input.files;
Events.fire('files-selected', { Events.fire('files-selected', {
files: files, files: files,
to: this._peer.id to: this._peer.id
@ -599,40 +662,54 @@ class PeerUI {
setProgress(progress, status) { setProgress(progress, status) {
const $progress = this.$el.querySelector('.progress'); const $progress = this.$el.querySelector('.progress');
if (0.5 < progress && progress < 1) { if (0.5 < progress && progress < 1) {
$progress.classList.add('over50'); $progress.classList.add('over50');
} }
else { else {
$progress.classList.remove('over50'); $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); if (progress === 1) {
this.$el.querySelector('.status').innerText = statusName;
this.currentStatus = status;
}
}
else {
this.$el.removeAttribute('status');
this.$el.querySelector('.status').innerHTML = '';
progress = 0; progress = 0;
this.currentStatus = null; status = null;
} }
this.setStatus(status);
const degrees = `rotate(${360 * progress}deg)`; const degrees = `rotate(${360 * progress}deg)`;
$progress.style.setProperty('--progress', degrees); $progress.style.setProperty('--progress', degrees);
} }
_onDrop(e) { setStatus(status) {
e.preventDefault(); if (!status) {
this.$el.removeAttribute('status');
this.$el.querySelector('.status').innerHTML = '';
this.currentStatus = null;
NoSleepUI.disableIfPeersIdle();
return;
}
if (PeerUI._shareMode.active || Dialog.anyDialogShown()) return; if (status === this.currentStatus) return;
let statusName = {
"connect": Localization.getTranslation("peer-ui.connecting"),
"prepare": Localization.getTranslation("peer-ui.preparing"),
"transfer": Localization.getTranslation("peer-ui.transferring"),
"receive": Localization.getTranslation("peer-ui.receiving"),
"process": Localization.getTranslation("peer-ui.processing"),
"wait": Localization.getTranslation("peer-ui.waiting")
}[status];
this.$el.setAttribute('status', status);
this.$el.querySelector('.status').innerText = statusName;
this.currentStatus = status;
}
_onDrop(e) {
if (this._shareMode.active || Dialog.anyDialogShown()) return;
e.preventDefault();
if (e.dataTransfer.files.length > 0) { if (e.dataTransfer.files.length > 0) {
Events.fire('files-selected', { Events.fire('files-selected', {
@ -1315,8 +1392,8 @@ class PairDeviceDialog extends Dialog {
Events.on('ws-disconnected', _ => this.hide()); Events.on('ws-disconnected', _ => this.hide());
Events.on('pair-device-initiated', e => this._onPairDeviceInitiated(e.detail)); 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('pair-device-joined', e => this._onPairDeviceJoined(e.detail.peerId, e.detail.roomSecret));
Events.on('peers', e => this._onPeers(e.detail)); Events.on('peers', e => this._onPeers(e.detail.peers, e.detail.roomType, e.detail.roomId));
Events.on('peer-joined', e => this._onPeerJoined(e.detail)); Events.on('peer-joined', e => this._onPeerJoined(e.detail.peer, e.detail.roomType, e.detail.roomId));
Events.on('pair-device-join-key-invalid', _ => this._onPublicRoomJoinKeyInvalid()); Events.on('pair-device-join-key-invalid', _ => this._onPublicRoomJoinKeyInvalid());
Events.on('pair-device-canceled', e => this._onPairDeviceCanceled(e.detail)); Events.on('pair-device-canceled', e => this._onPairDeviceCanceled(e.detail));
Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets()) Events.on('evaluate-number-room-secrets', _ => this._evaluateNumberRoomSecrets())
@ -1426,18 +1503,19 @@ class PairDeviceDialog extends Dialog {
}; };
} }
_onPeers(message) { _onPeers(peers, roomType, roomId) {
message.peers.forEach(messagePeer => { peers.forEach(messagePeer => {
this._evaluateJoinedPeer(messagePeer.id, message.roomType, message.roomId); this._evaluateJoinedPeer(messagePeer, roomType, roomId);
}); });
} }
_onPeerJoined(message) { _onPeerJoined(peer, roomType, roomId) {
this._evaluateJoinedPeer(message.peer.id, message.roomType, message.roomId); this._evaluateJoinedPeer(peer, roomType, roomId);
} }
_evaluateJoinedPeer(peerId, roomType, roomId) { _evaluateJoinedPeer(peer, roomType, roomId) {
const noPairPeerSaved = !Object.keys(this.pairPeer); const noPairPeerSaved = !Object.keys(this.pairPeer);
const peerId = peer.id;
if (!peerId || !roomType || !roomId || noPairPeerSaved) return; if (!peerId || !roomType || !roomId || noPairPeerSaved) return;
@ -1447,13 +1525,13 @@ class PairDeviceDialog extends Dialog {
if (!samePeerId || !sameRoomSecret || !typeIsSecret) return; if (!samePeerId || !sameRoomSecret || !typeIsSecret) return;
this._onPairPeerJoined(peerId, roomId); this._onPairPeerJoined(peer, roomId);
this.pairPeer = {}; this.pairPeer = {};
} }
_onPairPeerJoined(peerId, roomSecret) { _onPairPeerJoined(peer, roomSecret) {
// if devices are paired that are already connected we must save the names at this point // if devices are paired that are already connected we must save the names at this point
const $peer = $(peerId); const $peer = $(peer.id);
let displayName, deviceName; let displayName, deviceName;
if ($peer) { if ($peer) {
displayName = $peer.ui._peer.name.displayName; displayName = $peer.ui._peer.name.displayName;
@ -1531,11 +1609,11 @@ class EditPairedDevicesDialog extends Dialog {
super('edit-paired-devices-dialog'); super('edit-paired-devices-dialog');
this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper'); this.$pairedDevicesWrapper = this.$el.querySelector('.paired-devices-wrapper');
this.$footerBadgePairedDevices = $$('.discovery-wrapper .badge-room-secret'); this.$footerBadgePairedDevices = $$('.discovery-wrapper .badge-room-secret');
this.$editPairedDevices = $('edit-paired-devices');
$('edit-paired-devices').addEventListener('click', _ => this._onEditPairedDevices()); this.$editPairedDevices.addEventListener('click', _ => this._onEditPairedDevices());
this.$footerBadgePairedDevices.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)); Events.on('keydown', e => this._onKeyDown(e));
} }
@ -1641,23 +1719,6 @@ class EditPairedDevicesDialog extends Dialog {
}) })
}); });
} }
_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 { class PublicRoomDialog extends Dialog {
@ -1692,7 +1753,7 @@ class PublicRoomDialog extends Dialog {
Events.on('keydown', e => this._onKeyDown(e)); Events.on('keydown', e => this._onKeyDown(e));
Events.on('public-room-created', e => this._onPublicRoomCreated(e.detail)); Events.on('public-room-created', e => this._onPublicRoomCreated(e.detail));
Events.on('peers', e => this._onPeers(e.detail)); Events.on('peers', e => this._onPeers(e.detail));
Events.on('peer-joined', e => this._onPeerJoined(e.detail)); Events.on('peer-joined', e => this._onPeerJoined(e.detail.peer, e.detail.roomId));
Events.on('public-room-id-invalid', e => this._onPublicRoomIdInvalid(e.detail)); Events.on('public-room-id-invalid', e => this._onPublicRoomIdInvalid(e.detail));
Events.on('public-room-left', _ => this._onPublicRoomLeft()); Events.on('public-room-left', _ => this._onPublicRoomLeft());
this.$el.addEventListener('paste', e => this._onPaste(e)); this.$el.addEventListener('paste', e => this._onPaste(e));
@ -1828,15 +1889,15 @@ class PublicRoomDialog extends Dialog {
}); });
} }
_onPeerJoined(message) { _onPeerJoined(peer, roomId) {
this._evaluateJoinedPeer(message.peer.id, message.roomId); this._evaluateJoinedPeer(peer.id, roomId);
} }
_evaluateJoinedPeer(peerId, roomId) { _evaluateJoinedPeer(peerId, roomId) {
const isInitiatedRoomId = roomId === this.roomId; const isInitiatedRoomId = roomId === this.roomId;
const isJoinedRoomId = roomId === this.roomIdJoin; const isJoinedRoomId = roomId === this.roomIdJoin;
if (!peerId || !roomId || !(isInitiatedRoomId || isJoinedRoomId)) return; if (!peerId || !roomId || (!isInitiatedRoomId && !isJoinedRoomId)) return;
this.hide(); this.hide();
@ -2624,11 +2685,12 @@ class NoSleepUI {
static enable() { static enable() {
if (!this._interval) { if (!this._interval) {
NoSleepUI._nosleep.enable(); NoSleepUI._nosleep.enable();
NoSleepUI._interval = setInterval(() => NoSleepUI.disable(), 10000); // Disable after 10s if all peers are idle
NoSleepUI._interval = setInterval(() => NoSleepUI.disableIfPeersIdle(), 10000);
} }
} }
static disable() { static disableIfPeersIdle() {
if ($$('x-peer[status]') === null) { if ($$('x-peer[status]') === null) {
clearInterval(NoSleepUI._interval); clearInterval(NoSleepUI._interval);
NoSleepUI._nosleep.disable(); NoSleepUI._nosleep.disable();