From abf96c02282c5171793f5487880918f4719e8f15 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 16:36:59 +0200 Subject: [PATCH 01/34] Test --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index fec4428..eb9f9c4 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# Snapdrop +# Snapdrop -[Snapdrop](https://snapdrop.net) is a Progressive Web App inspired by Apple's Airdrop. +[Snapdrop](https://snapdrop.net): local file sharing in your browser - inspired by Apple's Airdrop. -#### Snapdrop is built with the following awesome technologies: -* Vanilla HTML / JS / CSS +#### Snapdrop Version 2 is built with the following awesome technologies: +* Vanilla HTML5 / ES6 / CSS3 +* Progressive Web App * [WebRTC](http://webrtc.org/) * [WebSockets](http://www.websocket.org/) fallback (iDevices don't support WebRTC) * [NodeJS](https://nodejs.org/en/) @@ -24,7 +25,6 @@ It uses a P2P connection if WebRTC is supported by the browser. (WebRTC needs a If WebRTC isn’t supported (Safari, IE) it uses a Web Sockets fallback for the file transfer. The server connects the clients with a stream. - ##### What about privacy? Will files be saved on third-party-servers? None of your files are ever saved on any server. Snapdrop doesn't even use cookies or a database. If you are curious have a look [at the Server](https://github.com/RobinLinus/snapdrop/blob/master/server/ws-server.js). @@ -50,7 +50,7 @@ ShareDrop uses WebRTC only and isn't compatible with Safari Browsers. Snapdrop u npm install node index.js cd ../client - python -m http.server + python -m SimpleHTTPServer ``` Now point your browser to http://localhost:8000. From 36ec13d4285b1d8633cc3bddd26df8fddbb8e59e Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 18:53:31 +0200 Subject: [PATCH 02/34] Fix uncaught error in server --- README.md | 4 ++-- client/scripts/network-v2.js | 34 ---------------------------------- server/index.js | 2 +- 3 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 client/scripts/network-v2.js diff --git a/README.md b/README.md index eb9f9c4..0744ba4 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ ShareDrop uses WebRTC only and isn't compatible with Safari Browsers. Snapdrop u ## Local Development ``` - git clone git@github.com:RobinLinus/secret-snapdrop.git - cd secret-snapdrop/server + git clone git@github.com:RobinLinus/snapdrop.git + cd snapdrop/server npm install node index.js cd ../client diff --git a/client/scripts/network-v2.js b/client/scripts/network-v2.js deleted file mode 100644 index 7a15cbd..0000000 --- a/client/scripts/network-v2.js +++ /dev/null @@ -1,34 +0,0 @@ -class ServerConnection { - -} - -class Connection { - -} - -class WSConnection extends Connection { - -} - -class RTCConnection extends Connection { - -} - -class Peer { - - constructor(serverConnection) { - this._ws = new WSConnection(serverConnection); - this._rtc = new RTCConnection(serverConnection); - this._fileReceiver = new FileReceiver(this); - this._fileSender = new FileSender(this); - } - - send(message) { - - } - -} - -class Peers { - -} \ No newline at end of file diff --git a/server/index.js b/server/index.js index 2d8bbfd..52ecc7a 100644 --- a/server/index.js +++ b/server/index.js @@ -39,7 +39,7 @@ class SnapdropServer { } // relay message to recipient - if (message.to) { + if (message.to && this._rooms[sender.ip]) { const recipientId = message.to; // TODO: sanitize const recipient = this._rooms[sender.ip][recipientId]; delete message.to; From e9eeea48e5ea76214971daa1a879b0ae2ba1a196 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 18:54:52 +0200 Subject: [PATCH 03/34] Fix notifications on android --- client/scripts/network.js | 6 ++++++ client/scripts/ui.js | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index e7ebd87..e254fe0 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -3,9 +3,11 @@ class ServerConnection { constructor() { this._connect(); Events.on('beforeunload', e => this._disconnect(), false); + Events.on('pageshow', e => this._connect(), false); } _connect() { + if (this._isConnected()) return const ws = new WebSocket(this._endpoint()); ws.binaryType = 'arraybuffer'; ws.onopen = e => console.log('WS: server connection opened'); @@ -16,6 +18,10 @@ class ServerConnection { clearTimeout(this._reconnectTimer); } + _isConnected(){ + return this._socket && this._socket.readyState === this._socket.OPEN; + } + _onMessage(msg) { msg = JSON.parse(msg); console.log('WS:', msg); diff --git a/client/scripts/ui.js b/client/scripts/ui.js index 45c9483..29b7a41 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -358,12 +358,16 @@ class Notifications { } _notify(message, body) { - var img = '/images/logo_transparent_128x128.png'; - return new Notification(message, { + const config = { body: body, - icon: img, + icon: '/images/logo_transparent_128x128.png', vibrate: [200, 100, 200, 100, 200, 100, 400], - }); + } + if (serviceWorker && serviceWorker.showNotification) { + return serviceWorker.showNotification(message, config); + } else { + return new Notification(message, config); + } } _messageNotification(message) { @@ -434,10 +438,14 @@ document.copy = text => { return success; } -if ('serviceWorker' in navigator && isProductionEnvironment) { + +if ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service-worker.js') - .then(e => console.log("Service Worker Registered")); + .then(serviceWorker => { + console.log('Service Worker registered'); + window.serviceWorker = serviceWorker + }); } // Background Animation From 92a5f3b782b4037beaf9b404683239cf71105586 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 19:14:25 +0200 Subject: [PATCH 04/34] Cleanup; fix STUN servers --- client/scripts/network.js | 31 ++++++++++++++++--------------- client/scripts/ui.js | 3 ++- server/index.js | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index e254fe0..6bc4a1f 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -10,7 +10,7 @@ class ServerConnection { if (this._isConnected()) return const ws = new WebSocket(this._endpoint()); ws.binaryType = 'arraybuffer'; - ws.onopen = e => console.log('WS: server connection opened'); + ws.onopen = e => console.log('WS: server connected'); ws.onmessage = e => this._onMessage(e.data); ws.onclose = e => this._onDisconnect(); ws.onerror = e => console.error(e); @@ -18,7 +18,7 @@ class ServerConnection { clearTimeout(this._reconnectTimer); } - _isConnected(){ + _isConnected() { return this._socket && this._socket.readyState === this._socket.OPEN; } @@ -243,7 +243,7 @@ class RTCPeer extends Peer { _createChannel() { const channel = this._peer.createDataChannel('data-channel', { reliable: true }); channel.binaryType = 'arraybuffer'; - channel.onopen = e => this._onChannelOpened(e) + channel.onopen = e => this._onChannelOpened(e); this._peer.createOffer(d => this._onDescription(d), e => this._onError(e)); } @@ -466,16 +466,17 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti RTCPeer.config = { 'iceServers': [{ - urls: 'stun:stun.stunprotocol.org:3478' - }, { - urls: 'stun:stun.l.google.com:19302' - }, { - urls: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }, { - urls: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }] + urls: 'stun:stun.stunprotocol.org:3478' + }, { + urls: 'stun:stun.l.google.com:19302' + }, { + url: 'turn:192.158.29.39:3478?transport=tcp', + credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', + username: '28224511:1379330808' + } { + urls: 'turn:turn.anyfirewall.com:443?transport=tcp', + credential: 'webrtc', + username: 'webrtc' + } + ] } \ No newline at end of file diff --git a/client/scripts/ui.js b/client/scripts/ui.js index 29b7a41..fa34e69 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -335,8 +335,8 @@ class Notifications { constructor() { // Check if the browser supports notifications if (!('Notification' in window)) return; + // Check whether notification permissions have already been granted - if (Notification.permission !== 'granted') { this.$button = $('notification'); this.$button.removeAttribute('hidden'); @@ -364,6 +364,7 @@ class Notifications { vibrate: [200, 100, 200, 100, 200, 100, 400], } if (serviceWorker && serviceWorker.showNotification) { + // android doesn't support "new Notification" if service worker is installed return serviceWorker.showNotification(message, config); } else { return new Notification(message, config); diff --git a/server/index.js b/server/index.js index 52ecc7a..c50b4d8 100644 --- a/server/index.js +++ b/server/index.js @@ -84,7 +84,7 @@ class SnapdropServer { _leaveRoom(peer) { // delete the peer this._cancelKeepAlive(peer); - if (!this._rooms[peer.ip]) return; + if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return; delete this._rooms[peer.ip][peer.id]; From 6317c25b10f3d885c8ccc3374fb1e873e2c18f44 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 19:14:25 +0200 Subject: [PATCH 05/34] Cleanup; fix STUN servers --- client/scripts/network.js | 31 ++++++++++++++++--------------- client/scripts/ui.js | 3 ++- server/index.js | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index e254fe0..ec67d8f 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -10,7 +10,7 @@ class ServerConnection { if (this._isConnected()) return const ws = new WebSocket(this._endpoint()); ws.binaryType = 'arraybuffer'; - ws.onopen = e => console.log('WS: server connection opened'); + ws.onopen = e => console.log('WS: server connected'); ws.onmessage = e => this._onMessage(e.data); ws.onclose = e => this._onDisconnect(); ws.onerror = e => console.error(e); @@ -18,7 +18,7 @@ class ServerConnection { clearTimeout(this._reconnectTimer); } - _isConnected(){ + _isConnected() { return this._socket && this._socket.readyState === this._socket.OPEN; } @@ -243,7 +243,7 @@ class RTCPeer extends Peer { _createChannel() { const channel = this._peer.createDataChannel('data-channel', { reliable: true }); channel.binaryType = 'arraybuffer'; - channel.onopen = e => this._onChannelOpened(e) + channel.onopen = e => this._onChannelOpened(e); this._peer.createOffer(d => this._onDescription(d), e => this._onError(e)); } @@ -466,16 +466,17 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti RTCPeer.config = { 'iceServers': [{ - urls: 'stun:stun.stunprotocol.org:3478' - }, { - urls: 'stun:stun.l.google.com:19302' - }, { - urls: 'turn:turn.bistri.com:80', - credential: 'homeo', - username: 'homeo' - }, { - urls: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - }] + urls: 'stun:stun.stunprotocol.org:3478' + }, { + urls: 'stun:stun.l.google.com:19302' + }, { + url: 'turn:192.158.29.39:3478?transport=tcp', + credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', + username: '28224511:1379330808' + }, { + urls: 'turn:turn.anyfirewall.com:443?transport=tcp', + credential: 'webrtc', + username: 'webrtc' + } + ] } \ No newline at end of file diff --git a/client/scripts/ui.js b/client/scripts/ui.js index 29b7a41..fa34e69 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -335,8 +335,8 @@ class Notifications { constructor() { // Check if the browser supports notifications if (!('Notification' in window)) return; + // Check whether notification permissions have already been granted - if (Notification.permission !== 'granted') { this.$button = $('notification'); this.$button.removeAttribute('hidden'); @@ -364,6 +364,7 @@ class Notifications { vibrate: [200, 100, 200, 100, 200, 100, 400], } if (serviceWorker && serviceWorker.showNotification) { + // android doesn't support "new Notification" if service worker is installed return serviceWorker.showNotification(message, config); } else { return new Notification(message, config); diff --git a/server/index.js b/server/index.js index 52ecc7a..c50b4d8 100644 --- a/server/index.js +++ b/server/index.js @@ -84,7 +84,7 @@ class SnapdropServer { _leaveRoom(peer) { // delete the peer this._cancelKeepAlive(peer); - if (!this._rooms[peer.ip]) return; + if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return; delete this._rooms[peer.ip][peer.id]; From e573d5741979fa48fdbfebed4f3cedd7f82f9ea5 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 19:17:07 +0200 Subject: [PATCH 06/34] STUN server tests --- client/scripts/network.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index ec67d8f..948db6c 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -18,7 +18,7 @@ class ServerConnection { clearTimeout(this._reconnectTimer); } - _isConnected() { + _isConnected(){ return this._socket && this._socket.readyState === this._socket.OPEN; } @@ -466,17 +466,16 @@ window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnecti RTCPeer.config = { 'iceServers': [{ - urls: 'stun:stun.stunprotocol.org:3478' - }, { - urls: 'stun:stun.l.google.com:19302' - }, { - url: 'turn:192.158.29.39:3478?transport=tcp', - credential: 'JZEOEt2V3Qb0y27GRntt2u2PAYA=', - username: '28224511:1379330808' - }, { - urls: 'turn:turn.anyfirewall.com:443?transport=tcp', - credential: 'webrtc', - username: 'webrtc' - } - ] + urls: 'stun:stun.stunprotocol.org:3478' + }, { + urls: 'stun:stun.l.google.com:19302' + }, { + urls: 'turn:turn.bistri.com:80', + credential: 'homeo', + username: 'homeo' + }, { + urls: 'turn:turn.anyfirewall.com:443?transport=tcp', + credential: 'webrtc', + username: 'webrtc' + }] } \ No newline at end of file From 31e5f635d19a69ebe719194aeb8465368e9f672d Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 19:24:01 +0200 Subject: [PATCH 07/34] Add connection state handler --- client/scripts/network.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index 948db6c..4831717 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -18,7 +18,7 @@ class ServerConnection { clearTimeout(this._reconnectTimer); } - _isConnected(){ + _isConnected() { return this._socket && this._socket.readyState === this._socket.OPEN; } @@ -230,7 +230,7 @@ class RTCPeer extends Peer { this._peerId = peerId; this._peer = new RTCPeerConnection(RTCPeer.config); this._peer.onicecandidate = e => this._onIceCandidate(e); - this._peer.onconnectionstatechange = e => console.log('RTC: state changed:', this._peer.connectionState); + this._peer.onconnectionstatechange = e => this._onConnectionStateChange(e); } if (isCaller) { @@ -288,11 +288,19 @@ class RTCPeer extends Peer { } _onChannelClosed() { - console.log('RTC: channel closed ', this._peerId); + console.log('RTC: channel closed', this._peerId); if (!this.isCaller) return; this._start(this._peerId, true); // reopen the channel } + _onConnectionStateChange(e) { + console.log('RTC: state changed:', this._peer.connectionState); + switch (this._peer.connectionState) { + 'disconnected': this._onChannelClosed(); + break; + } + } + _send(message) { this._channel.send(message); } From 96e37aef40c7a69203b172655b290732dc236dc3 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 19:25:01 +0200 Subject: [PATCH 08/34] Fix typo --- client/scripts/network.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index 4831717..fdc1f8c 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -296,7 +296,7 @@ class RTCPeer extends Peer { _onConnectionStateChange(e) { console.log('RTC: state changed:', this._peer.connectionState); switch (this._peer.connectionState) { - 'disconnected': this._onChannelClosed(); + case 'disconnected': this._onChannelClosed(); break; } } From 728aabd449ed8f70231c643d756488fef7431aae Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 19:32:59 +0200 Subject: [PATCH 09/34] Fix typo in server --- server/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/index.js b/server/index.js index c50b4d8..f35a299 100644 --- a/server/index.js +++ b/server/index.js @@ -34,8 +34,10 @@ class SnapdropServer { switch (message.type) { case 'disconnect': this._leaveRoom(sender); + break; case 'pong': sender.lastBeat = Date.now(); + break; } // relay message to recipient From 4cf2beda9075d75e1efc6dca3dc0e390612882fd Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 19:49:16 +0200 Subject: [PATCH 10/34] Fix beforeunload on iphone --- client/scripts/network.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index fdc1f8c..c986092 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -3,11 +3,11 @@ class ServerConnection { constructor() { this._connect(); Events.on('beforeunload', e => this._disconnect(), false); - Events.on('pageshow', e => this._connect(), false); + Events.on('pagehide', e => this._disconnect(), false); } _connect() { - if (this._isConnected()) return + if (this._isConnected()) return; const ws = new WebSocket(this._endpoint()); ws.binaryType = 'arraybuffer'; ws.onopen = e => console.log('WS: server connected'); From 61697d3abc5430ad58478b12a9b7fc36d8978881 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 20:01:59 +0200 Subject: [PATCH 11/34] Cancel keep alive on join room --- server/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/index.js b/server/index.js index 7cc3626..025ab45 100644 --- a/server/index.js +++ b/server/index.js @@ -53,11 +53,13 @@ class SnapdropServer { } _joinRoom(peer) { - this._cancelKeepAlive(peer); // if room doesn't exist, create it if (!this._rooms[peer.ip]) { this._rooms[peer.ip] = {}; } + if (this._rooms[peer.ip][peer.id]) { + this._cancelKeepAlive(this._rooms[peer.ip][peer.id]); + } // console.log(peer.id, ' joined the room', peer.ip); // notify all other peers @@ -128,6 +130,7 @@ class SnapdropServer { type: 'ping' }); } + this._cancelKeepAlive(peer); peer.timerId = setTimeout(() => this._keepAlive(peer), timeout); } From 0731a21d685c35e45985a39e3df32549730953db Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 20:09:49 +0200 Subject: [PATCH 12/34] Cancel keep alive on join room --- server/index.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/index.js b/server/index.js index 025ab45..4f78674 100644 --- a/server/index.js +++ b/server/index.js @@ -111,6 +111,7 @@ class SnapdropServer { _send(peer, message) { if (!peer) return console.error('undefined peer'); + if (this._wss.readyState !== this._wss.OPEN) return console.error('Socket is closed'); message = JSON.stringify(message); peer.socket.send(message, error => { if (error) this._leaveRoom(peer); @@ -119,17 +120,16 @@ class SnapdropServer { _keepAlive(peer) { var timeout = 10000; - // console.log(Date.now() - peer.lastBeat); + if (!peer.lastBeat) { + peer.lastBeat = Date.now(); + } if (Date.now() - peer.lastBeat > 2 * timeout) { this._leaveRoom(peer); return; } - if (this._wss.readyState == this._wss.OPEN) { - this._send(peer, { - type: 'ping' - }); - } + this._send(peer, { type: 'ping' }); + this._cancelKeepAlive(peer); peer.timerId = setTimeout(() => this._keepAlive(peer), timeout); } @@ -137,6 +137,7 @@ class SnapdropServer { _cancelKeepAlive(peer) { if (peer.timerId) { clearTimeout(peer.timerId); + peer.lastBeat = null; } } } From e71564a97cc083b606f144db4de3e35e5e172bfb Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 20:15:55 +0200 Subject: [PATCH 13/34] Cancel keep alive on join room --- server/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/index.js b/server/index.js index 4f78674..b6c4ab7 100644 --- a/server/index.js +++ b/server/index.js @@ -87,10 +87,10 @@ class SnapdropServer { } _leaveRoom(peer) { - this._cancelKeepAlive(peer); - // delete the peer if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return; + this._cancelKeepAlive(peer); + // delete the peer delete this._rooms[peer.ip][peer.id]; peer.socket.terminate(); @@ -137,7 +137,6 @@ class SnapdropServer { _cancelKeepAlive(peer) { if (peer.timerId) { clearTimeout(peer.timerId); - peer.lastBeat = null; } } } From 1d9581632fe3a99c2068d8c0cece9345c9397cc8 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 20:25:54 +0200 Subject: [PATCH 14/34] Cancel keep alive on join room --- server/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/index.js b/server/index.js index b6c4ab7..17e6b07 100644 --- a/server/index.js +++ b/server/index.js @@ -11,7 +11,6 @@ class SnapdropServer { this._wss.on('headers', (headers, response) => this._onHeaders(headers, response)); this._rooms = {}; - this._timerID = 0; console.log('Snapdrop is running on port', port); } @@ -119,6 +118,7 @@ class SnapdropServer { } _keepAlive(peer) { + this._cancelKeepAlive(peer); var timeout = 10000; if (!peer.lastBeat) { peer.lastBeat = Date.now(); @@ -130,7 +130,6 @@ class SnapdropServer { this._send(peer, { type: 'ping' }); - this._cancelKeepAlive(peer); peer.timerId = setTimeout(() => this._keepAlive(peer), timeout); } From 0ede41f8d5397e8b7beb62ba6abe2571296f4690 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 20:34:49 +0200 Subject: [PATCH 15/34] Cancel keep alive on join room --- server/index.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/server/index.js b/server/index.js index 17e6b07..f231651 100644 --- a/server/index.js +++ b/server/index.js @@ -56,9 +56,6 @@ class SnapdropServer { if (!this._rooms[peer.ip]) { this._rooms[peer.ip] = {}; } - if (this._rooms[peer.ip][peer.id]) { - this._cancelKeepAlive(this._rooms[peer.ip][peer.id]); - } // console.log(peer.id, ' joined the room', peer.ip); // notify all other peers @@ -87,7 +84,7 @@ class SnapdropServer { _leaveRoom(peer) { if (!this._rooms[peer.ip] || !this._rooms[peer.ip][peer.id]) return; - this._cancelKeepAlive(peer); + this._cancelKeepAlive(this._rooms[peer.ip][peer.id]); // delete the peer delete this._rooms[peer.ip][peer.id]; @@ -112,9 +109,7 @@ class SnapdropServer { if (!peer) return console.error('undefined peer'); if (this._wss.readyState !== this._wss.OPEN) return console.error('Socket is closed'); message = JSON.stringify(message); - peer.socket.send(message, error => { - if (error) this._leaveRoom(peer); - }); + peer.socket.send(message, error => console.log(error)); } _keepAlive(peer) { @@ -134,7 +129,7 @@ class SnapdropServer { } _cancelKeepAlive(peer) { - if (peer.timerId) { + if (peer && peer.timerId) { clearTimeout(peer.timerId); } } From 7194c65c74681883089d21c574275dfc01864d50 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 20:51:56 +0200 Subject: [PATCH 16/34] Reconnect on rejoin room --- client/scripts/network.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index c986092..90ae29e 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -4,6 +4,8 @@ class ServerConnection { this._connect(); Events.on('beforeunload', e => this._disconnect(), false); Events.on('pagehide', e => this._disconnect(), false); + document.addEventListener('visibilitychange', e => this._onVisibilityChange()); + } _connect() { @@ -71,6 +73,11 @@ class ServerConnection { clearTimeout(this._reconnectTimer); this._reconnectTimer = setTimeout(_ => this._connect(), 5000); } + + _onVisibilityChange() { + if (document.hidden) return; + this._connect(); + } } class Peer { @@ -296,8 +303,13 @@ class RTCPeer extends Peer { _onConnectionStateChange(e) { console.log('RTC: state changed:', this._peer.connectionState); switch (this._peer.connectionState) { - case 'disconnected': this._onChannelClosed(); - break; + case 'disconnected': + this._onChannelClosed(); + break; + case 'failed': + this._peer = null; + this._onChannelClosed(); + break; } } From 9c9ca70d05fb2894d4e8113acc3a99a7b582ec78 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 20:55:15 +0200 Subject: [PATCH 17/34] Reconnect on rejoin room --- client/scripts/network.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index 90ae29e..bf30fe5 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -324,7 +324,7 @@ class RTCPeer extends Peer { refresh() { // check if channel open. otherwise create one if (this._peer && this._channel && this._channel.readyState !== 'open') return; - this._createChannel(this._peerId, this._isCaller); + this._start(this._peerId, this._isCaller); } } From 3ac40fb3d7467df6c0f66b0d156202cbfbf80f09 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 21:04:48 +0200 Subject: [PATCH 18/34] =?UTF-8?q?Don=E2=80=99t=20reconnect=20if=20already?= =?UTF-8?q?=20connecting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/scripts/network.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index bf30fe5..98c43ac 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -9,7 +9,7 @@ class ServerConnection { } _connect() { - if (this._isConnected()) return; + if (this._isConnected() || this._isConnecting()) return; const ws = new WebSocket(this._endpoint()); ws.binaryType = 'arraybuffer'; ws.onopen = e => console.log('WS: server connected'); @@ -24,6 +24,10 @@ class ServerConnection { return this._socket && this._socket.readyState === this._socket.OPEN; } + _isConnecting() { + return this._socket && this._socket.readyState === this._socket.CONNECTING; + } + _onMessage(msg) { msg = JSON.parse(msg); console.log('WS:', msg); From 600d3551f4765d65a4348a33012b57ee370b3da3 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 21:12:11 +0200 Subject: [PATCH 19/34] Add will-change --- client/scripts/network.js | 2 +- client/styles.css | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/scripts/network.js b/client/scripts/network.js index 98c43ac..6e61a78 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -9,6 +9,7 @@ class ServerConnection { } _connect() { + clearTimeout(this._reconnectTimer); if (this._isConnected() || this._isConnecting()) return; const ws = new WebSocket(this._endpoint()); ws.binaryType = 'arraybuffer'; @@ -17,7 +18,6 @@ class ServerConnection { ws.onclose = e => this._onDisconnect(); ws.onerror = e => console.error(e); this._socket = ws; - clearTimeout(this._reconnectTimer); } _isConnected() { diff --git a/client/styles.css b/client/styles.css index 517e727..f6e84b5 100644 --- a/client/styles.css +++ b/client/styles.css @@ -301,6 +301,7 @@ x-dialog x-background { background: rgba(0, 0, 0, 0.61); z-index: 10; transition: opacity 300ms; + will-change: opacity; padding: 16px; } @@ -313,6 +314,7 @@ x-dialog x-paper { max-width: 400px; box-sizing: border-box; transition: transform 300ms; + will-change: transform; } x-dialog:not([show]) { From 5a631d3833a63e76d3611dc97ec073e471af58ba Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 21:21:44 +0200 Subject: [PATCH 20/34] Notfication API on android --- client/scripts/ui.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/scripts/ui.js b/client/scripts/ui.js index fa34e69..1b90a8f 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -361,7 +361,8 @@ class Notifications { const config = { body: body, icon: '/images/logo_transparent_128x128.png', - vibrate: [200, 100, 200, 100, 200, 100, 400], + // vibrate: [200, 100, 200, 100, 200, 100, 400], + // requireInteraction: true } if (serviceWorker && serviceWorker.showNotification) { // android doesn't support "new Notification" if service worker is installed From 476cb0ae6525177d7ea0519c9f0c4de67d2bf0f3 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 22:31:46 +0200 Subject: [PATCH 21/34] Notifications --- client/scripts/ui.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/client/scripts/ui.js b/client/scripts/ui.js index 1b90a8f..9bd7c14 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -335,7 +335,7 @@ class Notifications { constructor() { // Check if the browser supports notifications if (!('Notification' in window)) return; - + // Check whether notification permissions have already been granted if (Notification.permission !== 'granted') { this.$button = $('notification'); @@ -361,11 +361,12 @@ class Notifications { const config = { body: body, icon: '/images/logo_transparent_128x128.png', - // vibrate: [200, 100, 200, 100, 200, 100, 400], - // requireInteraction: true } if (serviceWorker && serviceWorker.showNotification) { // android doesn't support "new Notification" if service worker is installed + config.actions = { + { "action": "yes", "title": "Yes"} + }; return serviceWorker.showNotification(message, config); } else { return new Notification(message, config); @@ -385,11 +386,20 @@ class Notifications { _downloadNotification(message) { const notification = this._notify(message, 'Click to download'); if (window.isDownloadSupported) return; - notification.onclick = e => { - document.querySelector('x-dialog [download]').click(); - }; + notification.onclick = e => this._download(e); } + _download(e) { + document.querySelector('x-dialog [download]').click(); + e.target.close(); + } + + _copyText(notification, message) { + console.log('message'); + document.copy(message); + this._notify('Copied to clipboard'); + notification.close(); + } } class Snapdrop { From f537b9621350fd1ea6694e7a2a8d3eca1edbf012 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 22:32:39 +0200 Subject: [PATCH 22/34] Notifications --- client/scripts/ui.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/scripts/ui.js b/client/scripts/ui.js index 9bd7c14..c3c4cd9 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -364,9 +364,9 @@ class Notifications { } if (serviceWorker && serviceWorker.showNotification) { // android doesn't support "new Notification" if service worker is installed - config.actions = { + config.actions = [ { "action": "yes", "title": "Yes"} - }; + ]; return serviceWorker.showNotification(message, config); } else { return new Notification(message, config); From 52bd7692e951c5fafdcdb182a69646b78f03884d Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Fri, 21 Sep 2018 23:19:54 +0200 Subject: [PATCH 23/34] Notifications Android & Desktop --- client/scripts/ui.js | 50 +++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/client/scripts/ui.js b/client/scripts/ui.js index c3c4cd9..42ca141 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -1,8 +1,8 @@ const $ = query => document.getElementById(query); const $$ = query => document.body.querySelector(query); const isURL = text => /^((https?:\/\/|www)[^\s]+)/g.test(text.toLowerCase()); -const isDownloadSupported = (typeof document.createElement('a').download !== 'undefined'); -const isProductionEnvironment = !window.location.host.startsWith('localhost'); +window.isDownloadSupported = (typeof document.createElement('a').download !== 'undefined'); +window.isProductionEnvironment = !window.location.host.startsWith('localhost'); class PeersUI { @@ -236,10 +236,8 @@ class ReceiveDialog extends Dialog { this.$el.querySelector('#fileSize').textContent = this._formatFileSize(file.size); this.show(); - if (!isDownloadSupported) return; - // $a.target = "_blank"; // fallback - $a.target = "_system"; // fallback - $a.href = 'external:' + $a.href; + if (window.isDownloadSupported) return; + $a.target = "_blank"; // fallback } _formatFileSize(bytes) { @@ -362,43 +360,51 @@ class Notifications { body: body, icon: '/images/logo_transparent_128x128.png', } - if (serviceWorker && serviceWorker.showNotification) { - // android doesn't support "new Notification" if service worker is installed - config.actions = [ - { "action": "yes", "title": "Yes"} - ]; - return serviceWorker.showNotification(message, config); - } else { + try { return new Notification(message, config); + } catch (e) { + // android doesn't support "new Notification" if service worker is installed + if (!serviceWorker || !serviceWorker.showNotification) return; + return serviceWorker.showNotification(message, config); } + } _messageNotification(message) { if (isURL(message)) { const notification = this._notify(message, 'Click to open link'); - notification.onclick = e => window.open(message, '_blank', null, true); + this._bind(notification, e => window.open(message, '_blank', null, true)); } else { const notification = this._notify(message, 'Click to copy text'); - notification.onclick = e => document.copy(message); + this._bind(notification, e => this._copyText(message, notification)); } } _downloadNotification(message) { const notification = this._notify(message, 'Click to download'); - if (window.isDownloadSupported) return; - notification.onclick = e => this._download(e); + if (!window.isDownloadSupported) return; + this._bind(notification, e => this._download(notification)); } - _download(e) { + _download(notification) { document.querySelector('x-dialog [download]').click(); - e.target.close(); + notification.close(); } - _copyText(notification, message) { - console.log('message'); + _copyText(message, notification) { document.copy(message); - this._notify('Copied to clipboard'); notification.close(); + this._notify('Copied text to clipboard'); + } + + _bind(notification, handler) { + if (notification.then) { + notification.then(e => serviceWorker.getNotifications().then(notifications => { + serviceWorker.addEventListener('notificationclick', handler); + })); + } else { + notification.onclick = handler; + } } } From 04415ef28f8e7281c13546f168f2582a82bcb34f Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Sat, 22 Sep 2018 04:44:17 +0200 Subject: [PATCH 24/34] Cleanup --- README.md | 16 ++--- client/scripts/network.js | 122 +++++++++++++++++++------------------- client/scripts/ui.js | 2 +- client/styles.css | 1 + 4 files changed, 73 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 0744ba4..5cb278f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [Snapdrop](https://snapdrop.net): local file sharing in your browser - inspired by Apple's Airdrop. -#### Snapdrop Version 2 is built with the following awesome technologies: +#### Snapdrop (Version 2) is built with the following awesome technologies: * Vanilla HTML5 / ES6 / CSS3 * Progressive Web App * [WebRTC](http://webrtc.org/) @@ -18,12 +18,12 @@ * [idownloadblog](http://www.idownloadblog.com/2015/12/29/snapdrop/) * [thenextweb](http://thenextweb.com/insider/2015/12/27/snapdrop-is-a-handy-web-based-replacement-for-apples-fiddly-airdrop-file-transfer-tool/) * [winboard](http://www.winboard.org/artikel-ratgeber/6253-dateien-vom-desktop-pc-mit-anderen-plattformen-teilen-mit-snapdrop.html) -* [免費資源網路社群](https://free.com.tw/snapdrop/?utm_content=buffere6987&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer) +* [免費資源網路社群](https://free.com.tw/snapdrop/) ##### What about the connection? Is it a P2P-connection directly from device to device or is there any third-party-server? It uses a P2P connection if WebRTC is supported by the browser. (WebRTC needs a Signaling Server, but it is only used to establish a connection and is not involved in the file transfer). -If WebRTC isn’t supported (Safari, IE) it uses a Web Sockets fallback for the file transfer. The server connects the clients with a stream. +If WebRTC isn’t supported (Safari, IE) it uses a Web Sockets fallback for the file transfer. The server connects the clients with each other. ##### What about privacy? Will files be saved on third-party-servers? None of your files are ever saved on any server. @@ -33,15 +33,15 @@ But it does use Google Analytics. ##### Is SnapDrop a fork of ShareDrop? No. ShareDrop is built with Ember. Snapdrop is built with vanilla ES6. I wanted to play around with Progressive Web Apps and then I got the idea of a local file sharing app. By doing research on this idea I found and analysed ShareDrop. I liked it and thought about how to improve it. -ShareDrop uses WebRTC only and isn't compatible with Safari Browsers. Snapdrop uses a Websocket fallback and some hacks to make Snapdrop work due to the download restrictions on iDevices. +ShareDrop uses WebRTC only and isn't compatible with Safari browsers. Snapdrop uses a Websocket fallback and some hacks to make Snapdrop work due to the download restrictions on iDevices. ### Snapdrop is awesome! How can I support it? * [File bugs, give feedback, submit suggestions](https://github.com/RobinLinus/snapdrop/issues) * Share Snapdrop on your social media. * [Buy me a cup of coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R9C5E42UYEQCN) -* Fix bugs and make a Pull Request. This is my first open source project, so I am not very used to the common workflow, but we'll figure it out! -* Do Security Analysis and suggestions +* Fix bugs and make a pull request. +* Do security analysis and suggestions ## Local Development ``` @@ -49,7 +49,9 @@ ShareDrop uses WebRTC only and isn't compatible with Safari Browsers. Snapdrop u cd snapdrop/server npm install node index.js - cd ../client + + # open a second shell: + cd snapdrop/client python -m SimpleHTTPServer ``` diff --git a/client/scripts/network.js b/client/scripts/network.js index 6e61a78..ff113f3 100644 --- a/client/scripts/network.js +++ b/client/scripts/network.js @@ -20,14 +20,6 @@ class ServerConnection { this._socket = ws; } - _isConnected() { - return this._socket && this._socket.readyState === this._socket.OPEN; - } - - _isConnecting() { - return this._socket && this._socket.readyState === this._socket.CONNECTING; - } - _onMessage(msg) { msg = JSON.parse(msg); console.log('WS:', msg); @@ -82,6 +74,14 @@ class ServerConnection { if (document.hidden) return; this._connect(); } + + _isConnected() { + return this._socket && this._socket.readyState === this._socket.OPEN; + } + + _isConnecting() { + return this._socket && this._socket.readyState === this._socket.CONNECTING; + } } class Peer { @@ -192,16 +192,12 @@ class Peer { } _onDownloadProgress(progress) { - Events.fire('file-progress', { - sender: this._peerId, - progress: progress - }); + Events.fire('file-progress', { sender: this._peerId, progress: progress }); } _onFileReceived(proxyFile) { Events.fire('file-received', proxyFile); this.sendJSON({ type: 'transfer-complete' }); - // this._digester = null; } _onTransferCompleted() { @@ -213,17 +209,13 @@ class Peer { } sendText(text) { - this.sendJSON({ - type: 'text', - text: btoa(unescape(encodeURIComponent(text))) - }); + const unescaped = btoa(unescape(encodeURIComponent(text))); + this.sendJSON({ type: 'text', text: unescaped }); } _onTextReceived(message) { - Events.fire('text-received', { - text: decodeURIComponent(escape(atob(message.text))), - sender: this._peerId - }); + const escaped = decodeURIComponent(escape(atob(message.text))); + Events.fire('text-received', { text: escaped, sender: this._peerId }); } } @@ -232,35 +224,37 @@ class RTCPeer extends Peer { constructor(serverConnection, peerId) { super(serverConnection, peerId); if (!peerId) return; // we will listen for a caller - this._start(peerId, true); + this._connect(peerId, true); } - _start(peerId, isCaller) { - if (!this._peer) { - this._isCaller = isCaller; - this._peerId = peerId; - this._peer = new RTCPeerConnection(RTCPeer.config); - this._peer.onicecandidate = e => this._onIceCandidate(e); - this._peer.onconnectionstatechange = e => this._onConnectionStateChange(e); - } + _connect(peerId, isCaller) { + if (!this._conn) this._openConnection(peerId, isCaller); if (isCaller) { - this._createChannel(); + this._openChannel(); } else { - this._peer.ondatachannel = e => this._onChannelOpened(e); + this._conn.ondatachannel = e => this._onChannelOpened(e); } } - _createChannel() { - const channel = this._peer.createDataChannel('data-channel', { reliable: true }); + _openConnection(peerId, isCaller) { + this._isCaller = isCaller; + this._peerId = peerId; + this._conn = new RTCPeerConnection(RTCPeer.config); + this._conn.onicecandidate = e => this._onIceCandidate(e); + this._conn.onconnectionstatechange = e => this._onConnectionStateChange(e); + } + + _openChannel() { + const channel = this._conn.createDataChannel('data-channel', { reliable: true }); channel.binaryType = 'arraybuffer'; channel.onopen = e => this._onChannelOpened(e); - this._peer.createOffer(d => this._onDescription(d), e => this._onError(e)); + this._conn.createOffer(d => this._onDescription(d), e => this._onError(e)); } _onDescription(description) { // description.sdp = description.sdp.replace('b=AS:30', 'b=AS:1638400'); - this._peer.setLocalDescription(description, + this._conn.setLocalDescription(description, _ => this._sendSignal({ sdp: description }), e => this._onError(e)); } @@ -270,23 +264,16 @@ class RTCPeer extends Peer { this._sendSignal({ ice: event.candidate }); } - _sendSignal(signal) { - signal.type = 'signal'; - signal.to = this._peerId; - this._server.send(signal); - } - onServerMessage(message) { - if (!this._peer) this._start(message.sender, false); - const conn = this._peer; + if (!this._conn) this._connect(message.sender, false); if (message.sdp) { - this._peer.setRemoteDescription(new RTCSessionDescription(message.sdp), () => { + this._conn.setRemoteDescription(new RTCSessionDescription(message.sdp), () => { if (message.sdp.type !== 'offer') return; - this._peer.createAnswer(d => this._onDescription(d), e => this._onError(e)); + this._conn.createAnswer(d => this._onDescription(d), e => this._onError(e)); }, e => this._onError(e)); } else if (message.ice) { - this._peer.addIceCandidate(new RTCIceCandidate(message.ice)); + this._conn.addIceCandidate(new RTCIceCandidate(message.ice)); } } @@ -301,34 +288,48 @@ class RTCPeer extends Peer { _onChannelClosed() { console.log('RTC: channel closed', this._peerId); if (!this.isCaller) return; - this._start(this._peerId, true); // reopen the channel + this._connect(this._peerId, true); // reopen the channel } _onConnectionStateChange(e) { - console.log('RTC: state changed:', this._peer.connectionState); - switch (this._peer.connectionState) { + console.log('RTC: state changed:', this._conn.connectionState); + switch (this._conn.connectionState) { case 'disconnected': this._onChannelClosed(); break; case 'failed': - this._peer = null; + this._conn = null; this._onChannelClosed(); break; } } - _send(message) { - this._channel.send(message); - } - _onError(error) { console.error(error); } + _send(message) { + this._channel.send(message); + } + + _sendSignal(signal) { + signal.type = 'signal'; + signal.to = this._peerId; + this._server.send(signal); + } + refresh() { - // check if channel open. otherwise create one - if (this._peer && this._channel && this._channel.readyState !== 'open') return; - this._start(this._peerId, this._isCaller); + // check if channel is open. otherwise create one + if (this._isConnected() || this._isConnecting()) return; + this._connect(this._peerId, this._isCaller); + } + + _isConnected() { + return this._channel && this._channel.readyState === 'open'; + } + + _isConnecting() { + return this._channel && this._channel.readyState === 'connecting'; } } @@ -422,7 +423,7 @@ class FileChunker { this._partitionSize += chunk.byteLength; this._onChunk(chunk); if (this._isPartitionEnd() || this.isFileEnd()) { - this._onPartitionEnd(this._partitionSize); + this._onPartitionEnd(this._offset); return; } this._readChunk(); @@ -447,6 +448,7 @@ class FileChunker { } class FileDigester { + constructor(meta, callback) { this._buffer = []; this._bytesReceived = 0; @@ -473,6 +475,7 @@ class FileDigester { }); this._callback = null; } + } class Events { @@ -485,7 +488,6 @@ class Events { } } - window.isRtcSupported = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection); RTCPeer.config = { diff --git a/client/scripts/ui.js b/client/scripts/ui.js index 42ca141..8ef5251 100644 --- a/client/scripts/ui.js +++ b/client/scripts/ui.js @@ -392,8 +392,8 @@ class Notifications { } _copyText(message, notification) { - document.copy(message); notification.close(); + if(!document.copy(message)) return; this._notify('Copied text to clipboard'); } diff --git a/client/styles.css b/client/styles.css index f6e84b5..121ea7b 100644 --- a/client/styles.css +++ b/client/styles.css @@ -625,6 +625,7 @@ screen and (min-width: 1100px) { x-instructions { top: 24px; } + footer .logo { --icon-size: 40px; } From 891859680a1565cead8fe3dca771449b5e1e3035 Mon Sep 17 00:00:00 2001 From: RobinLinus Date: Sat, 22 Sep 2018 05:55:09 +0200 Subject: [PATCH 25/34] Refactor about page --- client/index.html | 90 +++++++++++++++++++---------------- client/styles.css | 119 +++++++++++++++++++++------------------------- 2 files changed, 102 insertions(+), 107 deletions(-) diff --git a/client/index.html b/client/index.html index 629f84b..ff6eb9e 100644 --- a/client/index.html +++ b/client/index.html @@ -36,6 +36,18 @@ +
+ + + + + + +
@@ -97,47 +109,41 @@
File Transfer Completed
- -
- - - + + +
+
+ + + + + +
+ -
- -

Snapdrop

-
The easiest way to transfer files across devices.
- -
- - - - -
-
- +

Snapdrop

+
The easiest way to transfer files across devices.
+ + + + @@ -172,8 +178,8 @@ - - + +