- Enable renaming of own display name permanently via UI

- Make peerId completely ephemeral
- Stabilize RTCConnection by closing connections cleanly
This commit is contained in:
schlagmichdoch 2023-03-01 21:35:00 +01:00
parent a3b348d9b6
commit d56ee87437
9 changed files with 377 additions and 115 deletions

View file

@ -6,6 +6,7 @@ class ServerConnection {
constructor() {
this._connect();
Events.on('pagehide', _ => this._disconnect());
Events.on('beforeunload', _ => this._onBeforeUnload());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
@ -19,10 +20,10 @@ class ServerConnection {
Events.on('online', _ => this._connect());
}
async _connect() {
_connect() {
clearTimeout(this._reconnectTimer);
if (this._isConnected() || this._isConnecting()) return;
const ws = new WebSocket(await this._endpoint());
const ws = new WebSocket(this._endpoint());
ws.binaryType = 'arraybuffer';
ws.onopen = _ => this._onOpen();
ws.onmessage = e => this._onMessage(e.data);
@ -105,6 +106,7 @@ class ServerConnection {
case 'file-transfer-complete':
case 'message-transfer-complete':
case 'text':
case 'display-name-changed':
case 'ws-chunk':
Events.fire('ws-relay', JSON.stringify(msg));
break;
@ -119,45 +121,29 @@ class ServerConnection {
}
_onDisplayName(msg) {
sessionStorage.setItem("peerId", msg.message.peerId);
PersistentStorage.get('peerId').then(peerId => {
if (!peerId) {
// save peerId to indexedDB to retrieve after PWA is installed
PersistentStorage.set('peerId', msg.message.peerId).then(peerId => {
console.log(`peerId saved to indexedDB: ${peerId}`);
});
}
}).catch(_ => _ => PersistentStorage.logBrowserNotCapable())
Events.fire('display-name', msg);
}
async _endpoint() {
_endpoint() {
// hack to detect if deployment or development environment
const protocol = location.protocol.startsWith('https') ? 'wss' : 'ws';
const webrtc = window.isRtcSupported ? '/webrtc' : '/fallback';
let ws_url = new URL(protocol + '://' + location.host + location.pathname + 'server' + webrtc);
const peerId = await this._peerId();
if (peerId) ws_url.searchParams.append('peer_id', peerId)
return ws_url.toString();
}
async _peerId() {
// make peerId persistent when pwa is installed
return window.matchMedia('(display-mode: minimal-ui)').matches
? await PersistentStorage.get('peerId')
: sessionStorage.getItem("peerId");
}
_disconnect() {
this.send({ type: 'disconnect' });
_onBeforeUnload() {
if (this._socket) {
this._socket.onclose = null;
this._socket.close();
this._socket = null;
Events.fire('ws-disconnected');
}
}
_disconnect() {
this.send({ type: 'disconnect' });
}
_onDisconnect() {
console.log('WS: server disconnected');
Events.fire('notify-user', 'No server connection. Retry in 5s...');
@ -324,13 +310,12 @@ class Peer {
this.sendJSON({ type: 'progress', progress: progress });
}
_onMessage(message, logMessage = true) {
_onMessage(message) {
if (typeof message !== 'string') {
this._onChunkReceived(message);
return;
}
message = JSON.parse(message);
if (logMessage) console.log('RTC:', message);
switch (message.type) {
case 'request':
this._onFilesTransferRequest(message);
@ -359,6 +344,9 @@ class Peer {
case 'text':
this._onTextReceived(message);
break;
case 'display-name-changed':
this._onDisplayNameChanged(message);
break;
}
}
@ -496,6 +484,11 @@ class Peer {
Events.fire('text-received', { text: escaped, peerId: this._peerId });
this.sendJSON({ type: 'message-transfer-complete' });
}
_onDisplayNameChanged(message) {
if (!message.displayName) return;
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
}
}
class RTCPeer extends Peer {
@ -506,6 +499,13 @@ class RTCPeer extends Peer {
this._connect(peerId, true);
}
_onMessage(message) {
if (typeof message !== 'string') {
console.log('RTC:', JSON.parse(message));
}
super._onMessage(message);
}
_connect(peerId, isCaller) {
if (!this._conn || this._conn.signalingState === "closed") this._openConnection(peerId, isCaller);
@ -568,14 +568,14 @@ class RTCPeer extends Peer {
_onChannelOpened(event) {
console.log('RTC: channel opened with', this._peerId);
Events.fire('peer-connected', {peerId: this._peerId, connectionHash: this.getConnectionHash()});
const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data);
channel.onclose = _ => this._onChannelClosed();
Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._closeChannel());
channel.onclose = e => this._onChannelClosed(e);
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()});
}
getConnectionHash() {
@ -608,13 +608,21 @@ class RTCPeer extends Peer {
if (this._busy) {
e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?";
} else {
this._disconnect();
}
}
_closeChannel() {
if (this._channel) this._channel.onclose = null;
if (this._conn) this._conn.close();
this._conn = null;
_onPageHide() {
this._disconnect();
}
_disconnect() {
if (this._conn && this._channel) {
this._channel.onclose = null;
this._channel.close();
}
Events.fire('peer-disconnected', this._peerId);
}
_onChannelClosed() {
@ -628,9 +636,11 @@ class RTCPeer extends Peer {
console.log('RTC: state changed:', this._conn.connectionState);
switch (this._conn.connectionState) {
case 'disconnected':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection disconnected');
break;
case 'failed':
Events.fire('peer-disconnected', this._peerId);
this._onError('rtc connection failed');
break;
}
@ -683,6 +693,7 @@ class WSPeer extends Peer {
constructor(serverConnection, peerId, roomType, roomSecret) {
super(serverConnection, peerId, roomType, roomSecret);
if (!peerId) return; // we will listen for a caller
this._isCaller = true;
this._sendSignal();
}
@ -694,6 +705,7 @@ class WSPeer extends Peer {
}
sendJSON(message) {
console.debug(message)
message.to = this._peerId;
message.roomType = this._roomType;
message.roomSecret = this._roomSecret;
@ -705,9 +717,9 @@ class WSPeer extends Peer {
}
onServerMessage(message) {
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (this._peerId) return;
this._peerId = message.sender.id;
Events.fire('peer-connected', {peerId: message.sender.id, connectionHash: this.getConnectionHash()})
if (this._isCaller) return;
this._sendSignal();
}
@ -728,8 +740,11 @@ class PeersManager {
Events.on('respond-to-files-transfer-request', e => this._onRespondToFileTransferRequest(e.detail))
Events.on('send-text', e => this._onSendText(e.detail));
Events.on('peer-left', e => this._onPeerLeft(e.detail));
Events.on('peer-connected', e => this._onPeerConnected(e.detail.peerId));
Events.on('peer-disconnected', e => this._onPeerDisconnected(e.detail));
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('ws-relay', e => this._onWsRelay(e.detail));
}
@ -768,10 +783,6 @@ class PeersManager {
})
}
sendTo(peerId, message) {
this.peers[peerId].send(message);
}
_onRespondToFileTransferRequest(detail) {
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
}
@ -806,6 +817,10 @@ class PeersManager {
}
}
_onPeerConnected(peerId) {
this._notifyPeerDisplayNameChanged(peerId);
}
_onPeerDisconnected(peerId) {
const peer = this.peers[peerId];
delete this.peers[peerId];
@ -823,6 +838,23 @@ class PeersManager {
}
}
}
_notifyPeersDisplayNameChanged(newDisplayName) {
this._displayName = newDisplayName ? newDisplayName : this._originalDisplayName;
for (const peerId in this.peers) {
this._notifyPeerDisplayNameChanged(peerId);
}
}
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return;
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
}
_onDisplayName(displayName) {
this._originalDisplayName = displayName;
}
}
class FileChunker {

View file

@ -10,8 +10,8 @@ window.pasteMode.activated = false;
// set display name
Events.on('display-name', e => {
const me = e.detail.message;
const $displayName = $('display-name')
$displayName.textContent = 'You are known as ' + me.displayName;
const $displayName = $('display-name');
$displayName.setAttribute('placeholder', me.displayName);
$displayName.title = me.deviceName;
});
@ -44,6 +44,61 @@ class PeersUI {
Events.on('peer-added', _ => this.evaluateOverflowing());
Events.on('bg-resize', _ => this.evaluateOverflowing());
this.$displayName = $('display-name');
this.$displayName.addEventListener('keydown', e => this._onKeyDownDisplayName(e));
this.$displayName.addEventListener('keyup', e => this._onKeyUpDisplayName(e));
this.$displayName.addEventListener('blur', e => this._saveDisplayName(e.target.innerText));
Events.on('self-display-name-changed', e => this._insertDisplayName(e.detail));
Events.on('peer-display-name-changed', e => this._changePeerDisplayName(e.detail.peerId, e.detail.displayName));
// Load saved display name
PersistentStorage.get('editedDisplayName').then(displayName => {
console.log("Retrieved edited display name:", displayName)
if (displayName) Events.fire('self-display-name-changed', displayName);
});
}
_insertDisplayName(displayName) {
this.$displayName.textContent = displayName;
}
_onKeyDownDisplayName(e) {
if (e.key === "Enter" || e.key === "Escape") {
e.preventDefault();
e.target.blur();
}
}
_onKeyUpDisplayName(e) {
if (/(\n|\r|\r\n)/.test(e.target.innerText)) e.target.innerText = e.target.innerText.replace(/(\n|\r|\r\n)/, '');
}
async _saveDisplayName(newDisplayName) {
const savedDisplayName = await PersistentStorage.get('editedDisplayName') ?? "";
if (newDisplayName === savedDisplayName) return;
if (newDisplayName) {
PersistentStorage.set('editedDisplayName', newDisplayName).then(_ => {
Events.fire('notify-user', `Display name is set permanently.`);
Events.fire('self-display-name-changed', newDisplayName);
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: newDisplayName});
});
} else {
PersistentStorage.delete('editedDisplayName').then(_ => {
Events.fire('notify-user', 'Display name is randomly generated again.');
Events.fire('self-display-name-changed', '');
Events.fire('broadcast-send', {type: 'self-display-name-changed', detail: ''});
});
}
}
_changePeerDisplayName(peerId, displayName) {
this.peers[peerId].name.displayName = displayName;
const peerIdNode = $(peerId);
if (peerIdNode && displayName) peerIdNode.querySelector('.name').textContent = displayName;
}
_onKeyDown(e) {
@ -521,6 +576,7 @@ class ReceiveFileDialog extends ReceiveDialog {
}
_dequeueFile() {
// Todo: change change count in document.title and move '- PairDrop' to back
if (!this._filesQueue.length) { // nothing to do
this._busy = false;
return;
@ -662,7 +718,7 @@ class ReceiveRequestDialog extends ReceiveDialog {
constructor() {
super('receive-request-dialog');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#requesting-peer-display-name');
this.$requestingPeerDisplayNameNode = this.$el.querySelector('#receive-request-dialog .display-name');
this.$fileStemNode = this.$el.querySelector('#file-stem');
this.$fileExtensionNode = this.$el.querySelector('#file-extension');
this.$fileOtherNode = this.$el.querySelector('#file-other');
@ -992,7 +1048,7 @@ class SendTextDialog extends Dialog {
super('send-text-dialog');
Events.on('text-recipient', e => this._onRecipient(e.detail.peerId, e.detail.deviceName));
this.$text = this.$el.querySelector('#text-input');
this.$peerDisplayName = this.$el.querySelector('#text-send-peer-display-name');
this.$peerDisplayName = this.$el.querySelector('#send-text-dialog .display-name');
this.$form = this.$el.querySelector('form');
this.$submit = this.$el.querySelector('button[type="submit"]');
this.$form.addEventListener('submit', _ => this._send());
@ -1060,7 +1116,7 @@ class ReceiveTextDialog extends Dialog {
Events.on("keydown", e => this._onKeyDown(e));
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-peer-display-name');
this.$receiveTextPeerDisplayNameNode = this.$el.querySelector('#receive-text-dialog .display-name');
this._receiveTextQueue = [];
}
@ -1684,6 +1740,23 @@ class PersistentStorage {
}
}
class Broadcast {
constructor() {
this.bc = new BroadcastChannel('pairdrop');
this.bc.addEventListener('message', e => this._onMessage(e));
Events.on('broadcast-send', e => this._broadcastMessage(e.detail));
}
_broadcastMessage(message) {
this.bc.postMessage(message);
}
_onMessage(e) {
console.log('Broadcast message received:', e.data)
Events.fire(e.data.type, e.data.detail);
}
}
class PairDrop {
constructor() {
Events.on('load', _ => {
@ -1703,6 +1776,7 @@ class PairDrop {
const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI();
const broadCast = new Broadcast();
});
}
}