├── 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 |
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 |
9 |
10 |
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 |
18 |
19 |
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 |
19 |
20 |
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 |
9 |
10 | {count ? (
11 | {count}
12 | ) : null}
13 |
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 |
26 | You need to enable JavaScript to run this app.
27 |
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 |
40 |
41 |
42 |
43 |
44 |
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 |
62 |
63 | {children}
64 |
65 |
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 |
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 |
16 | Close
17 |
18 |
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 |
30 | You need to enable JavaScript to get this document.
31 |
32 |
33 |
34 |
35 | Loading and decrypting...
36 |
37 |
38 |
39 |
44 |
45 |
46 |
47 | )
48 | }
49 | }
50 |
51 | DocViewer.propTypes = {
52 | encryptedDoc: PropTypes.object.isRequired,
53 | docScript: PropTypes.string.isRequired
54 | }
55 |
56 | export default DocViewer
57 |
--------------------------------------------------------------------------------
/test/e2e-load/replica-change-text-behavior.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = async ({page, worker, text, beforeWaitMS = 10000, sessionDurationMS = 20000, typeIntervalMS = 50, coolDownMS = 120000}) => {
4 | const startedAt = Date.now()
5 | const endAt = startedAt + sessionDurationMS
6 | let insertOp = false
7 |
8 | while (Date.now() < endAt) {
9 | if (insertOp) {
10 | await insert()
11 | } else {
12 | await remove()
13 | }
14 |
15 | insertOp = !insertOp
16 |
17 | if (Math.random() < 0.05) {
18 | await page.waitFor(4000)
19 | } else {
20 | await page.waitFor(typeIntervalMS)
21 | }
22 | }
23 |
24 | await page.waitFor(coolDownMS)
25 | const finalText = await page.evaluate(() => {
26 | return window.__peerPadEditor.getValue()
27 | })
28 |
29 | text.setFinal(finalText)
30 |
31 | async function insert () {
32 | const [pos, char] = text.randomNewChar()
33 | const [added, currentText] = await page.evaluate((pos, char) => {
34 | const editor = window.__peerPadEditor
35 | if (editor.getValue().length < pos) {
36 | return [false, editor.getValue()]
37 | } else {
38 | const fromPos = editor.posFromIndex(pos)
39 | editor.replaceRange(char, fromPos)
40 | return [true, editor.getValue()]
41 | }
42 | }, pos, char)
43 |
44 | if (added) {
45 | text.addOp(['+', pos, char])
46 | }
47 | text.setCurrent(currentText)
48 | }
49 |
50 | async function remove () {
51 | const [pos, char] = text.randomRemovableChar()
52 | const [removed, currentText] = await page.evaluate((pos, char) => {
53 | const editor = window.__peerPadEditor
54 | const text = editor.getValue()
55 | const ch = text.charAt(pos)
56 | if (ch !== char) {
57 | return [false, editor.getValue()]
58 | } else {
59 | const fromPos = editor.posFromIndex(pos)
60 | const toPos = editor.posFromIndex(pos + 1)
61 | editor.replaceRange('', fromPos, toPos)
62 | return [true, editor.getValue()]
63 | }
64 | }, pos, char)
65 |
66 | if (removed) {
67 | text.addOp(['-', pos, char])
68 | }
69 | text.setCurrent(currentText)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/index.styl:
--------------------------------------------------------------------------------
1 | @import 'colors.styl'
2 |
3 | input:focus,
4 | button:focus
5 | outline 0
6 |
7 | .white-lilac
8 | color $white-lilac
9 |
10 | .dove-gray
11 | color $dove-gray
12 |
13 | .bright-turquoise
14 | color $bright-turquoise
15 |
16 | .pigeon-post
17 | color $pigeon-post
18 |
19 | .big-stone
20 | color $big-stone
21 |
22 | .blue-bayox
23 | color $blue-bayox
24 |
25 | .razzmatazz
26 | color $razzmatazz
27 |
28 | .hover--white-lilac
29 | &:hover
30 | color $white-lilac
31 |
32 | .hover--white
33 | &:hover
34 | color white
35 |
36 | .hover-target:hover .hover--bright-turquoise,
37 | .hover--bright-turquoise:hover
38 | color $bright-turquoise
39 |
40 | .hover--blue-bayox
41 | &:hover
42 | color $blue-bayox
43 |
44 | .dim
45 | opacity 1
46 |
47 | .bg-big-stone
48 | background-color $big-stone
49 |
50 | .bg-firefly
51 | background-color $firefly
52 |
53 | .bg-bright-turquoise
54 | background-color $bright-turquoise
55 |
56 | .bg-cloud-burst
57 | background-color $cloud-burst
58 |
59 | .bg-razzmatazz
60 | background-color $razzmatazz
61 |
62 | .bg-caribbean-green
63 | background-color $caribbean-green
64 |
65 | .b--black-stone
66 | border-color $black-pearl
67 |
68 | .b--firefly
69 | border-color $firefly
70 |
71 | .b--bright-turquoise
72 | border-color $bright-turquoise
73 |
74 | .b--pigeon-post
75 | border-color $pigeon-post
76 |
77 | .b--caribbean-green
78 | border-color $caribbean-green
79 |
80 | .b--caribbean-green-soft
81 | border-color rgba(0, 222, 219, 0.4)
82 |
83 | .fill--current-color path,
84 | .fill--current-color line,
85 | .fill--current-color rect
86 | fill currentColor
87 |
88 | .stroke--current-color path,
89 | .stroke--current-color polyline,
90 | .stroke--current-color circle
91 | stroke currentColor
92 |
93 | .tracked--1
94 | letter-spacing 1px
95 |
96 | .tracked--2
97 | letter-spacing 2px
98 |
99 | .fancy-underline
100 | &::after
101 | content ''
102 | display block
103 | background $caribbean-green
104 | width 100px
105 | height 1px
106 | margin 10px auto
107 |
108 | .appearance-none
109 | appearance none
110 |
111 | .disabled, button:disabled
112 | opacity 0.2
--------------------------------------------------------------------------------
/src/components/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/components/EditorArea.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Editor from './Editor'
3 | import Preview from './Preview'
4 | import Toolbar from './toolbar/Toolbar'
5 |
6 | const EditorArea = ({
7 | docName,
8 | docType,
9 | encodedKeys,
10 | docText,
11 | viewMode,
12 | onEditor,
13 | onEditorValueChange,
14 | snapshots,
15 | onTakeSnapshot,
16 | lastEditorValue,
17 | convertMarkdown,
18 | onDebuggingStart,
19 | onDebuggingStop,
20 | isDebuggingEnabled
21 | }) => {
22 | const editor = (
23 |
24 | )
25 |
26 | const toolbar = (
27 |
38 | )
39 |
40 | // No preview for richtext, source and preview are the same thing
41 | if (docType === 'richtext') {
42 | return (
43 |
44 |
45 | {editor}
46 |
47 | {toolbar}
48 |
49 | )
50 | }
51 |
52 | // source mode has no preview, only editor and toolbar
53 | if (viewMode === 'source') {
54 | return (
55 |
56 |
57 | {editor}
58 |
59 | {toolbar}
60 |
61 | )
62 | }
63 |
64 | const preview =
65 |
66 | if (viewMode === 'both') {
67 | return (
68 |
69 |
70 | {editor}
71 |
72 |
73 | {preview}
74 |
75 |
76 | {toolbar}
77 |
78 |
79 | )
80 | }
81 |
82 | // viewMode === 'prevew'
83 | return (
84 |
85 |
86 | {preview}
87 |
88 | {toolbar}
89 |
90 | )
91 | }
92 |
93 | export default EditorArea
94 |
--------------------------------------------------------------------------------
/docs/DEPLOY.md:
--------------------------------------------------------------------------------
1 | # Deploying PeerPad
2 |
3 | PeerPad is a single page web app. The `src` directory is built into a static website in the `build` directory and deployed to IPFS infrastructure by the [Jenkins CI job](https://ci.ipfs.team/blue/organizations/jenkins/IPFS%20Shipyard%2Fpeer-pad/activity).
4 |
5 | Jenkins watches the [repo](https://github.com/ipfs-shipyard/peer-pad) for changes, and automatically build an IPFS hosted preview site for every commit (e.g. [preview](https://ipfs.io/ipfs/QmUjBkWSiTxdETKv5g3gawzKTG3mfmDaX22QEPE3CkkUf3)).
6 |
7 | **Commits to master are automatically published to peerpad.net**
8 |
9 | The [`ci/Jenkinsfile`](../ci/Jenkinsfile) defines the domain to deploy the app under ([peerpad.net](https://peerpad.net/)) and the directory to use as the webroot for the domain (`/build`).
10 |
11 | On every commit Jenkins runs the `build` target in the [`Makefile`](../Makefile). If it completes without error, then the build dir is added to IPFS and the CID of the directory gives us the IPFS address for the new deployment.
12 |
13 | If the successful build was triggered by a commit to **master**, then that build is published to the domain. The DNS records for peerpad.net are updated to point at the new deployment. The `_dnslink` subdomain is updated with a TXT record like `dnslink=/ipfs/QmHash"`. IPFS uses the dnslink record to map the domain to the current CID, and is used to resolve the IPNS address [/ipns/peerpad.net](https://ipfs.io/ipns/peerpad.net)
14 |
15 | The `build` target defined in the [`Makefile`](../Makefile) is executed on Jenkins on every PR and merge to master. It installs the dependencies, and delegates the rest of the steps to `npm run build` which is defined in the `scripts.build` property in the [`package.json`](../package.json).
16 |
17 | That calls [`scripts/build.js`](../scripts/build.js) which creates the optimised build of the peerpad `src` into the `build/` directory. That script is the result of ejecting a create-react-app generated scaffolding. It's been tweaked to disable mangling, _(though that may no longer be necessary since https://github.com/ipfs/aegir/pull/214)_
18 |
19 | Jenkins CI build peerpad as a "website" job.
20 |
21 | https://ci.ipfs.team/job/IPFS%20Shipyard/job/peer-pad/
22 |
23 | The build is executed in a docker container:
24 |
25 | ```sh
26 | sh 'docker run -i -v `pwd`:/site ipfs/ci-websites make -C /site build'
27 | ```
28 | See: https://github.com/ipfs/jenkins-libs/blob/796cab23030077109f98bbb092d57ed9f4964772/vars/website.groovy#L80
29 |
30 | The ipfs/ci-websites docker image is built from this Dockefile, which defines the build environment https://github.com/ipfs/ci-websites/blob/af0b98f712a5e6bd4174eb86e2ee05c9bdaacb57/Dockerfile
31 |
--------------------------------------------------------------------------------
/src/components/toolbar/buttons/SnapshotsButton.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import Button from './Button'
4 | import SnapshotLink from '../../SnapshotLink'
5 | import { SnapshotIcon } from '../../icons'
6 | import { Dropleft, DropleftMenu } from '../../dropdown/Dropleft'
7 |
8 | export default class SnapshotsButton extends Component {
9 | constructor (props) {
10 | super(props)
11 |
12 | this.state = { dropleftMenuOpen: false }
13 |
14 | this.onDropleftTriggerClick = this.onDropleftTriggerClick.bind(this)
15 | this.onDropleftMenuDismiss = this.onDropleftMenuDismiss.bind(this)
16 | }
17 |
18 | onDropleftTriggerClick () {
19 | this.setState({ dropleftMenuOpen: true })
20 | }
21 |
22 | onDropleftMenuDismiss () {
23 | this.setState({ dropleftMenuOpen: false })
24 | }
25 |
26 | render () {
27 | const {
28 | onDropleftTriggerClick,
29 | onDropleftMenuDismiss
30 | } = this
31 |
32 | const { theme, onTakeSnapshot, snapshots } = this.props
33 | const { dropleftMenuOpen } = this.state
34 |
35 | return (
36 |
37 |
38 |
39 |
40 | {snapshots.length ? (
41 |
42 | {snapshots.map((ss) => {
43 | return (
44 |
45 | {ss.createdAt}
46 |
50 |
51 | )
52 | })}
53 |
54 | ) : (
55 |
No snapshots taken
56 | )}
57 |
58 | Take Snapshot
59 |
60 |
61 |
62 |
63 | )
64 | }
65 | }
66 |
67 | SnapshotsButton.propTypes = {
68 | theme: PropTypes.oneOf(['light', 'dark']),
69 | onTakeSnapshot: PropTypes.func.isRequired,
70 | snapshots: PropTypes.array.isRequired
71 | }
72 |
--------------------------------------------------------------------------------
/config/paths.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const fs = require('fs');
5 | const url = require('url');
6 |
7 | // Make sure any symlinks in the project folder are resolved:
8 | // https://github.com/facebook/create-react-app/issues/637
9 | const appDirectory = fs.realpathSync(process.cwd());
10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
11 |
12 | const envPublicUrl = process.env.PUBLIC_URL;
13 |
14 | function ensureSlash(inputPath, needsSlash) {
15 | const hasSlash = inputPath.endsWith('/');
16 | if (hasSlash && !needsSlash) {
17 | return inputPath.substr(0, inputPath.length - 1);
18 | } else if (!hasSlash && needsSlash) {
19 | return `${inputPath}/`;
20 | } else {
21 | return inputPath;
22 | }
23 | }
24 |
25 | const getPublicUrl = appPackageJson =>
26 | envPublicUrl || require(appPackageJson).homepage;
27 |
28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer
29 | // "public path" at which the app is served.
30 | // Webpack needs to know it to put the right