- 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

@ -453,7 +453,7 @@ class Peer {
this._setIP(request);
// set peer id
this._setPeerId(request)
this.id = crypto.randomUUID();
// is WebRTC supported ?
this.rtcSupported = request.url.indexOf('webrtc') > -1;
@ -525,15 +525,6 @@ class Peer {
return false;
}
_setPeerId(request) {
let peer_id = new URL(request.url, "http://server").searchParams.get("peer_id");
if (peer_id && Peer.isValidUuid(peer_id)) {
this.id = peer_id;
} else {
this.id = crypto.randomUUID();
}
}
toString() {
return `<Peer id=${this.id} ip=${this.ip} rtcSupported=${this.rtcSupported}>`
}

View file

@ -89,7 +89,11 @@
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<div id="display-name" placeholder="&nbsp;"></div>
<div>
<span>You are known as:</span>
<div id="display-name" placeholder="Loading..." autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon"><use xlink:href="#edit-pen-icon" /></svg>
</div>
<div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
@ -145,7 +149,7 @@
<h2 class="center">PairDrop</h2>
<div class="text-center file-description">
<div>
<span id="requesting-peer-display-name"></span>
<span class="display-name"></span>
<span>would like to share</span>
</div>
<div id="file-name" class="row" >
@ -190,7 +194,7 @@
<h2 class="text-center">PairDrop</h2>
<div class="text-center">
<span>Send a Message to</span>
<span id="text-send-peer-display-name"></span>
<span class="display-name"></span>
</div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="row-reverse">
@ -208,8 +212,8 @@
<x-paper shadow="2">
<h2>PairDrop - Message Received</h2>
<div id="receive-text-description-container">
<span id="receive-text-peer-display-name"></span>
<span>sent the following message:</span>
<span class="display-name"></span>
<span>sent a message:</span>
</div>
<div class="row-separator"></div>
<div id="text"></div>
@ -326,6 +330,10 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
</symbol>
</svg>
<!-- Scripts -->
<script src="scripts/util.js"></script>

View file

@ -8,6 +8,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));
@ -21,10 +22,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);
@ -109,45 +110,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...');
@ -320,7 +305,6 @@ class Peer {
return;
}
message = JSON.parse(message);
console.log('RTC:', message);
switch (message.type) {
case 'request':
this._onFilesTransferRequest(message);
@ -349,6 +333,9 @@ class Peer {
case 'text':
this._onTextReceived(message);
break;
case 'display-name-changed':
this._onDisplayNameChanged(message);
break;
}
}
@ -486,6 +473,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 {
@ -496,6 +488,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);
@ -558,14 +557,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() {
@ -598,13 +597,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() {
@ -618,9 +625,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;
}
@ -679,8 +688,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));
}
_onMessage(message) {
@ -704,10 +716,6 @@ class PeersManager {
})
}
sendTo(peerId, message) {
this.peers[peerId].send(message);
}
_onRespondToFileTransferRequest(detail) {
this.peers[detail.to]._respondToFileTransferRequest(detail.accepted);
}
@ -739,6 +747,10 @@ class PeersManager {
}
}
_onPeerConnected(peerId) {
this._notifyPeerDisplayNameChanged(peerId);
}
_onPeerDisconnected(peerId) {
const peer = this.peers[peerId];
delete this.peers[peerId];
@ -756,6 +768,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) {
@ -520,6 +575,7 @@ class ReceiveFileDialog extends ReceiveDialog {
}
_dequeueFile() {
// Todo: change count in document.title and move '- PairDrop' to back
if (!this._filesQueue.length) { // nothing to do
this._busy = false;
return;
@ -661,7 +717,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');
@ -991,7 +1047,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());
@ -1059,7 +1115,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 = [];
}
@ -1683,6 +1739,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', _ => {
@ -1702,6 +1775,7 @@ class PairDrop {
const webShareTargetUI = new WebShareTargetUI();
const webFileHandlersUI = new WebFileHandlersUI();
const noSleepUI = new NoSleepUI();
const broadCast = new Broadcast();
});
}
}

View file

@ -450,6 +450,7 @@ x-peer[status] x-icon {
}
.device-descriptor {
width: 100%;
text-align: center;
}
@ -557,6 +558,28 @@ footer .font-body2 {
padding-bottom: 1px;
}
#display-name {
display: inline-block;
text-align: left;
padding-right: 1rem;
border: none;
outline: none;
max-width: 18em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: -5px;
}
#edit-pen {
width: 1rem;
height: 1rem;
margin-left: -1rem;
margin-bottom: -2px;
position: relative;
z-index: -1;
}
/* Dialog */
x-dialog x-background {
@ -1012,11 +1035,11 @@ button::-moz-focus-inner {
x-toast {
position: absolute;
min-height: 48px;
bottom: 24px;
top: 50px;
width: 100%;
max-width: 344px;
background-color: #323232;
color: rgba(255, 255, 255, 0.95);
background-color: rgb(var(--text-color));
color: rgb(var(--bg-color));
align-items: center;
box-sizing: border-box;
padding: 8px 24px;
@ -1030,7 +1053,7 @@ x-toast {
x-toast:not([show]):not(:hover) {
opacity: 0;
transform: translateY(100px);
transform: translateY(-100px);
}

View file

@ -89,7 +89,11 @@
<svg class="icon logo">
<use xlink:href="#wifi-tethering" />
</svg>
<div id="display-name" placeholder="&nbsp;"></div>
<div>
<span>You are known as:</span>
<div id="display-name" placeholder="Loading..." autocorrect="off" autocomplete="off" autocapitalize="none" spellcheck="false" contenteditable></div>
<svg id="edit-pen" class="icon"><use xlink:href="#edit-pen-icon" /></svg>
</div>
<div class="font-body2">
You can be discovered by everyone <span id="on-this-network">on&nbsp;this&nbsp;network</span>
<span id="and-by-paired-devices" hidden> and&nbsp;by&nbsp;<span id="paired-devices">paired&nbsp;devices</span></span>
@ -148,7 +152,7 @@
<h2 class="center">PairDrop</h2>
<div class="text-center file-description">
<div>
<span id="requesting-peer-display-name"></span>
<span class="display-name"></span>
<span>would like to share</span>
</div>
<div id="file-name" class="row" >
@ -193,7 +197,7 @@
<h2 class="text-center">PairDrop</h2>
<div class="text-center">
<span>Send a Message to</span>
<span id="text-send-peer-display-name"></span>
<span class="display-name"></span>
</div>
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="row-reverse">
@ -211,8 +215,8 @@
<x-paper shadow="2">
<h2>PairDrop - Message Received</h2>
<div id="receive-text-description-container">
<span id="receive-text-peer-display-name"></span>
<span>sent the following message:</span>
<span class="display-name"></span>
<span>sent a message:</span>
</div>
<div class="row-separator"></div>
<div id="text"></div>
@ -329,6 +333,10 @@
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
<path d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L489.3 358.2l90.5-90.5c56.5-56.5 56.5-148 0-204.5c-50-50-128.8-56.5-186.3-15.4l-1.6 1.1c-14.4 10.3-17.7 30.3-7.4 44.6s30.3 17.7 44.6 7.4l1.6-1.1c32.1-22.9 76-19.3 103.8 8.6c31.5 31.5 31.5 82.5 0 114l-96 96-31.9-25C430.9 239.6 420.1 175.1 377 132c-52.2-52.3-134.5-56.2-191.3-11.7L38.8 5.1zM239 162c30.1-14.9 67.7-9.9 92.8 15.3c20 20 27.5 48.3 21.7 74.5L239 162zM406.6 416.4L220.9 270c-2.1 39.8 12.2 80.1 42.2 110c38.9 38.9 94.4 51 143.6 36.3zm-290-228.5L60.2 244.3c-56.5 56.5-56.5 148 0 204.5c50 50 128.8 56.5 186.3 15.4l1.6-1.1c14.4-10.3 17.7-30.3 7.4-44.6s-30.3-17.7-44.6-7.4l-1.6 1.1c-32.1 22.9-76 19.3-103.8-8.6C74 372 74 321 105.5 289.5l61.8-61.8-50.6-39.9z"/>
</symbol>
<symbol id="edit-pen-icon" viewBox="0 0 512 512">
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path d="M362.7 19.3L314.3 67.7 444.3 197.7l48.4-48.4c25-25 25-65.5 0-90.5L453.3 19.3c-25-25-65.5-25-90.5 0zm-71 71L58.6 323.5c-10.4 10.4-18 23.3-22.2 37.4L1 481.2C-1.5 489.7 .8 498.8 7 505s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L421.7 220.3 291.7 90.3z"/>
</symbol>
</svg>
<!-- Scripts -->
<script src="scripts/util.js"></script>

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();
});
}
}

View file

@ -477,6 +477,7 @@ x-peer.ws-peer .highlight-wrapper {
}
.device-descriptor {
width: 100%;
text-align: center;
}
@ -583,6 +584,28 @@ footer .font-body2 {
padding-bottom: 1px;
}
#display-name {
display: inline-block;
text-align: left;
padding-right: 1rem;
border: none;
outline: none;
max-width: 18em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: -5px;
}
#edit-pen {
width: 1rem;
height: 1rem;
margin-left: -1rem;
margin-bottom: -2px;
position: relative;
z-index: -1;
}
/* Dialog */
x-dialog x-background {
@ -1038,11 +1061,11 @@ button::-moz-focus-inner {
x-toast {
position: absolute;
min-height: 48px;
bottom: 24px;
top: 50px;
width: 100%;
max-width: 344px;
background-color: #323232;
color: rgba(255, 255, 255, 0.95);
background-color: rgb(var(--text-color));
color: rgb(var(--bg-color));
align-items: center;
box-sizing: border-box;
padding: 8px 24px;
@ -1056,7 +1079,7 @@ x-toast {
x-toast:not([show]):not(:hover) {
opacity: 0;
transform: translateY(100px);
transform: translateY(-100px);
}