├── public ├── favicon.ico ├── images │ ├── footer.png │ ├── globe.png │ ├── hero-wave.png │ ├── hero-tooltip.png │ ├── how-it-works.png │ ├── line-top-right.svg │ ├── line-top-left.svg │ ├── line-bottom-left.svg │ ├── line-bottom-right.svg │ ├── encrypted.svg │ ├── realtime.svg │ ├── private.svg │ ├── take-notes.svg │ ├── collaborative.svg │ ├── write-articles.svg │ ├── work-with-multiple-users.svg │ ├── collaborate.svg │ ├── logo-peerpad.svg │ ├── hex.svg │ └── logo-peerpad-lg.svg ├── manifest.json └── index.html ├── test ├── e2e-load │ ├── config.js │ ├── inject-config.js │ ├── build.js │ ├── spawn-server.js │ ├── spawn-relay.js │ ├── spawn-pinner.js │ ├── replica-add-text-behavior.js │ ├── replica.js │ ├── read-only-replica.js │ ├── spawn-cluster.js │ ├── index.js │ ├── replica-change-text-behavior.js │ ├── bootstrap.js │ └── text │ │ ├── insert-only-text.js │ │ └── mutable-text.js └── e2e │ └── smoke.test.js ├── config ├── enzyme.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── env.js └── webpackDevServer.config.js ├── src ├── lib │ ├── peer-color.js │ ├── merge-aliases.js │ ├── debug-dumper.js │ ├── parse-symm-key.js │ ├── markdown.js │ ├── take-snapshot.js │ ├── markdown-sanitize.json │ └── bind-editor.js ├── components │ ├── header │ │ ├── buttons │ │ │ ├── index.js │ │ │ ├── NewButton.js │ │ │ ├── NotificationsButton.js │ │ │ └── PeersButton.js │ │ ├── Header.js │ │ └── ViewMode.js │ ├── toolbar │ │ ├── buttons │ │ │ ├── index.js │ │ │ ├── ToggleButton.js │ │ │ ├── Button.js │ │ │ ├── SnapshotsButton.js │ │ │ └── LinkButton.js │ │ └── Toolbar.js │ ├── App.test.js │ ├── dropdown │ │ ├── Overlay.js │ │ ├── Menu.js │ │ ├── Dropleft.js │ │ └── Dropdown.js │ ├── icons │ │ ├── plus.js │ │ ├── text.js │ │ ├── close.js │ │ ├── user.js │ │ ├── directory.js │ │ ├── code.js │ │ ├── index.js │ │ ├── shortcuts.js │ │ ├── link.js │ │ ├── snapshot.js │ │ ├── bell.js │ │ ├── debug.js │ │ ├── settings.js │ │ └── alpha.js │ ├── SnapshotLink.js │ ├── Status.js │ ├── Preview.module.styl │ ├── App.js │ ├── Preview.js │ ├── Codemirror.module.styl │ ├── DocViewerHTML.js │ ├── home │ │ └── Home.js │ ├── Editor.js │ ├── Warning.js │ ├── DocViewer.js │ ├── logo.svg │ ├── EditorArea.js │ └── Doc.js ├── viewer.js ├── colors.styl ├── index.js ├── snapshoter.js ├── rollbar.js ├── config.js ├── index.styl └── registerServiceWorker.js ├── Makefile ├── .gitignore ├── docs ├── TECHNOLOGY.md ├── CONTRIBUTING.md ├── DEPLOY.md └── SECURITY.md ├── bin └── peerpad-print-dump ├── LICENSE ├── .circleci └── config.yml ├── scripts ├── test.js ├── start.js └── build.js ├── README.md └── package.json /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peer-base/peer-pad/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peer-base/peer-pad/HEAD/public/images/footer.png -------------------------------------------------------------------------------- /public/images/globe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peer-base/peer-pad/HEAD/public/images/globe.png -------------------------------------------------------------------------------- /public/images/hero-wave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peer-base/peer-pad/HEAD/public/images/hero-wave.png -------------------------------------------------------------------------------- /public/images/hero-tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peer-base/peer-pad/HEAD/public/images/hero-tooltip.png -------------------------------------------------------------------------------- /public/images/how-it-works.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peer-base/peer-pad/HEAD/public/images/how-it-works.png -------------------------------------------------------------------------------- /test/e2e-load/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | ipfs: { 5 | swarm: ['/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star'] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /config/enzyme.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { configure } from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | configure({ adapter: new Adapter() }); -------------------------------------------------------------------------------- /src/lib/peer-color.js: -------------------------------------------------------------------------------- 1 | import ColorHash from 'color-hash' 2 | const colorHash = new ColorHash() 3 | 4 | export default (peerId) => { 5 | return colorHash.hex(peerId.substring(Math.round(peerId.length / 2))) 6 | } 7 | -------------------------------------------------------------------------------- /src/components/header/buttons/index.js: -------------------------------------------------------------------------------- 1 | export { default as NewButton } from './NewButton' 2 | export { default as NotificationsButton } from './NotificationsButton' 3 | export { default as PeersButton } from './PeersButton' 4 | -------------------------------------------------------------------------------- /src/components/toolbar/buttons/index.js: -------------------------------------------------------------------------------- 1 | export { default as Button } from './Button' 2 | export { default as LinkButton } from './LinkButton' 3 | export { default as SnapshotsButton } from './SnapshotsButton' 4 | export { default as ToggleButton } from './ToggleButton' 5 | -------------------------------------------------------------------------------- /src/viewer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import DocViewerHTML from './components/DocViewerHTML' 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('react-app')) 8 | -------------------------------------------------------------------------------- /src/components/App.test.js: -------------------------------------------------------------------------------- 1 | /* global it */ 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import App from './App' 5 | 6 | it('renders without crashing', () => { 7 | const div = document.createElement('div') 8 | ReactDOM.render(, div) 9 | }) 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | npm --version 5 | # Pin the npm version to 6.0.0 6 | # Using npx is a workaround for npm<5.6 not being able to self update 7 | # See: https://github.com/ipfs/ci-websites/issues/3 8 | npx npm@5.6 i -g npm@6.4.1 9 | npm --version 10 | npm ci 11 | npm run build 12 | -------------------------------------------------------------------------------- /test/e2e-load/inject-config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const config = require('./config') 4 | 5 | module.exports = (page) => { 6 | const injectable = ` 7 | Object.defineProperty(window, '__peerStarConfig', { 8 | get() { 9 | return ${JSON.stringify(config)} 10 | } 11 | }) 12 | ` 13 | return page.evaluateOnNewDocument(injectable) 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env* 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | yarn.lock 23 | 24 | *.swp 25 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/colors.styl: -------------------------------------------------------------------------------- 1 | /* Thanks http://chir.ag/projects/name-that-color */ 2 | 3 | $blue-bayox = #50617B 4 | $bright-turquoise = #23e0f7 5 | $dull-turquoise = #1fccdf 6 | $caribbean-green = #02caad 7 | $cloud-burst = #233855 8 | $dove-gray = #656464 9 | $firefly = #0e1d32 10 | $big-stone = #1a2c45 11 | $black-pearl = #081527 12 | $pigeon-post = #b5c5df 13 | $razzmatazz = #FB0D5B 14 | $white-lilac = #f3f6fb 15 | -------------------------------------------------------------------------------- /src/components/dropdown/Overlay.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // Invisible click grabber, to detect when the user clicks away. 4 | const Overlay = ({onClick}) => { 5 | return ( 6 |
13 | ) 14 | } 15 | 16 | export default Overlay 17 | -------------------------------------------------------------------------------- /src/components/icons/plus.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /src/components/header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | export default ({ children }) => ( 5 |
6 |
7 | 8 | PeerPad logo 9 | 10 | {children} 11 |
12 |
13 | ) 14 | -------------------------------------------------------------------------------- /src/lib/merge-aliases.js: -------------------------------------------------------------------------------- 1 | module.exports = (aliases) => { 2 | return Array.from(aliases).sort(sortSets).reduce((all, aliases) => { 3 | return Object.assign(all, aliases) 4 | }, {}) 5 | } 6 | 7 | function sortSets (a, b) { 8 | const ar = JSON.stringify(Array.from(a)) 9 | const br = JSON.stringify(Array.from(b)) 10 | if (ar < br) { 11 | return -1 12 | } else if (ar === br) { 13 | return 0 14 | } 15 | return 1 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/debug-dumper.js: -------------------------------------------------------------------------------- 1 | import { encode } from 'delta-crdts-msgpack-codec' 2 | 3 | export default function bindDebugDumper (doc) { 4 | if (!window.peerPadDevTools) window.peerPadDevTools = {} 5 | const tools = window.peerPadDevTools 6 | tools.doc = doc 7 | tools.dumpState = function dumpState () { 8 | console.log('State:', doc.shared.state()) 9 | console.log('State (Base64):', encode(doc.shared.state()).toString('base64')) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e-load/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spawn = require('child_process').spawn 4 | 5 | module.exports = () => { 6 | return new Promise((resolve, reject) => { 7 | const build = spawn('npm', ['run', 'build'], { stdio: 'inherit' }) 8 | build.once('close', (code) => { 9 | if (code !== 0) { 10 | reject(new Error(`build ended with code ${code}`)) 11 | } else { 12 | resolve() 13 | } 14 | }) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /docs/TECHNOLOGY.md: -------------------------------------------------------------------------------- 1 | # PeerPad Technology Stack 2 | 3 | ## Technologies 4 | 5 | - React 6 | - Webpack 7 | - Jest for testing 8 | - {editing libraries} - Still to be decided upon research is concluded 9 | 10 | ## Principles 11 | 12 | - Every call must have tests 13 | - Every code change should be done through PR + CR, reviewed by at least one other person 14 | - Code Coverage should always go up or maintain per PR 15 | - Tests need to run in simulated envinronment 16 | -------------------------------------------------------------------------------- /src/components/dropdown/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // Styling for the dropdown box and shadow, and reset positon to relative. 4 | const Menu = ({children}) => ( 5 |
13 | {children} 14 |
15 | ) 16 | 17 | export default Menu 18 | -------------------------------------------------------------------------------- /test/e2e-load/spawn-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spawn = require('child_process').spawn 4 | 5 | module.exports = () => { 6 | return new Promise((resolve, reject) => { 7 | const server = spawn('npm', ['run', 'serve:build'], { 8 | stdio: ['inherit', 'pipe', 'inherit'] 9 | }) 10 | server.stdout.on('data', (d) => { 11 | if (d.toString().indexOf('Hit CTRL-C to stop the server') >= 0) { 12 | resolve(server) 13 | } 14 | }) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/SnapshotLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const GATEWAY_PREFIX = 'https://ipfs.io/ipfs' 4 | 5 | export const toSnapshotUrl = ({hash, key, gateway = GATEWAY_PREFIX}) => `${gateway}/${hash}/#${key}` 6 | 7 | const SnapshotLink = ({snapshot, className, style, target = '_blank', children}) => ( 8 | 9 | {children || snapshot.hash} 10 | 11 | ) 12 | 13 | export default SnapshotLink 14 | -------------------------------------------------------------------------------- /src/components/icons/text.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /test/e2e-load/spawn-relay.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spawn = require('child_process').spawn 4 | 5 | module.exports = () => { 6 | return new Promise((resolve, reject) => { 7 | let started = false 8 | const relay = spawn('npm', ['run', 'start:rendezvous'], { 9 | stdio: ['inherit', 'pipe', 'inherit'] 10 | }) 11 | relay.stdout.on('data', (d) => { 12 | if (!started && d.toString().indexOf('Listening on') >= 0) { 13 | started = true 14 | resolve(relay) 15 | } 16 | process.stdout.write(d) 17 | }) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /public/images/line-top-right.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /public/images/line-top-left.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /public/images/line-bottom-left.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /src/components/header/buttons/NewButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { PlusIcon } from '../../icons' 4 | 5 | const NewButton = ({ onClick }) => { 6 | if (!onClick) return null 7 | return ( 8 | 11 | ) 12 | } 13 | 14 | NewButton.propTypes = { 15 | onClick: PropTypes.func 16 | } 17 | 18 | export default NewButton 19 | -------------------------------------------------------------------------------- /test/e2e-load/spawn-pinner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const spawn = require('child_process').spawn 4 | 5 | module.exports = () => { 6 | return new Promise((resolve, reject) => { 7 | let started = false 8 | const pinner = spawn('npm', ['run', 'start:test:pinner'], { 9 | stdio: ['inherit', 'pipe', 'inherit'] 10 | }) 11 | pinner.stdout.on('data', (d) => { 12 | if (!started && d.toString().indexOf('Swarm listening on') >= 0) { 13 | started = true 14 | resolve(pinner) 15 | } 16 | process.stdout.write('pinner: ' + d) 17 | }) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /public/images/line-bottom-right.svg: -------------------------------------------------------------------------------- 1 | Artboard 1 -------------------------------------------------------------------------------- /src/components/icons/close.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /src/components/icons/user.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /src/components/Status.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | class Status extends Component { 5 | render () { 6 | return ( 7 |
8 | Status: 9 | 13 | {this.props.status} 14 | 15 |
16 | ) 17 | } 18 | } 19 | 20 | Status.propTypes = { 21 | status: PropTypes.string.isRequired 22 | } 23 | 24 | export default Status 25 | -------------------------------------------------------------------------------- /src/lib/parse-symm-key.js: -------------------------------------------------------------------------------- 1 | const b58Decode = require('bs58').decode 2 | const crypto = require('libp2p-crypto') 3 | const pify = require('pify') 4 | 5 | const createKey = pify(crypto.aes.create.bind(crypto.aes)) 6 | 7 | const defaultOptions = { 8 | keyLength: 32, 9 | ivLength: 16 10 | } 11 | 12 | function parseSymmetricalKey (string, _options) { 13 | const options = Object.assign({}, defaultOptions, _options) 14 | const rawKey = b58Decode(string) 15 | 16 | return createKey( 17 | rawKey.slice(0, options.keyLength), 18 | rawKey.slice(options.keyLength, options.keyLength + options.ivLength)) 19 | } 20 | 21 | module.exports = parseSymmetricalKey 22 | -------------------------------------------------------------------------------- /src/components/icons/directory.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | ) 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import initRollbar from './rollbar' 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | // TODO: register service worker: 5 | // import registerServiceWorker from './registerServiceWorker' 6 | import 'tachyons/css/tachyons.css' 7 | import 'tachyons-flexbox/css/tachyons-flexbox.css' 8 | import 'tachyons-forms/css/tachyons-forms.css' 9 | import './index.styl' 10 | 11 | import App from './components/App' 12 | 13 | // Initialize Rollbar first so we can catch all errors 14 | const rollbar = initRollbar(window.location.hostname) 15 | window.Rollbar = rollbar 16 | 17 | ReactDOM.render(, document.getElementById('root')) 18 | // registerServiceWorker() 19 | -------------------------------------------------------------------------------- /public/images/encrypted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/code.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/components/icons/index.js: -------------------------------------------------------------------------------- 1 | export { default as BellIcon } from './bell' 2 | export { default as CodeIcon } from './code' 3 | export { default as DirectoryIcon } from './directory' 4 | export { default as LinkIcon } from './link' 5 | export { default as PlusIcon } from './plus' 6 | export { default as SettingsIcon } from './settings' 7 | export { default as ShortcutsIcon } from './shortcuts' 8 | export { default as SnapshotIcon } from './snapshot' 9 | export { default as TextIcon } from './text' 10 | export { default as UserIcon } from './user' 11 | export { default as CloseIcon } from './close' 12 | export { default as AlphaIcon } from './alpha' 13 | export { default as DebugIcon } from './debug' 14 | -------------------------------------------------------------------------------- /src/components/toolbar/buttons/ToggleButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function btnClass (disabled, theme) { 4 | const stem = 'button-reset db bg-transparent bw0 pigeon-post pointer' 5 | if (disabled) return stem 6 | if (theme === 'light') { 7 | return stem + ' blue-bayox' 8 | } 9 | return stem + ' white-lilac' 10 | } 11 | 12 | const ToggleButton = ({ theme, icon: Icon, title, onClick, disabled }) => ( 13 | 20 | ) 21 | 22 | export default ToggleButton 23 | -------------------------------------------------------------------------------- /src/snapshoter.js: -------------------------------------------------------------------------------- 1 | import waterfall from 'async/waterfall' 2 | 3 | export default (ipfs, cipher) => { 4 | return async function saveToIPFS (doc) { 5 | const clear = Buffer.from(doc) 6 | 7 | return new Promise((resolve, reject) => { 8 | waterfall([ 9 | (callback) => cipher(callback), 10 | (cipher, callback) => cipher.encrypt(clear, callback), 11 | (ciphered, callback) => ipfs.files.add(ciphered, callback), 12 | (resArray, callback) => callback(null, resArray[resArray.length - 1]) 13 | ], (err, result) => { 14 | if (err) { 15 | reject(err) 16 | } else { 17 | resolve(result) 18 | } 19 | }) 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/icons/shortcuts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | ) 8 | -------------------------------------------------------------------------------- /bin/peerpad-print-dump: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const CRDTs = require('delta-crdts') 4 | const { decode } = require('delta-crdts-msgpack-codec') 5 | const concat = require('concat-stream') 6 | 7 | process.stdin.pipe(concat(buffer => { 8 | // console.log('Buffer', buffer) 9 | const encoded = Buffer.from(buffer.toString(), 'base64') 10 | // console.log('Encoded', encoded) 11 | const decoded = decode(encoded) 12 | // console.log('Decoded', decoded) 13 | const RGA = CRDTs('rga') 14 | const collaboration = RGA('imported') 15 | collaboration.apply(decoded) 16 | console.log('Value:\n') 17 | console.log(collaboration.value().join('')) 18 | })) 19 | process.stdin.on('error', err => { 20 | console.error(err) 21 | process.exit(1) 22 | }) 23 | -------------------------------------------------------------------------------- /public/images/realtime.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/toolbar/buttons/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function btnClass (disabled, theme) { 4 | const stem = 'button-reset db bg-transparent bw0 pigeon-post' 5 | if (disabled) return stem 6 | if (theme === 'light') { 7 | return stem + ' pointer hover--blue-bayox' 8 | } 9 | return stem + ' pointer hover--white-lilac' 10 | } 11 | 12 | const Button = ({ theme, icon: Icon, title, onClick, disabled = false }) => ( 13 | 21 | ) 22 | 23 | export default Button 24 | -------------------------------------------------------------------------------- /src/components/Preview.module.styl: -------------------------------------------------------------------------------- 1 | @import '../colors.styl' 2 | 3 | .Preview 4 | padding-left 35px 5 | 6 | h1::before, 7 | h2::before, 8 | h3::before, 9 | h4::before, 10 | h5::before, 11 | h6::before 12 | font-size 14px 13 | font-weight lighter 14 | color $pigeon-post 15 | display inline-block 16 | width 0 17 | margin-left -35px 18 | margin-right 35px 19 | vertical-align baseline 20 | 21 | h1::before 22 | content 'H1' 23 | 24 | h2::before 25 | content 'H2' 26 | 27 | h3::before 28 | content 'H3' 29 | 30 | h4::before 31 | content 'H4' 32 | 33 | h5::before 34 | content 'H5' 35 | 36 | h6::before 37 | content 'H6' 38 | 39 | & > :first-child 40 | margin-top 0 !important 41 | 42 | -------------------------------------------------------------------------------- /src/components/icons/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | import { HashRouter as Router, Route } from 'react-router-dom' 4 | 5 | import Home from './home/Home' 6 | import Edit from './Edit' 7 | 8 | class App extends Component { 9 | render () { 10 | return ( 11 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | ) 19 | } 20 | 21 | renderEditor (props) { 22 | return () 23 | } 24 | 25 | onBackend (backend) { 26 | this._backend = backend 27 | } 28 | } 29 | 30 | export default App 31 | -------------------------------------------------------------------------------- /src/components/icons/snapshot.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ) 13 | -------------------------------------------------------------------------------- /test/e2e-load/replica-add-text-behavior.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = async ({page, worker, text, beforeWaitMS = 10000, sessionDurationMS = 30000, typeIntervalMS = 50, coolDownMS = 120000}) => { 4 | page.waitFor(beforeWaitMS) 5 | 6 | const editorSelector = '[class=CodeMirror-code][contenteditable=true]' 7 | const startedAt = Date.now() 8 | const endAt = startedAt + sessionDurationMS 9 | 10 | while (Date.now() < endAt) { 11 | if (Math.random() < 0.1) { 12 | await page.waitFor(4000) 13 | } else { 14 | await page.waitFor(typeIntervalMS) 15 | } 16 | 17 | await page.type(editorSelector, text()) 18 | } 19 | 20 | text.finished() 21 | await text.allDone() 22 | await page.waitFor(coolDownMS) 23 | const finalText = await page.evaluate(() => { 24 | return window.__peerPadEditor.getValue() 25 | }) 26 | text.submitResult(finalText) 27 | } 28 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)); 11 | 12 | if (filename.match(/\.svg$/)) { 13 | return `module.exports = { 14 | __esModule: true, 15 | default: ${assetFilename}, 16 | ReactComponent: (props) => ({ 17 | $$typeof: Symbol.for('react.element'), 18 | type: 'svg', 19 | ref: null, 20 | key: null, 21 | props: Object.assign({}, props, { 22 | children: ${assetFilename} 23 | }) 24 | }), 25 | };`; 26 | } 27 | 28 | return `module.exports = ${assetFilename};`; 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /public/images/private.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/icons/bell.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | -------------------------------------------------------------------------------- /src/lib/markdown.js: -------------------------------------------------------------------------------- 1 | const pify = require('pify') 2 | const Remark = require('remark') 3 | const RemarkHtml = require('remark-html') 4 | const RemarkMath = require('remark-math') 5 | const RemarkHtmlKatex = require('remark-html-katex') 6 | 7 | const converters = { 8 | markdown: Remark().use(RemarkHtml, { sanitize: true }), 9 | math: Remark() 10 | .use(RemarkMath) 11 | .use(RemarkHtmlKatex) 12 | .use(RemarkHtml, { sanitize: require('./markdown-sanitize.json') }) 13 | } 14 | Object.keys(converters).forEach((key) => { 15 | const converter = converters[key] 16 | converters[key] = pify(converter.process.bind(converter)) 17 | }) 18 | 19 | const convert = async (md, type) => { 20 | const converter = converters[type] 21 | if (!converter) { 22 | throw new Error('no converter for type ' + type) 23 | } 24 | return converter(md).then((result) => result.contents) 25 | } 26 | 27 | export { convert } 28 | -------------------------------------------------------------------------------- /src/components/header/buttons/NotificationsButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { BellIcon } from '../../icons' 4 | 5 | const NotificationsButton = ({ onClick, count }) => { 6 | if (!onClick) return null 7 | return ( 8 | 14 | ) 15 | } 16 | 17 | NotificationsButton.propTypes = { 18 | onClick: PropTypes.func, 19 | count: PropTypes.number 20 | } 21 | 22 | export default NotificationsButton 23 | -------------------------------------------------------------------------------- /test/e2e-load/replica.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ms = require('milliseconds') 4 | const injectConfig = require('./inject-config') 5 | const replicaAddTextBehavior = require('./replica-add-text-behavior') 6 | const replicaChangeTextBehavior = require('./replica-change-text-behavior') 7 | 8 | module.exports = ({events, text}) => async ({ page, data: url, worker }) => { 9 | try { 10 | await injectConfig(page) 11 | page.setDefaultNavigationTimeout(120000) 12 | await page.goto(url) 13 | page.on('console', (m) => events.emit('message', `[worker ${worker.id}]: ${m.text()}`)) 14 | await page.waitForSelector('[data-id=ipfs-status][data-value=online]', {timeout: ms.minutes(2)}) 15 | await replicaAddTextBehavior({page, worker, text}) 16 | await replicaChangeTextBehavior({page, worker, text: text.mutable()}) 17 | } catch (err) { 18 | console.error(`error in worker ${worker.id}:`, err) 19 | throw err 20 | } 21 | events.emit('worker ended', worker.id) 22 | } 23 | -------------------------------------------------------------------------------- /public/images/take-notes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Preview.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Doc from './Doc' 4 | import styles from './Preview.module.styl' 5 | import 'katex/dist/katex.css' 6 | 7 | export default class Preview extends Component { 8 | constructor (props) { 9 | super(props) 10 | this.state = { html: '' } 11 | } 12 | 13 | componentWillMount () { 14 | const { md } = this.props 15 | if (!md) return 16 | this.convert(md) 17 | } 18 | 19 | componentWillReceiveProps (nextProps) { 20 | this.convert(nextProps.md) 21 | } 22 | 23 | // TODO: debounce? 24 | async convert (md) { 25 | if (!this.props.convertMarkdown) { 26 | return 27 | } 28 | const html = await this.props.convertMarkdown(md, this.props.type) 29 | this.setState({ html }) 30 | } 31 | 32 | render () { 33 | return 34 | } 35 | } 36 | 37 | Preview.propTypes = { 38 | md: PropTypes.string, 39 | convertMarkdown: PropTypes.func 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Codemirror.module.styl: -------------------------------------------------------------------------------- 1 | @import '../colors.styl' 2 | 3 | .CodeMirrorContainer 4 | :global 5 | .CodeMirror 6 | color $white-lilac 7 | background-color $big-stone 8 | height auto 9 | min-height calc(100vh - 220px) 10 | pre 11 | padding-left 30px 12 | 13 | .CodeMirror-scroll 14 | min-height calc(100vh - 220px) 15 | 16 | .CodeMirror-gutters 17 | background-color $cloud-burst 18 | border-right 0 19 | 20 | .CodeMirror-lines 21 | padding 30px 0 22 | 23 | .CodeMirror-linenumber 24 | color $blue-bayox 25 | padding 0 13px 0 7px 26 | font-size 10px 27 | 28 | .CodeMirror-cursor 29 | border-color $white-lilac 30 | 31 | .cm-s-default 32 | .cm-header 33 | font-weight normal 34 | color $bright-turquoise 35 | .cm-comment 36 | color $pigeon-post 37 | 38 | .cm-s-default .cm-variable-2, 39 | .cm-s-default .cm-quote 40 | color $white-lilac 41 | 42 | .cm-s-default .cm-link, 43 | .cm-s-default .cm-url 44 | color $bright-turquoise -------------------------------------------------------------------------------- /public/images/collaborative.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/images/write-articles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/rollbar.js: -------------------------------------------------------------------------------- 1 | import rollbar from 'rollbar' 2 | 3 | const rollbarTransformer = (payload) => { 4 | // This removes the last part of a URL, in case we're inside a pad, we don't 5 | // want to share the encryption key with Rollbar 6 | payload.request.url = payload.request.url.split('/').slice(0, -1).join('/') 7 | } 8 | 9 | export default (hostname) => { 10 | const rollbarConfig = { 11 | // Only enable error reporting if user is loading the website via peerpad.net 12 | enabled: hostname === 'peerpad.net' || hostname === 'dev.peerpad.net', 13 | captureIp: false, 14 | accessToken: '2eaed8c2c5e243af8497d15ea90b407e', 15 | captureUncaught: true, 16 | captureUnhandledRejections: true, 17 | transform: rollbarTransformer, 18 | payload: { 19 | environment: hostname, 20 | client: { 21 | javascript: { 22 | source_map_enabled: true, 23 | code_version: process.env.GIT_COMMIT, 24 | guess_uncaught_frames: true 25 | } 26 | } 27 | } 28 | } 29 | 30 | const Rollbar = rollbar.init(rollbarConfig) 31 | return Rollbar 32 | } 33 | -------------------------------------------------------------------------------- /test/e2e-load/read-only-replica.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ms = require('milliseconds') 4 | const injectConfig = require('./inject-config') 5 | const replicaAddTextBehavior = require('./replica-add-text-behavior') 6 | const replicaChangeTextBehavior = require('./replica-change-text-behavior') 7 | 8 | module.exports = ({events, coolDownMS = 120000}) => async ({ page, data: url, worker }) => { 9 | try { 10 | console.log('starting new read only replica') 11 | await injectConfig(page) 12 | page.setDefaultNavigationTimeout(120000) 13 | await page.goto(url) 14 | page.on('console', (m) => events.emit('message', `[worker ${worker.id}]: ${m.text()}`)) 15 | await page.waitForSelector('[data-id=ipfs-status][data-value=online]', {timeout: ms.minutes(2)}) 16 | await page.waitFor(coolDownMS) 17 | const result = await page.evaluate(() => { 18 | console.log('reading new replica editor value...') 19 | return window.__peerPadEditor.getValue() 20 | }) 21 | events.emit('worker ended', result) 22 | } catch (err) { 23 | console.error(`error in worker ${worker.id}:`, err) 24 | throw err 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Protocol Labs Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PeerPad 2 | 3 | ## Dev Workflow 4 | 5 | - After making changes run `npm test` 6 | - If there are snapshot failures you will see a diff of html. If the changes listed are as they are expected, run `npm test -- -u`, which will generate a new version of the jest snapshots 7 | - Once all tests are passing commit your changes, including the snapshot files (`.snap`) 8 | - Create your PR and request a review from one of the repo maintainers. If you are not sure who the maintainers are, you can skip adding a reviewer and the Lead Maintainer will add one or review it themselves 9 | - If the PR is open long enough to get conflicts, rebase with master and fix the conflicts 10 | - Make sure to re-run `npm test` and, if needed, `npm test — -u` 11 | - Once approved, a maintainer will merge your branch to master 12 | 13 | ### Jest Snapshots 14 | 15 | When html is changed, running `npm test` will fail as jest catches the changes. The diff in snapshots listed in the output after running the tests should **always** be reviewed prior to updating the snapshots with `npm test -- -u`, to avoid commiting an unwanted change to the test suite. 16 | -------------------------------------------------------------------------------- /public/images/work-with-multiple-users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/icons/debug.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /src/components/DocViewerHTML.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import parseSymmetricalKey from '../lib/parse-symm-key' 4 | import Doc from './Doc' 5 | 6 | class DocViewerHTML extends Component { 7 | constructor (props) { 8 | super(props) 9 | this.state = { 10 | doc: 'Loading and decrypting doc...', 11 | error: null 12 | } 13 | } 14 | 15 | render () { 16 | const { 17 | error, 18 | doc 19 | } = this.state 20 | 21 | return ( 22 |
23 | {error ? ( 24 |

Error: {this.state.error}

25 | ) : ( 26 | 27 | )} 28 |
29 | ) 30 | } 31 | 32 | async componentDidMount () { 33 | try { 34 | const key = await parseSymmetricalKey(window.location.hash.substr(1)) 35 | key.decrypt(this.props.encryptedDoc, (err, decrypted) => { 36 | if (err) { 37 | this.setState({error: err.message}) 38 | } else { 39 | this.setState({doc: decrypted.toString('utf8')}) 40 | } 41 | }) 42 | } catch (err) { 43 | this.setState({error: err.message}) 44 | } 45 | } 46 | } 47 | 48 | DocViewerHTML.propTypes = { 49 | encryptedDoc: PropTypes.object.isRequired 50 | } 51 | 52 | export default DocViewerHTML 53 | -------------------------------------------------------------------------------- /src/components/home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Redirect } from 'react-router-dom' 3 | 4 | class Home extends Component { 5 | constructor (props) { 6 | super(props) 7 | this.state = { 8 | redirect: null 9 | } 10 | } 11 | async componentDidMount () { 12 | try { 13 | const generateRandomKeys = (await import('peer-base/src/keys/generate')).default 14 | const generateRandomName = (await import('peer-base/src/keys/generate-random-name')).default 15 | const uriEncodeKey = (await import('peer-base/src/keys/uri-encode')).default 16 | const type = encodeURIComponent('markdown') 17 | const name = encodeURIComponent(generateRandomName()) 18 | const keys = await generateRandomKeys() 19 | const url = '/w/' + type + '/' + name + '/' + uriEncodeKey(keys) 20 | this.setState({redirect: url}) 21 | } catch (err) { 22 | window.alert( 23 | 'An error occurred while trying to create pad for you.\n' + 24 | 'This may be because you may be using a non-compatible browser.\n' + 25 | 'If this is the case, please try, if you can, with latest Firefox or Chrome.') 26 | throw err 27 | } 28 | } 29 | render () { 30 | if (this.state.redirect) { 31 | return 32 | } 33 | return null 34 | } 35 | } 36 | 37 | export default Home 38 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:10.15.1 6 | steps: 7 | - checkout 8 | - run: 9 | command: npm ci 10 | - run: 11 | command: npm run build 12 | - persist_to_workspace: 13 | root: . 14 | paths: 15 | - build 16 | 17 | deploy: 18 | docker: 19 | - image: olizilla/ipfs-dns-deploy 20 | environment: 21 | DOMAIN: peerpad.net 22 | DEV_DOMAIN: dev.peerpad.net 23 | BUILD_DIR: build 24 | steps: 25 | - attach_workspace: 26 | at: /tmp/workspace 27 | - run: 28 | name: Deploy website to IPFS 29 | command: | 30 | pin_name="$DOMAIN build $CIRCLE_BUILD_NUMBER" 31 | 32 | hash=$(pin-to-cluster.sh "$pin_name" /tmp/workspace/$BUILD_DIR) 33 | 34 | echo "Website added to IPFS: https://ipfs.io/ipfs/$hash" 35 | 36 | if [ "$CIRCLE_BRANCH" == "production" ] ; then 37 | dnslink-dnsimple -d $DOMAIN -r _dnslink -l /ipfs/$hash 38 | 39 | elif [ "$CIRCLE_BRANCH" == "master" ] ; then 40 | dnslink-dnsimple -d $DOMAIN -r _dnslink.dev -l /ipfs/$hash 41 | 42 | fi 43 | 44 | workflows: 45 | version: 2 46 | build-deploy: 47 | jobs: 48 | - build 49 | - deploy: 50 | context: ipfs-dns-deploy 51 | requires: 52 | - build 53 | -------------------------------------------------------------------------------- /test/e2e-load/spawn-cluster.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events') 4 | const { Cluster } = require('puppeteer-cluster') 5 | const Bootstrap = require('./bootstrap') 6 | const Pinner = require('./spawn-pinner') 7 | 8 | module.exports = async ({ replicaCount = 10, baseURL = 'http://localhost:1337', spawnPinner = false } = {}) => { 9 | const events = new EventEmitter() 10 | events.waitFor = (eventName) => { 11 | return new Promise((resolve) => { 12 | events.once(eventName, resolve) 13 | }) 14 | } 15 | 16 | const cluster = await Cluster.launch({ 17 | concurrency: Cluster.CONCURRENCY_BROWSER, 18 | maxConcurrency: replicaCount, 19 | // workerCreationDelay: 100, 20 | timeout: 3000000, 21 | // monitor: true, 22 | // puppeteerOptions: { 23 | // headless: false 24 | // devtools: false, 25 | // timeout: 300000, 26 | // dumpio: true, 27 | // handleSIGINT: false, 28 | // pipe: true 29 | // } 30 | }) 31 | 32 | events.close = () => cluster.close() 33 | events.systemMonitor = cluster.systemMonitor 34 | events.idle = () => cluster.idle() 35 | 36 | cluster.on('taskerror', (err, data) => { 37 | events.emit('error', err) 38 | }) 39 | 40 | const pinnerSpawner = spawnPinner ? Pinner : null 41 | 42 | cluster.queue(baseURL, Bootstrap({cluster, replicaCount, events, pinnerSpawner})).then(() => { 43 | events.emit('bootstrapped') 44 | }) 45 | 46 | return events 47 | } 48 | -------------------------------------------------------------------------------- /test/e2e-load/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const delay = require('delay') 4 | const build = require('./build') 5 | const spawnRelay = require('./spawn-relay') 6 | const spawnServer = require('./spawn-server') 7 | const spawnCluster = require('./spawn-cluster') 8 | 9 | let relay 10 | let server 11 | let cluster 12 | 13 | console.log('Going to test the most recent build...') 14 | 15 | const onFatalError = (err) => { 16 | console.log(err) 17 | if (cluster) { 18 | cluster.close() 19 | } 20 | if (relay) { 21 | relay.kill() 22 | } 23 | if (server) { 24 | server.kill() 25 | } 26 | throw err 27 | } 28 | 29 | process.once('unhandledRejection', onFatalError) 30 | process.once('uncaughtException', onFatalError) 31 | 32 | ;(async () => { 33 | console.log('Building...') 34 | await build() 35 | console.log('Built.') 36 | 37 | console.log('Spawning relay...') 38 | relay = await spawnRelay() 39 | console.log('Spawned relay.') 40 | 41 | console.log('Spawning server...') 42 | server = await spawnServer() 43 | console.log('Spawned server.') 44 | 45 | cluster = await spawnCluster({ 46 | replicaCount: 8, 47 | spawnPinner: true 48 | }) 49 | 50 | cluster.on('message', (m) => { 51 | console.log(m) 52 | }) 53 | 54 | cluster.once('bootstrapped', () => { 55 | console.log('bootstrapped') 56 | }) 57 | 58 | cluster.once('ended', async () => { 59 | console.log('ended') 60 | await cluster.idle() 61 | await delay(1000) 62 | relay.kill() 63 | server.kill() 64 | cluster.close() 65 | }) 66 | })() 67 | -------------------------------------------------------------------------------- /public/images/collaborate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI, in coverage mode, or explicitly running all tests 42 | if ( 43 | !process.env.CI && 44 | argv.indexOf('--coverage') === -1 && 45 | argv.indexOf('--watchAll') === -1 46 | ) { 47 | // https://github.com/facebook/create-react-app/issues/5210 48 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 49 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 50 | } 51 | 52 | 53 | jest.run(argv); 54 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const NODE_ENV = process.env.NODE_ENV 2 | 3 | const isDev = NODE_ENV === 'development' 4 | 5 | const defaultSwarmAddresses = { 6 | development: '/ip4/0.0.0.0/tcp/9090/ws/p2p-websocket-star', 7 | production: '/dns4/ws-star1.par.dwebops.pub/tcp/443/wss/p2p-websocket-star' 8 | } 9 | 10 | const swarmAddress = process.env.WEBSOCKET_STAR || 11 | defaultSwarmAddresses[NODE_ENV] 12 | 13 | if (!swarmAddress) { 14 | throw new Error(`Could not find default swarm address for ${NODE_ENV} NODE_ENV`) 15 | } 16 | 17 | module.exports = { 18 | peerStar: { 19 | ipfs: { 20 | swarm: [swarmAddress], 21 | bootstrap: isDev ? [] : [ 22 | '/dns4/ams-1.bootstrap.libp2p.io/tcp/443/wss/ipfs/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd', 23 | '/dns4/lon-1.bootstrap.libp2p.io/tcp/443/wss/ipfs/QmSoLMeWqB7YGVLJN3pNLQpmmEk35v6wYtsMGLzSr5QBU3', 24 | '/dns4/sfo-3.bootstrap.libp2p.io/tcp/443/wss/ipfs/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM', 25 | '/dns4/sgp-1.bootstrap.libp2p.io/tcp/443/wss/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu', 26 | '/dns4/nyc-1.bootstrap.libp2p.io/tcp/443/wss/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm', 27 | '/dns4/nyc-2.bootstrap.libp2p.io/tcp/443/wss/ipfs/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64', 28 | '/dns4/node0.preload.ipfs.io/tcp/443/wss/ipfs/QmZMxNdpMkewiVZLMRxaNxUeZpDUb34pWjZ1kZvsd16Zic', 29 | '/dns4/node1.preload.ipfs.io/tcp/443/wss/ipfs/Qmbut9Ywz9YEDrz8ySBSgWyJk41Uvm2QJPhwDJzJyGFsD6' 30 | ] 31 | }, 32 | transport: { 33 | maxThrottleDelayMS: 1000 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | PeerPad 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/icons/settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /src/components/icons/alpha.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ className, style }) => ( 4 | 5 | 6 | 7 | ) 8 | -------------------------------------------------------------------------------- /src/lib/take-snapshot.js: -------------------------------------------------------------------------------- 1 | import pify from 'pify' 2 | import { encode as b58Encode } from 'bs58' 3 | import React from 'react' 4 | import { renderToString } from 'react-dom/server' 5 | import { convert as convertMarkdown } from './markdown' 6 | 7 | const version = require('../../package.json').version 8 | 9 | export default async (keys, doc, options) => { 10 | let docText = doc.shared.value().join('') 11 | docText = await convertMarkdown(docText, options.type) 12 | docText = Buffer.from(docText) 13 | 14 | const key = await keys.generateSymmetrical() 15 | const encrypt = pify(key.key.encrypt.bind(key.key)) 16 | const html = htmlForDoc(await encrypt(docText), options.docScript, options.DocViewer) 17 | const title = (await doc.sub('title', 'rga')).shared.value().join('') 18 | 19 | const files = [ 20 | { 21 | path: './meta.json', 22 | content: Buffer.from(JSON.stringify({ 23 | type: options.type, 24 | name: title, 25 | version 26 | }, null, '\t')) 27 | }, 28 | { 29 | path: './index.html', 30 | content: html 31 | } 32 | ] 33 | 34 | const stream = doc.app.ipfs.files.addReadableStream() 35 | return new Promise((resolve, reject) => { 36 | stream.once('error', (err) => reject(err)) 37 | stream.on('data', (node) => { 38 | if (node.path === '.') { 39 | resolve({ 40 | key: b58Encode(key.raw), 41 | hash: node.hash 42 | }) 43 | } 44 | }) 45 | files.forEach((file) => stream.write(file)) 46 | stream.end() 47 | }) 48 | } 49 | 50 | function htmlForDoc (encryptedDoc, docScript, DocViewer) { 51 | const doc = '\n' + 52 | renderToString(React.createElement(DocViewer, { 53 | encryptedDoc, 54 | docScript 55 | })) 56 | 57 | return Buffer.from(doc) 58 | } 59 | -------------------------------------------------------------------------------- /src/components/toolbar/Toolbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { 4 | DebugIcon 5 | } from '../icons' 6 | import { LinkButton, SnapshotsButton, ToggleButton } from './buttons' 7 | 8 | const Toolbar = ({ 9 | theme = 'light', 10 | docType, 11 | docName, 12 | encodedKeys, 13 | onTakeSnapshot, 14 | onDebuggingStart, 15 | onDebuggingStop, 16 | isDebuggingEnabled, 17 | snapshots 18 | }) => { 19 | const debugging = 20 |
21 | 27 |
28 | 29 | return ( 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 | {false && debugging} 38 |
39 | ) 40 | } 41 | 42 | Toolbar.propTypes = { 43 | theme: PropTypes.oneOf(['light', 'dark']), 44 | docType: PropTypes.oneOf(['markdown', 'richtext', 'math']).isRequired, 45 | docName: PropTypes.string.isRequired, 46 | encodedKeys: PropTypes.string.isRequired, 47 | onTakeSnapshot: PropTypes.func.isRequired, 48 | snapshots: PropTypes.array.isRequired, 49 | onDirectoryClick: PropTypes.func, 50 | onSettingsClick: PropTypes.func, 51 | onShortcutsClick: PropTypes.func, 52 | onDebuggingStart: PropTypes.func.isRequired, 53 | onDebuggingStop: PropTypes.func.isRequired, 54 | isDebuggingEnabled: PropTypes.bool.isRequired 55 | } 56 | 57 | export default Toolbar 58 | -------------------------------------------------------------------------------- /src/components/header/ViewMode.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { CodeIcon, TextIcon } from '../icons' 4 | 5 | export default class ViewMode extends Component { 6 | constructor (props) { 7 | super(props) 8 | this.onClick = this.onClick.bind(this) 9 | } 10 | 11 | onClick (e) { 12 | const mode = e.currentTarget.getAttribute('data-mode') 13 | const active = this.isActive(mode) 14 | 15 | if (active && this.props.mode !== 'both') { 16 | return 17 | } 18 | 19 | let nextMode 20 | 21 | if (active) { 22 | nextMode = mode === 'source' ? 'preview' : 'source' 23 | } else { 24 | nextMode = 'both' 25 | } 26 | 27 | this.props.onChange(nextMode) 28 | } 29 | 30 | isActive (mode) { 31 | return this.props.mode === mode || this.props.mode === 'both' 32 | } 33 | 34 | render () { 35 | const { onClick } = this 36 | 37 | return ( 38 |
39 | 42 | 45 |
46 | ) 47 | } 48 | } 49 | 50 | ViewMode.propTypes = { 51 | mode: PropTypes.oneOf(['source', 'preview', 'both']).isRequired, 52 | onChange: PropTypes.func.isRequired 53 | } 54 | 55 | const Button = ({ mode, active, onClick, children }) => { 56 | const className = active 57 | ? 'bt bw1 b--bright-turquoise bright-turquoise' 58 | : 'bt bw1 b--firefly white hover--bright-turquoise' 59 | 60 | return ( 61 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /src/components/Editor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | // import Quill from 'quill' 4 | // import 'quill/dist/quill.bubble.css' 5 | import CodeMirror from 'codemirror' 6 | import 'codemirror/mode/markdown/markdown' 7 | import 'codemirror/lib/codemirror.css' 8 | import styles from './Codemirror.module.styl' 9 | 10 | export default class Editor extends Component { 11 | constructor (props) { 12 | super(props) 13 | this.onRef = this.onRef.bind(this) 14 | } 15 | 16 | shouldComponentUpdate () { 17 | return false 18 | } 19 | 20 | onRef (ref) { 21 | this.editorEle = ref 22 | } 23 | 24 | componentDidMount () { 25 | const { onEditor, onChange } = this.props 26 | let editor 27 | 28 | if (this.editorEle) { 29 | // if (type === 'richtext') { 30 | // editor = new Quill(ref, { 31 | // theme: 'bubble' 32 | // }) 33 | 34 | // editor.disable() 35 | // } else { 36 | // See: http://codemirror.net/doc/manual.html#config 37 | editor = CodeMirror(this.editorEle, { 38 | autofocus: true, 39 | inputStyle: 'contenteditable', 40 | lineNumbers: true, 41 | value: '', 42 | viewportMargin: Infinity, 43 | lineWrapping: true, 44 | mode: 'markdown', 45 | readOnly: 'nocursor' 46 | }) 47 | 48 | editor.on('change', () => { 49 | if (onChange) onChange(editor.getValue(), editor) 50 | }) 51 | 52 | window.__peerPadEditor = editor 53 | // } 54 | } 55 | 56 | if (onEditor) onEditor(editor) 57 | } 58 | 59 | render () { 60 | return ( 61 |
62 |
63 |
64 | ) 65 | } 66 | } 67 | 68 | Editor.propTypes = { 69 | type: PropTypes.oneOf(['richtext', 'markdown', 'math']), 70 | editable: PropTypes.bool, 71 | onEditor: PropTypes.func, 72 | onChange: PropTypes.func 73 | } 74 | 75 | Editor.defaultProps = { 76 | editable: true 77 | } 78 | -------------------------------------------------------------------------------- /src/components/Warning.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { CloseIcon, AlphaIcon } from './icons' 3 | 4 | export const Warning = ({onClose}) => ( 5 |
6 | 7 | PeerPad is in Alpha. 8 | 9 | The codebase hasn't been audited by Security specialists and it shouldn't be used to store, share or publish sensitive information. 10 | 11 | {onClose ? ( 12 | 19 | ) : null } 20 |
21 | ) 22 | 23 | export class WarningContainer extends Component { 24 | constructor (props) { 25 | super(props) 26 | this.state = { 27 | showWarning: false 28 | } 29 | this.onDismissWarning = this.onDismissWarning.bind(this) 30 | } 31 | 32 | componentDidMount () { 33 | if (!window.localStorage) return 34 | const showWarning = !window.localStorage.getItem('alpha-warning-dismissed') 35 | if (showWarning !== this.state.showWarning) { 36 | this.setState({ 37 | showWarning: showWarning 38 | }) 39 | } 40 | } 41 | 42 | onDismissWarning () { 43 | this.setState({showWarning: false}) 44 | if (!window.localStorage) return 45 | window.localStorage.setItem('alpha-warning-dismissed', Date.now()) 46 | } 47 | 48 | render () { 49 | if (!this.state.showWarning) return null 50 | return 51 | } 52 | } 53 | 54 | // The Warning should not be dismissable yet... 55 | // see: https://github.com/ipfs-shipyard/peerpad/issues/59 56 | // Export WarningContainer when we're ready for dismissable warnings. 57 | export default Warning 58 | -------------------------------------------------------------------------------- /src/components/DocViewer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | 5 | window.ReactDOM = ReactDOM 6 | 7 | // Minimal styling so the decrypting messages don't look too old school. 8 | // This'll be applied to doc too. 9 | const bodyStyle = { 10 | maxWidth: '54rem', 11 | margin: '0 auto', 12 | padding: '0 10px 50px', 13 | fontFamily: `-apple-system, BlinkMacSystemFont, 'avenir next', avenir, 'helvetica neue', helvetica, ubuntu, roboto, noto, 'segoe ui', arial, sans-serif` 14 | } 15 | 16 | class DocViewer extends Component { 17 | render () { 18 | return ( 19 | 20 | 21 | 22 | 23 | PeerPad doc 24 | {/* normalize.css@7.0.0 */} 25 | 26 | 27 | 28 | 29 | 32 |
33 |
34 |
35 | Loading and decrypting... 36 |
37 |
38 |
39 |