├── .gitignore ├── README.md ├── index.html ├── index.js ├── package.json └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .*.swp 3 | bundle.js 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperpad 2 | 3 | > A peer-to-peer collaborative text editor for humans and communities. 4 | 5 | ## What is it? 6 | 7 | Hyperpad is a free, open source, peer-to-peer text editor for people and their 8 | communities. Authors control who gets access, and data is hosted by the peers 9 | who are interested in it. 10 | 11 | ## Current Status 12 | 13 | I've paused work on the web client in favour of building out an Electron-based 14 | native client, [hyperpad-desktop](https://github.com/noffle/hyperpad-desktop). 15 | This lets me dodge some difficult problems around web tech (indexed-db 16 | performance; webrtc) and focus on making the core 17 | [hyper-string](https://github.com/noffle/hyper-string) primitive robust and 18 | durable. 19 | 20 | ## Why another collaborative editor? 21 | 22 | Some of the most popular collaborative document editors today include [Google 23 | Docs](https://www.google.com/docs/about/) and [Etherpad](http://etherpad.org/). 24 | 25 | Google Docs gets the fundamental piece right: real-time text editing. However, 26 | all of your data is stored by and readable by Google, Inc. It is closed source 27 | proprietary software. 28 | 29 | Etherpad takes this a step further in multiple directions: it is [open 30 | source](https://github.com/ether/etherpad-lite), and can be deployed by anyone 31 | on any server. This lets any individual or group run etherpad and keep ownership 32 | and privacy to their data. 33 | 34 | Etherpad is most of the way there, but Hyperpad goes the rest of the way in two 35 | crucial aspects: 36 | 37 | ### 1. No servers required 38 | 39 | In peer-to-peer networks, all users are equal. 40 | 41 | Nobody needs the monetary resources and technical know-how to run a server. 42 | 43 | Unlike centralized services, you own each pad you create. Turn on encryption, 44 | and your data becomes unreadable to anyone but those you grant access to. There 45 | are no service providers to go out of business and lose your data. 46 | 47 | Everything is client-side HTML and Javascript: you can just save the Hyperpad 48 | website and run it locally on your computer, and it will function just fine! 49 | 50 | ### 2. Works great offline 51 | 52 | Not everybody in the world is online. Among those who are, many do not have 53 | consistent, broadband connections. People-respecting software must work 54 | excellently offline; no exceptions. 55 | 56 | Forgetting this is the *The Silicon Valley Privilege* (TODO: link to article). 57 | 58 | Hyperpad uses an *eventually consistent* data structure called 59 | [hyperlog](https://github.com/mafintosh/hyperlog), which operates happily 60 | offline and will sync with other users whenever a network connection is 61 | available. 62 | 63 | ## How does it work? 64 | 65 | Hyperpad relies on the browser itself for storing documents, and powerful 66 | peer-to-peer primitives like 67 | [WebRTC](https://developer.mozilla.org/en-US/docs/Web/Guide/API/WebRTC) and 68 | [hyperlog](https://github.com/mafintosh/hyperlog) to organize and transfer 69 | documents to those with access. 70 | 71 | The act of having a document open in your browser immediately lets a user act 72 | as a host for that document's data, sharing it in real-time with others with 73 | others. In the case that a user is offline, they can still freely make edits 74 | locally, which will propagate to others storing the document when they 75 | re-establish a network connection. 76 | 77 | Hyperpad is built in a modular fashion atop a set of do-one-thing-well modules: 78 | 79 | - [hyper-textarea](https://github.com/noffle/hyper-textarea): back a textarea 80 | with a hyper-string for conflict-free p2p replication 81 | - [hyper-string](https://github.com/noffle/hyper-string): a conflict-free p2p 82 | string data structure 83 | - [textarea-op-stream](https://github.com/noffle/textarea-op-stream): readable 84 | stream of a textarea's inserts and deletes 85 | - lots of great modules from [mafintosh](https://github.com/mafintosh/): 86 | [hyperlog](https://github.com/mafintosh/hyperlog), 87 | [signalhub](https://github.com/mafintosh/signalhub), and others! 88 | 89 | ## Coming Soon(tm) 90 | 91 | - [hyperpad-desktop](https://github.com/noffle/hyperpad-desktop) 92 | - ~Faster operations (batching in the `hyper-string` layer)~ 93 | - Encryption (with separate read/write privileges) 94 | - Secure app delivery (maybe [hyperboot](https://github.com/substack/hyperboot)) 95 | 96 | ## License 97 | 98 | [ISC](https://en.wikipedia.org/wiki/ISC_license) 99 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | hyperpad 8 | 9 | 10 | 11 |
12 |

Initializing..

13 | 14 |
Share this URL to let others edit this pad!
15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var swarm = require('webrtc-swarm') 2 | var signalhub = require('signalhub') 3 | var hyperize = require('hyper-textarea') 4 | var hstring = require('hyper-string') 5 | var memdown = require('memdown') 6 | var down = require('level-js') 7 | var levelup = require('levelup') 8 | var query = require('query-string') 9 | var debug = console.log//require('debug')('hyperpad') 10 | var eos = require('end-of-stream') 11 | var hyperlog = require('hyperlog') 12 | 13 | var ta = document.getElementById('pad') 14 | 15 | onNewPad = function () { 16 | window.open(window.location.href.substring(0, window.location.href.indexOf('?'))) 17 | } 18 | 19 | var q = query.parse(location.search) 20 | var doc = 'Untitled_' + (''+Math.random()).substring(2, 25) 21 | if (q.doc) { 22 | doc = q.doc 23 | } else { 24 | window.location.href += '?doc=' + doc 25 | } 26 | 27 | // var string = hstring(levelup('hyperpad-'+doc, { db: down })) 28 | // var string = hstring(levelup('hyperpad-'+doc, { db: memdown })) 29 | var indexDb = levelup('hyperpad-'+doc, { db: down }) 30 | var string = hstring(indexDb) 31 | var storageLog = hyperlog(indexDb, string.log.valueEncoding) 32 | 33 | var r = storageLog.replicate({ live: true }) 34 | var s = string.log.replicate({ live: true }) 35 | r.pipe(s).pipe(r) 36 | 37 | r.on('end', function () { 38 | console.log('replication /w indexeddb log ended!') 39 | }) 40 | r.on('error', function (err) { 41 | console.log('replication /w indexeddb log ended: ' + err) 42 | }) 43 | 44 | hyperize(ta, string) 45 | 46 | document.getElementById('title').innerHTML = doc 47 | 48 | var hub = signalhub('hyperpad-' + doc, [ 49 | 'http://eight45.net:9400', 50 | // 'http://localhost:2500' 51 | // 'https://signalhub.mafintosh.com' 52 | ]) 53 | 54 | var rtcConfig = { 55 | "iceServers": [ 56 | { 57 | "urls": "stun:23.21.150.121" 58 | }, 59 | { 60 | "username": "a03c3482a5ac94f85f98bf6478c7b779ed1c45756798f5246e870741ef0cda04", 61 | "credential": "EIveHo0aA0i8WuCepOoWNGopB4LYxI72013GBv35ZGA=", 62 | "urls": "turn:global.turn.twilio.com:3478?transport=udp" 63 | }, 64 | { 65 | "username": "a03c3482a5ac94f85f98bf6478c7b779ed1c45756798f5246e870741ef0cda04", 66 | "credential": "EIveHo0aA0i8WuCepOoWNGopB4LYxI72013GBv35ZGA=", 67 | "urls": "turn:global.turn.twilio.com:3478?transport=tcp" 68 | }, 69 | { 70 | "username": "a03c3482a5ac94f85f98bf6478c7b779ed1c45756798f5246e870741ef0cda04", 71 | "credential": "EIveHo0aA0i8WuCepOoWNGopB4LYxI72013GBv35ZGA=", 72 | "urls": "turn:global.turn.twilio.com:443?transport=tcp" 73 | } 74 | ] 75 | } 76 | 77 | var sw = swarm(hub, { 78 | config: rtcConfig 79 | }) 80 | sw.on('peer', function (peer, id) { 81 | debug('replicating to a new peer:', id) 82 | 83 | var r = string.createReplicationStream({ live: true }) 84 | eos(r, end) 85 | eos(peer, end) 86 | r.pipe(peer).pipe(r) 87 | 88 | function end (err) { 89 | debug('replication stream ended:', err) 90 | } 91 | }) 92 | 93 | sw.on('disconnect', function (peer, id) { 94 | debug('disconnected from a peer:', id) 95 | }) 96 | 97 | sw.on('close', function () { 98 | debug('signalhub connection lost') 99 | }) 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperpad", 3 | "description": "p2p collaborative text editor", 4 | "version": "0.0.10", 5 | "author": "Stephen Whitmore ", 6 | "repository": { 7 | "url": "git://github.com/noffle/hyperpad.git" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "start": "browserify index.js > bundle.js && ecstatic -p 7000" 12 | }, 13 | "dependencies": { 14 | "debug": "^2.2.0", 15 | "end-of-stream": "^1.1.0", 16 | "hyper-string": "^2.0.1", 17 | "hyper-textarea": "^1.0.0", 18 | "hyperlog": "^4.12.1", 19 | "level-js": "^2.2.4", 20 | "levelup": "^1.3.2", 21 | "memdown": "^1.2.4", 22 | "query-string": "^4.2.2", 23 | "signalhub": "^4.7.4", 24 | "webrtc-swarm": "^2.6.1" 25 | }, 26 | "devDependencies": { 27 | "browserify": "^13.1.1", 28 | "ecstatic": "^3.1.1" 29 | }, 30 | "license": "ISC" 31 | } 32 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html { 2 | background: #fafafa; 3 | font-family: -apple-system, BlinkMacSystemFont, /* MacOS and iOS */ 4 | 'avenir next', avenir, /* MacOS and iOS */ 5 | 'Segoe UI', /* Windows */ 6 | 'lucida grande', /* Older MacOS */ 7 | 'helvetica neue', helvetica, /* Older MacOS */ 8 | 'Fira Sans', /* Firefox OS */ 9 | roboto, noto, /* Google stuff */ 10 | 'Droid Sans', /* Old Google stuff */ 11 | cantarell, oxygen, ubuntu, /* Linux stuff */ 12 | 'franklin gothic medium', 'century gothic', /* Windows stuff */ 13 | 'Liberation Sans', /* Linux */ 14 | sans-serif; /* Everything else */ 15 | font-size: 16px; 16 | } 17 | html, body { 18 | margin: 0; 19 | padding: 0; 20 | } 21 | #wrap { 22 | box-sizing: border-box; 23 | display: flex; 24 | flex-direction: column; 25 | height: 100vh; 26 | padding: 5px 10px; 27 | position: relative; 28 | } 29 | #hint { 30 | font-size: 0.85em; 31 | margin-bottom: 5px; 32 | font-style: italic; 33 | text-align: right; 34 | } 35 | .logo { 36 | color: #aaa; 37 | margin-top: 5px; 38 | text-align: right; 39 | } 40 | .logo2 { 41 | font-weight: 900; 42 | } 43 | #title { 44 | font-weight: 300; 45 | margin: 0; 46 | } 47 | #new-pad { 48 | background: #fff; 49 | border: 1px solid #aaa; 50 | border-radius: 4px; 51 | color: #444; 52 | cursor: pointer; 53 | font-size: 13px; 54 | padding: .25em .75em .3em; 55 | position: absolute; 56 | right: 10px; 57 | top: 10px; 58 | } 59 | #new-pad:hover, 60 | #new-pad:active, 61 | #new-pad:focus { 62 | background: #eee; 63 | border-color: #777; 64 | color: #222; 65 | } 66 | #pad { 67 | background: #fff; 68 | border: 1px solid #ccc; 69 | color: #333; 70 | font-size: 16px; 71 | flex-grow: 1; 72 | outline: none; 73 | padding: 1em; 74 | resize: none; 75 | } 76 | #pad:focus { 77 | border-color: #aaa; 78 | color: #111; 79 | } 80 | --------------------------------------------------------------------------------