├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── actions ├── actionTypes.js ├── board.js ├── comment.js └── post.js ├── components ├── App.js ├── Board.js ├── BoardEditorForm.js ├── BoardPage.js ├── Boards.js ├── BoardsItem.js ├── OpenBoardForm.js ├── Post.js └── PostForm.js ├── containers ├── Board.js ├── BoardEditor.js ├── Boards.js ├── OpenBoard.js ├── PostEditor.js ├── WithBoard.js └── WithStats.js ├── index.js ├── orbitdb ├── constants.js └── index.js ├── reducers ├── boards.js ├── index.js ├── openboard.js └── post.js ├── registerServiceWorker.js ├── sagas ├── boards.js ├── index.js ├── persistence.js └── posts.js ├── store └── configureStore.js └── utils ├── ipfs.js ├── orbitdb.js └── persistence.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IPFS Boards 2 | 3 | Boards is an experiment with the goal of figuring out whether it's possible to have 4 | a discussion board, forum or social network that works inside a normal browser tab 5 | without relying on servers, desktop applications, browser extensions, the blockchain 6 | or anything else. 7 | 8 | The goals in detail: 9 | 10 | - all communication should happen in the most distributed way possible using 11 | peer to peer systems 12 | - should work completely offline without anything else other than a web browser 13 | after it's downloaded for the first time 14 | - all data including assets, code and user created content needs to be distributed 15 | and/or replicated between the users 16 | 17 | The project is in prototype stage and works thanks to the [IPFS](https://ipfs.io) 18 | distributed file system. 19 | 20 | The first iteration focuses on creating boards, posting content and commenting. 21 | Moderation tools, encryption, friends lists, private messages, real time chat and 22 | other features will be attempted in the future 23 | 24 | ### Under the hood 25 | 26 | This project is a web UI, [orbit-db-discussion-board](https://github.com/fazo96/orbit-db-discussion-board) 27 | is the underlying library 28 | 29 | ## Try it out 30 | 31 | Super early build hosted on IPFS: 32 | 33 | https://ipfs.io/ipfs/QmYT9EzvQY8zwtxQxUhPcphSGR4XtTRkT4MnXmQKPFamQ7 34 | 35 | This allows you to create boards and posts. There is no moderation 36 | or commenting yet and a lot of things are super wonky 37 | 38 | ## Working on the code 39 | 40 | This is a react project using redux, react-router-redux and redux-saga 41 | 42 | The UI is being implemented using semantic-ui-react 43 | 44 | Clone this repo, then run 45 | 46 | - `npm install` to install dependencies 47 | - `npm start` to start a development server 48 | - `npm run build` to create a production build 49 | 50 | ## Old Version 51 | 52 | You're looking at the new implementation of Boards. If you want to check out the 53 | old one [follow this link](https://github.com/fazo96/ipfs-boards/tree/legacy) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipfs-boards", 3 | "version": "0.1.0", 4 | "homepage": ".", 5 | "private": true, 6 | "dependencies": { 7 | "connected-react-router": "^6.3.1", 8 | "ipfs": "~0.33.0", 9 | "moment": "^2.24.0", 10 | "orbit-db": "~0.19.9", 11 | "orbit-db-discussion-board": "https://github.com/fazo96/orbit-db-discussion-board.git", 12 | "react": "^16.8.4", 13 | "react-dom": "^16.8.4", 14 | "react-hot-loader": "^4.8.0", 15 | "react-redux": "^6.0.1", 16 | "react-router-dom": "^4.3.1", 17 | "react-scripts": "2.1.8", 18 | "redux": "^4.0.1", 19 | "redux-saga": "^1.0.2", 20 | "semantic-ui-css": "^2.4.1", 21 | "semantic-ui-react": "^0.85.0" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build" 26 | }, 27 | "devDependencies": { 28 | "redux-immutable-state-invariant": "^2.1.0" 29 | }, 30 | "browserslist": [ 31 | ">0.2%", 32 | "not dead", 33 | "not ie <= 11", 34 | "not op_mini all" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fazo96/ipfs-boards/e90e840a60c4e519b92a1b792372e108ac4a5329/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | IPFS Boards 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Boards", 3 | "name": "IPFS Boards", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | 2 | export const ADD_POST = 'ADD_POST' 3 | export const EDIT_POST = 'EDIT_POST' 4 | export const HIDE_POST = 'HIDE_POST' 5 | export const UPDATE_BOARD_METADATA = 'UPDATE_BOARD_METADATA' 6 | 7 | export const ADD_COMMENT = 'ADD_COMMENT' 8 | export const EDIT_COMMENT = 'EDIT_COMMENT' 9 | export const HIDE_COMMENT = 'HIDE_COMMENT' 10 | 11 | export const OPEN_BOARD = 'OPEN_BOARD' 12 | export const OPENED_BOARD = 'OPENED_BOARD' 13 | export const CLOSE_BOARD = 'CLOSE_BOARD' 14 | 15 | export const UPDATE_BOARD = 'UPDATE_BOARD' 16 | 17 | export const ORBITDB_WRITE = 'ORBITDB_WRITE' 18 | 19 | export const ORBITDB_REPLICATE = 'ORBITDB_REPLICATE' 20 | export const ORBITDB_REPLICATE_PROGRESS = 'ORBITDB_REPLICATE_PROGRESS' 21 | export const ORBITDB_REPLICATED = 'ORBITDB_REPLICATED' 22 | 23 | export const ERROR = 'ERROR' -------------------------------------------------------------------------------- /src/actions/board.js: -------------------------------------------------------------------------------- 1 | import { 2 | OPEN_BOARD, 3 | CLOSE_BOARD, 4 | OPENED_BOARD, 5 | UPDATE_BOARD_METADATA 6 | } from './actionTypes' 7 | 8 | export function openBoard(board) { 9 | return { 10 | type: OPEN_BOARD, 11 | board 12 | } 13 | } 14 | 15 | export function createdBoard(board) { 16 | return { 17 | type: OPENED_BOARD, 18 | board 19 | } 20 | } 21 | 22 | export function updateBoardMetadata(address, metadata) { 23 | return { 24 | type: UPDATE_BOARD_METADATA, 25 | address, 26 | metadata 27 | } 28 | } 29 | 30 | export function closeBoard(address) { 31 | return { 32 | type: CLOSE_BOARD, 33 | address 34 | } 35 | } -------------------------------------------------------------------------------- /src/actions/comment.js: -------------------------------------------------------------------------------- 1 | import { HIDE_COMMENT, ADD_COMMENT, EDIT_COMMENT } from './actionTypes' 2 | 3 | export function addComment(address, postId, comment, replyTo = 'post') { 4 | return { 5 | type: ADD_COMMENT, 6 | address, 7 | postId, 8 | comment, 9 | replyTo 10 | } 11 | } 12 | 13 | export function editComment(address, postId, commentId, comment, replyTo = 'post') { 14 | return { 15 | type: EDIT_COMMENT, 16 | address, 17 | postId, 18 | commentId, 19 | comment, 20 | replyTo 21 | } 22 | } 23 | 24 | export function hideComment(address, postId, commentId, replyTo = 'post') { 25 | return { 26 | type: HIDE_COMMENT, 27 | address, 28 | postId, 29 | commentId, 30 | replyTo 31 | } 32 | } -------------------------------------------------------------------------------- /src/actions/post.js: -------------------------------------------------------------------------------- 1 | import { ADD_POST, EDIT_POST, HIDE_POST } from './actionTypes' 2 | 3 | export function addPost(address, post) { 4 | return { 5 | type: ADD_POST, 6 | post, 7 | address 8 | } 9 | } 10 | 11 | export function editPost(address, postId, post) { 12 | return { 13 | type: EDIT_POST, 14 | address, 15 | postId, 16 | post, 17 | } 18 | } 19 | 20 | export function hidePost(address, postId) { 21 | return { 22 | type: HIDE_POST, 23 | address, 24 | postId 25 | } 26 | } -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Switch, Route, withRouter } from 'react-router-dom' 3 | import Boards from '../containers/Boards' 4 | import OpenBoard from '../containers/OpenBoard' 5 | import WithBoard from '../containers/WithBoard' 6 | import BoardPage from '../components/BoardPage' 7 | import 'semantic-ui-css/semantic.css' 8 | 9 | class App extends Component { 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | } 20 | 21 | export default withRouter(App) 22 | -------------------------------------------------------------------------------- /src/components/Board.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Post from './Post' 3 | import { Divider, Icon, Grid, Header, List, Button, Card } from 'semantic-ui-react' 4 | import { Link } from 'react-router-dom' 5 | import { shortenAddress } from '../utils/orbitdb'; 6 | import moment from 'moment' 7 | 8 | export default function Board({ address, posts, metadata, replicating, stats, replicationInfo, lastReplicated }) { 9 | const { email, website, title, description } = metadata || {} 10 | const peerCount = (stats.peers || []).length 11 | const online = peerCount > 0 12 | const writeable = stats.access ? (stats.access.writeable ? 'Yes' : 'No') : '?' 13 | let replicationMessage = lastReplicated ? ('Last Activity at ' + moment(lastReplicated).format('H:mm')) : 'No Activity' 14 | if (replicating) { 15 | if (replicationInfo && replicationInfo.max !== undefined) { 16 | replicationMessage = 'Progress: ' + (replicationInfo.progress || 0) + '/' + replicationInfo.max 17 | } else { 18 | replicationMessage = 'Initializing Transfer' 19 | } 20 | } 21 | return 22 | 23 |
24 | {title || 'Unnamed Board'} 25 | Board 26 |
27 | { description ?

{description}

: null } 28 | 29 | 30 | 31 | 32 | 33 | Address 34 | 35 | {address} 36 | 37 | 38 | 39 | 40 | 41 | 42 | Size 43 | {stats.opLogLength || 0} Entries 44 | 45 | 46 | 47 | 48 | 49 | {online ? 'Online' : 'Offline'} 50 | {online ? peerCount + ' Connections' : 'No Connections'} 51 | 52 | 53 | 54 | 55 | 56 | {replicating ? 'Downloading' : 'Download'} 57 | {replicationMessage} 58 | 59 | 60 | 61 | 62 | 63 | Write Access 64 | {writeable} 65 | 66 | 67 | 68 | 69 | 70 | 71 | Posts 72 | {Object.values(posts || {}).length} 73 | 74 | 75 | 76 | 77 | 78 | Website 79 | {website ? {website} : 'N/A'} 80 | 81 | 82 | 83 | 84 | 85 | Mail 86 | {email ? {email} : 'N/A'} 87 | 88 | 89 | 90 |
91 | 94 | 97 | 100 |
101 |
102 | 103 | 104 | {Object.keys(posts || {}).map(i => )} 105 | 106 | 107 |
108 | } -------------------------------------------------------------------------------- /src/components/BoardEditorForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Icon, Container, Card, Form, Button } from 'semantic-ui-react' 3 | import { Link } from 'react-router-dom' 4 | import { shortenAddress } from '../utils/orbitdb'; 5 | 6 | export default class BoardEditorForm extends Component { 7 | constructor(props){ 8 | super(props) 9 | this.state = { 10 | title: props.title || '', 11 | website: props.website || '', 12 | email: props.email || '' 13 | } 14 | } 15 | 16 | render() { 17 | const { title, website, email } = this.state 18 | const { address, updateBoardMetadata } = this.props 19 | return 20 | 21 | 22 | Edit Board 23 | 24 | Boards is an experimental peer to peer application.
25 | All content you publish will be public and may be lost or 26 | changed at any time.
27 | Please do not use this version of Boards 28 | for anything other than testing purposes 29 |
30 |
31 | 32 |
33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 55 | 56 | 57 |
58 | 61 | 64 |
65 |
66 |
67 |
68 |
69 | } 70 | 71 | updateTitle(event) { 72 | const title = event.target.value 73 | this.setState({ title }) 74 | } 75 | 76 | updateWebsite(event) { 77 | const website = event.target.value 78 | this.setState({ website }) 79 | } 80 | 81 | updateEmail(event) { 82 | const email = event.target.value 83 | this.setState({ email }) 84 | } 85 | } -------------------------------------------------------------------------------- /src/components/BoardPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Switch, Route } from 'react-router-dom' 3 | import Board from '../containers/Board' 4 | import BoardEditor from '../containers/BoardEditor' 5 | import PostEditor from '../containers/PostEditor' 6 | import WithStats from '../containers/WithStats' 7 | 8 | function BoardPage({ match, address, posts, metadata }) { 9 | return 10 | 11 | 12 | 13 | 14 | } 15 | 16 | export default BoardPage -------------------------------------------------------------------------------- /src/components/Boards.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { List, Icon, Segment, Divider, Grid, Header, Button, Card } from 'semantic-ui-react' 3 | import { Link } from 'react-router-dom' 4 | import BoardsItem from './BoardsItem' 5 | 6 | export default function Boards({ stats, boards, createBoard, closeBoard }) { 7 | return 8 | 9 |
10 | 11 | 12 | IPFS Boards 13 | Experimental Build 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | Seeding 22 | {Object.keys(boards).length} Boards 23 | 24 | 25 | 26 | 27 | 28 | Connected Peers 29 | {stats.peers.length} 30 | 31 | 32 | 33 | 34 | 35 | Used Space 36 | Not Supported Yet 37 | 38 | 39 | 40 | 41 | 42 | IPFS ID 43 | {stats.id} 44 | 45 | 46 | 47 | 48 | 49 | OrbitDB Public Key 50 | {stats.pubKey} 51 | 52 | 53 | 54 |
55 | 58 | 61 | 64 |
65 |
66 | 67 | 68 | {Object.values(boards).map(board => )} 69 | {Object.keys(boards).length === 0 ? No boards opened : null} 70 | 71 | 72 |
73 | } -------------------------------------------------------------------------------- /src/components/BoardsItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Icon, List, Button, Card } from 'semantic-ui-react' 3 | import { Link } from 'react-router-dom' 4 | import { shortenAddress } from '../utils/orbitdb' 5 | 6 | export default function BoardsItem({ address, metadata, name, closeBoard }) { 7 | return 8 | 9 | 10 | { metadata.title || 'Unnamed board' } 11 | 12 | Board 13 | 14 | 15 | 16 | 17 | 18 | 19 | Name 20 | {name} 21 | 22 | 23 | 24 | 25 | 26 | Address 27 | {address} 28 | 29 | 30 | 31 | 32 | 33 |
34 | 37 | 40 |
41 |
42 |
43 | } -------------------------------------------------------------------------------- /src/components/OpenBoardForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Icon, Container, Card, Form, Button } from 'semantic-ui-react' 3 | import { Link } from 'react-router-dom' 4 | 5 | export default class OpenBoardForm extends Component { 6 | constructor(props){ 7 | super(props) 8 | this.state = { 9 | address: props.address || '' 10 | } 11 | } 12 | 13 | updateAddress(event) { 14 | const address = event.target.value 15 | this.setState({ address }) 16 | } 17 | 18 | render() { 19 | const { address } = this.state 20 | const { openBoard, opening } = this.props 21 | return 22 | 23 | 24 | Open a Board 25 | 26 | Boards is an experimental peer to peer application.
27 | All content you publish will be public and may be lost or 28 | changed at any time.
29 | Please do not use this version of Boards 30 | for anything other than testing purposes 31 |
32 |
33 | 34 |
35 | 36 | 41 | 42 |
43 | 46 | 49 |
50 |
51 |
52 |
53 |
54 | } 55 | } -------------------------------------------------------------------------------- /src/components/Post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { List, Card } from 'semantic-ui-react' 3 | 4 | export default function Post({ title, multihash, pubKey }) { 5 | return 6 | 7 | 8 | {title} 9 | 10 | Post 11 | 12 | 13 | 14 | 15 | 16 | 17 | Signed By 18 | {pubKey || 'Unknown'} 19 | 20 | 21 | 22 | 23 | 24 | Comments 25 | Not Supported Yet 26 | 27 | 28 | 29 | 30 | 31 | Content 32 | 33 | {multihash} 34 | 35 | 36 | 37 | 38 | 39 | 40 | } -------------------------------------------------------------------------------- /src/components/PostForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { Card, Container, Form, Icon, Button } from 'semantic-ui-react' 4 | import { shortenAddress } from '../utils/orbitdb'; 5 | 6 | export default class PostForm extends Component { 7 | constructor(props){ 8 | super(props) 9 | this.state = { 10 | title: props.title || '', 11 | content: props.content || '' 12 | } 13 | } 14 | 15 | updateTitle(event) { 16 | this.setState({ title: event.target.value }) 17 | } 18 | 19 | updateContent(event) { 20 | this.setState({ content: event.target.value }) 21 | } 22 | 23 | render() { 24 | const { title, content } = this.state 25 | const { onSave, board } = this.props 26 | const { address } = board 27 | return 28 | 29 | 30 | New Post 31 | 32 | Boards is an experimental peer to peer application.
33 | All content you publish will be public and may be lost or 34 | changed at any time.
35 | Please do not use this version of Boards 36 | for anything other than testing purposes 37 |
38 |
39 | 40 | {address} 41 | 42 | 43 |
44 | 45 | 46 | 51 | 52 | 53 | 54 | 59 | 60 | 63 | 66 |
67 |
68 |
69 |
70 | } 71 | } -------------------------------------------------------------------------------- /src/containers/Board.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import BoardComponent from '../components/Board' 4 | import { getBoardAddress } from '../utils/orbitdb' 5 | 6 | function Board({ stats, location, match, boards }) { 7 | const { hash, name } = match.params 8 | const address = getBoardAddress(hash, name) 9 | const boardStats = stats.dbs[address] || {} 10 | return 11 | } 12 | 13 | function mapStateToProps(state){ 14 | return { 15 | boards: state.boards.boards 16 | } 17 | } 18 | 19 | export default connect( 20 | mapStateToProps 21 | )(Board) 22 | -------------------------------------------------------------------------------- /src/containers/BoardEditor.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { connect } from 'react-redux' 4 | import BoardEditorForm from '../components/BoardEditorForm' 5 | import { updateBoardMetadata } from '../actions/board' 6 | import { getBoardAddress } from '../utils/orbitdb' 7 | 8 | function BoardEditor({ boards, boardEditor, match, updateBoardMetadata }) { 9 | const { hash, name } = match.params 10 | const address = getBoardAddress(hash, name) 11 | const board = boards[address] 12 | return 18 | } 19 | 20 | function mapStateToProps(state){ 21 | return { 22 | boards: state.boards.boards 23 | } 24 | } 25 | 26 | function mapDispatchToProps(dispatch) { 27 | return { 28 | updateBoardMetadata: (address, metadata) => dispatch(updateBoardMetadata(address, metadata)) 29 | } 30 | } 31 | 32 | export default connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(BoardEditor) 36 | -------------------------------------------------------------------------------- /src/containers/Boards.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { push } from 'react-router-redux' 4 | import BoardsComponent from '../components/Boards' 5 | import WithStats from './WithStats' 6 | import { closeBoard } from '../actions/board' 7 | 8 | const WrappedComponent = WithStats(BoardsComponent) 9 | 10 | function Boards({ boards, createBoard, closeBoard }) { 11 | return 16 | } 17 | 18 | function mapStateToProps(state){ 19 | return { 20 | boards: state.boards.boards 21 | } 22 | } 23 | 24 | function mapDispatchToProps(dispatch){ 25 | return { 26 | createBoard: () => dispatch(push('/b/new')), 27 | closeBoard: address => dispatch(closeBoard(address)), 28 | } 29 | } 30 | 31 | export default connect( 32 | mapStateToProps, 33 | mapDispatchToProps 34 | )(Boards) 35 | -------------------------------------------------------------------------------- /src/containers/OpenBoard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import OpenBoardForm from '../components/OpenBoardForm' 4 | import { openBoard } from '../actions/board' 5 | 6 | function OpenBoard(props) { 7 | return 8 | } 9 | 10 | function mapStateToProps(state){ 11 | return { 12 | opening: state.openBoard.opening 13 | } 14 | } 15 | 16 | function mapDispatchToProps(dispatch) { 17 | return { 18 | openBoard: board => dispatch(openBoard(board)) 19 | } 20 | } 21 | 22 | export default connect( 23 | mapStateToProps, 24 | mapDispatchToProps 25 | )(OpenBoard) 26 | -------------------------------------------------------------------------------- /src/containers/PostEditor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import PostForm from '../components/PostForm' 4 | import { addPost } from '../actions/post' 5 | import { getBoardAddress } from '../utils/orbitdb'; 6 | 7 | class PostEditor extends Component { 8 | render() { 9 | const { post, addPost, match, boards } = this.props 10 | const address = getBoardAddress(match.params.hash, match.params.name) 11 | const board = boards[address] 12 | return addPost(address, p)} /> 13 | } 14 | } 15 | 16 | function mapStateToProps(state){ 17 | return { 18 | post: state.postEditor.post, 19 | boards: state.boards.boards 20 | } 21 | } 22 | 23 | function mapDispatchToProps(dispatch) { 24 | return { 25 | addPost: (address, post) => dispatch(addPost(address, post)) 26 | } 27 | } 28 | 29 | export default connect( 30 | mapStateToProps, 31 | mapDispatchToProps 32 | )(PostEditor) -------------------------------------------------------------------------------- /src/containers/WithBoard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Dimmer } from 'semantic-ui-react' 3 | import { connect } from 'react-redux' 4 | import { openBoard } from '../actions/board' 5 | import { getBoardAddress } from '../utils/orbitdb' 6 | 7 | function mapStateToProps(state){ 8 | return { 9 | boards: state.boards.boards 10 | } 11 | } 12 | 13 | function mapDispatchToProps(dispatch){ 14 | return { 15 | openBoard: address => dispatch(openBoard({ address, redirect: false })) 16 | } 17 | } 18 | 19 | export default function WithBoard(WrappedComponent) { 20 | class ToExport extends Component { 21 | 22 | componentDidMount() { 23 | const { boards, match } = this.props 24 | const address = getBoardAddress(match.params.hash, match.params.name) 25 | if (!boards[address]) { 26 | this.props.openBoard(address) 27 | } 28 | } 29 | 30 | componentWillReceiveProps({ match, boards }) { 31 | const address = getBoardAddress(match.params.hash, match.params.name) 32 | if (!boards[address]) { 33 | this.props.openBoard(address) 34 | } 35 | } 36 | 37 | render() { 38 | const { boards, match } = this.props 39 | const address = getBoardAddress(match.params.hash, match.params.name) 40 | const board = boards[address] 41 | if (board) { 42 | return 43 | } else { 44 | return 45 | Opening this board 46 | 47 | } 48 | } 49 | } 50 | 51 | return connect( 52 | mapStateToProps, 53 | mapDispatchToProps 54 | )(ToExport) 55 | 56 | } -------------------------------------------------------------------------------- /src/containers/WithStats.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { getStats } from '../utils/ipfs' 3 | 4 | export default function(WrappedComponent) { 5 | return class extends Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = { 9 | stats: { 10 | id: '?', 11 | peers: [], 12 | pubKey: '?', 13 | dbs: {} 14 | }, 15 | timeout: null 16 | } 17 | } 18 | 19 | async refresh(loop = true) { 20 | const newStats = await getStats() 21 | const stats = Object.assign({}, this.state.stats, newStats) 22 | this.setState({ stats }, loop ? this.refreshDelayed.bind(this) : undefined) 23 | } 24 | 25 | refreshDelayed() { 26 | this.timeout = setTimeout(() => { 27 | this.refresh() 28 | }, 2000) 29 | } 30 | 31 | componentDidMount() { 32 | this.refresh() 33 | } 34 | 35 | componentWillUnmount() { 36 | if (this.timeout) clearTimeout(this.timeout) 37 | } 38 | 39 | render() { 40 | return 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'react-hot-loader/patch' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { AppContainer } from 'react-hot-loader' 5 | import configureStore, { history } from './store/configureStore' 6 | import App from './components/App' 7 | import registerServiceWorker from './registerServiceWorker' 8 | import { Provider } from 'react-redux' 9 | import { ConnectedRouter } from 'connected-react-router' 10 | import { start } from './orbitdb' 11 | 12 | const store = configureStore(); 13 | 14 | render( 15 | 16 | 17 | 18 | 19 | 20 | 21 | , 22 | document.getElementById('root') 23 | ); 24 | 25 | if (module.hot) { 26 | module.hot.accept('./components/App', () => { 27 | const NewApp = require('./components/App').default 28 | render( 29 | 30 | 31 | 32 | 33 | 34 | 35 | , 36 | document.getElementById('root') 37 | ) 38 | }) 39 | } 40 | 41 | registerServiceWorker() 42 | start(store.dispatch) -------------------------------------------------------------------------------- /src/orbitdb/constants.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | ADD_POST: 'ADD_POST', 4 | UPDATE_POST: 'UPDATE_POST', 5 | ADD_COMMENT: 'ADD_COMMENT', 6 | UPDATE_COMMENT: 'UPDATE_COMMENT', 7 | UPDATE_METADATA: 'UPDATE_METADATA', 8 | }; -------------------------------------------------------------------------------- /src/orbitdb/index.js: -------------------------------------------------------------------------------- 1 | import IPFS from 'ipfs' 2 | import OrbitDB from 'orbit-db' 3 | import BoardStore from 'orbit-db-discussion-board' 4 | import multihashes from 'multihashes' 5 | 6 | export function isValidID(id) { 7 | try { 8 | if (typeof id === 'string' && multihashes.fromB58String(id)) return true 9 | } catch (error) { 10 | return false 11 | } 12 | return false 13 | } 14 | 15 | export async function start() { 16 | if (!window.ipfs) { 17 | window.ipfs = new IPFS({ 18 | repo: 'ipfs-v6-boards-v0', 19 | EXPERIMENTAL: { 20 | pubsub: true 21 | } 22 | }); 23 | await new Promise(resolve => { 24 | window.ipfs.on('ready', () => resolve()) 25 | }) 26 | } 27 | if (!window.orbitDb) { 28 | OrbitDB.addDatabaseType(BoardStore.type, BoardStore) 29 | window.orbitDb = new OrbitDB(window.ipfs) 30 | } 31 | } 32 | 33 | export async function open(address, metadata) { 34 | if (window.dbs && window.dbs[address]) return window.dbs[address] 35 | await start() 36 | const options = { 37 | type: BoardStore.type, 38 | create: true, 39 | write: ['*'] 40 | } 41 | const db = await window.orbitDb.open(address, options) 42 | await db.load() 43 | if (metadata) { 44 | await db.updateMetadata(metadata) 45 | } 46 | if (!window.dbs) window.dbs = {} 47 | window.dbs[db.address.toString()] = db 48 | return db 49 | } 50 | 51 | export function connectDb(db, dispatch) { 52 | db.events.on('write', (dbname, hash, entry) => { 53 | dispatch({ 54 | type: 'ORBITDB_WRITE', 55 | time: Date.now(), 56 | address: db.address.toString(), 57 | hash, 58 | entry 59 | }) 60 | }) 61 | db.events.on('replicated', address => { 62 | dispatch({ 63 | type: 'ORBITDB_REPLICATED', 64 | time: Date.now(), 65 | address: db.address.toString() 66 | }) 67 | }) 68 | db.events.on('replicate.progress', (address, hash, entry, progress, have) => { 69 | dispatch({ 70 | type: 'ORBITDB_REPLICATE_PROGRESS', 71 | address: db.address.toString(), 72 | hash, 73 | entry, 74 | progress, 75 | have, 76 | time: Date.now(), 77 | replicationInfo: Object.assign({}, db._replicationInfo) 78 | }) 79 | }) 80 | db.events.on('replicate', address => { 81 | dispatch({ 82 | type: 'ORBITDB_REPLICATE', 83 | time: Date.now(), 84 | address: db.address.toString() 85 | }) 86 | }) 87 | db.events.on('close', address => { 88 | dispatch({ 89 | type: 'ORBITDB_CLOSE', 90 | time: Date.now(), 91 | address: db.address.toString() 92 | }) 93 | }) 94 | db.events.on('load', address => { 95 | dispatch({ 96 | type: 'ORBITDB_LOAD', 97 | time: Date.now(), 98 | address: db.address.toString() 99 | }) 100 | }) 101 | db.events.on('load.progress', (address, hash, entry, progress, total) => { 102 | dispatch({ 103 | type: 'ORBITDB_LOAD_PROGRESS', 104 | time: Date.now(), 105 | address: db.address.toString(), 106 | hash, 107 | entry, 108 | progress, 109 | total 110 | }) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /src/reducers/boards.js: -------------------------------------------------------------------------------- 1 | import { 2 | OPENED_BOARD, 3 | CLOSE_BOARD, 4 | UPDATE_BOARD, 5 | ORBITDB_REPLICATE_PROGRESS, 6 | ORBITDB_REPLICATED, 7 | ORBITDB_REPLICATE 8 | } from '../actions/actionTypes' 9 | 10 | function getInitialState() { 11 | return { 12 | boards: {} 13 | } 14 | } 15 | 16 | function updateBoard(existingBoards, address, value) { 17 | return Object.assign({}, existingBoards, { 18 | [address]: Object.assign({}, existingBoards[address] || {}, value) 19 | }) 20 | } 21 | 22 | function deleteBoard(existingBoards, address) { 23 | const boards = Object.assign({}, existingBoards) 24 | delete boards[address] 25 | return boards 26 | } 27 | 28 | export default function BoardsReducer(state = getInitialState(), action) { 29 | let address 30 | switch (action.type) { 31 | case OPENED_BOARD: 32 | address = action.board.address 33 | return Object.assign({}, state, { boards: updateBoard(state.boards, address, Object.assign({}, action.board, { open: true })) }) 34 | case UPDATE_BOARD: 35 | address = action.address 36 | let { posts, metadata } = action 37 | return Object.assign({}, state, { boards: updateBoard(state.boards, address, { posts, metadata })}) 38 | case ORBITDB_REPLICATE: 39 | address = action.address 40 | return Object.assign({}, state, { boards: updateBoard(state.boards, address, { 41 | replicating: true 42 | })}) 43 | case ORBITDB_REPLICATE_PROGRESS: 44 | address = action.address 45 | return Object.assign({}, state, { boards: updateBoard(state.boards, address, { 46 | replicating: true, 47 | replicationInfo: action.replicationInfo 48 | })}) 49 | case ORBITDB_REPLICATED: 50 | address = action.address 51 | return Object.assign({}, state, { boards: updateBoard(state.boards, address, { 52 | replicating: false, 53 | lastReplicated: action.time 54 | })}) 55 | case CLOSE_BOARD: 56 | address = action.address 57 | return Object.assign({}, state, { 58 | boards: deleteBoard(state.boards, address) 59 | }) 60 | default: 61 | return state; 62 | } 63 | } -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import { connectRouter} from 'connected-react-router' 3 | import postReducer from './post' 4 | import boardsReducer from './boards' 5 | import openBoardReducer from './openboard' 6 | 7 | export default history => combineReducers({ 8 | router: connectRouter(history), 9 | postEditor: postReducer, 10 | boards: boardsReducer, 11 | openBoard: openBoardReducer 12 | }) -------------------------------------------------------------------------------- /src/reducers/openboard.js: -------------------------------------------------------------------------------- 1 | import { 2 | OPEN_BOARD, 3 | OPENED_BOARD 4 | } from '../actions/actionTypes' 5 | 6 | function getInitialState() { 7 | return { 8 | opening: false 9 | } 10 | } 11 | 12 | export default function openBoardReducer(state = getInitialState(), action) { 13 | switch (action.type) { 14 | case OPEN_BOARD: 15 | return Object.assign({}, state, { opening: true }) 16 | case OPENED_BOARD: 17 | return Object.assign({}, state, { opening: false }) 18 | default: 19 | return state 20 | } 21 | } -------------------------------------------------------------------------------- /src/reducers/post.js: -------------------------------------------------------------------------------- 1 | import { ADD_POST } from '../actions/actionTypes' 2 | 3 | function getInitialState(){ 4 | return { 5 | post: { 6 | title: '', 7 | content: '' 8 | } 9 | } 10 | } 11 | 12 | export default function(state = getInitialState(), action) { 13 | switch (action.type) { 14 | case ADD_POST: 15 | return Object.assign({}, state, { post: action.post }) 16 | default: 17 | return state 18 | } 19 | } -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/sagas/boards.js: -------------------------------------------------------------------------------- 1 | import { put, call, fork, take } from 'redux-saga/effects' 2 | import { eventChannel } from 'redux-saga' 3 | import { push } from 'react-router-redux' 4 | import { open, connectDb } from '../orbitdb' 5 | import { createdBoard } from '../actions/board' 6 | import { shortenAddress, closeBoard as closeOrbitDBBoard } from '../utils/orbitdb' 7 | import { UPDATE_BOARD } from '../actions/actionTypes' 8 | import { saveSaga } from './persistence' 9 | 10 | export function* goToBoard({ board }){ 11 | if (board.redirect) { 12 | yield put(push(shortenAddress(board.address))) 13 | } 14 | } 15 | 16 | export function* updateBoard({ address }){ 17 | const db = window.dbs[address] 18 | yield put({ 19 | type: UPDATE_BOARD, 20 | address, 21 | posts: db.posts, 22 | metadata: Object.assign({}, db._index._index.metadata) // TODO: fix in lib and use db.metadata 23 | }) 24 | } 25 | 26 | export function* closeBoard({ address }){ 27 | yield call(closeOrbitDBBoard, address) 28 | yield saveSaga() 29 | } 30 | 31 | export function* updateBoardMetadata({ address, metadata }){ 32 | const db = window.dbs[address] 33 | if (db) { 34 | yield call([db, db.updateMetadata], [metadata]) 35 | yield goToBoard({ board: { address } }); 36 | } else { 37 | yield put({ type: 'ERROR', error: address + ' not found' }) 38 | } 39 | } 40 | 41 | export function* openBoard({ board }) { 42 | let db 43 | try { 44 | const metadata = board.title ? { title: board.title } : null 45 | db = yield call(open, board.address, metadata) 46 | } catch (error) { 47 | yield put({ type: 'ERROR', error }) 48 | } 49 | if (db) { 50 | const address = db.address.toString() 51 | const dbInfo = { address } 52 | dbInfo.posts = db.posts 53 | dbInfo.metadata = Object.assign({}, db._index._index.metadata) // TODO: fix in lib and use db.metadata 54 | dbInfo.name = db.dbname 55 | try { 56 | const channel = yield call(createDbEventChannel, db) 57 | yield fork(watchDb, channel) 58 | yield put(createdBoard(Object.assign({ redirect: !!board.redirect }, board, dbInfo))) 59 | } catch (error) { 60 | yield put({ type: 'ERROR', error }) 61 | } 62 | } 63 | } 64 | 65 | function* watchDb(channel) { 66 | // Dispatches action coming from the channel, terminates when ORBITDB_CLOSE arrives 67 | let action 68 | while(!action || action.type !== 'ORBITDB_CLOSE') { 69 | action = yield take(channel) 70 | yield put(action) 71 | } 72 | } 73 | 74 | function createDbEventChannel(db) { 75 | return eventChannel(emitter => { 76 | connectDb(db, emitter) 77 | return () => db.close() 78 | }) 79 | } -------------------------------------------------------------------------------- /src/sagas/index.js: -------------------------------------------------------------------------------- 1 | import { takeEvery, put, call } from 'redux-saga/effects' 2 | import { 3 | OPEN_BOARD, 4 | OPENED_BOARD, 5 | CLOSE_BOARD, 6 | ADD_POST, 7 | ORBITDB_REPLICATED, 8 | ORBITDB_WRITE, 9 | UPDATE_BOARD_METADATA 10 | } from '../actions/actionTypes' 11 | import { openBoard, updateBoard, goToBoard, updateBoardMetadata, closeBoard } from './boards' 12 | import { addPost } from './posts' 13 | import { openPreviousBoards, saveSaga } from './persistence' 14 | 15 | export default function* saga(){ 16 | yield takeEvery(OPEN_BOARD, openBoard) 17 | yield takeEvery(OPENED_BOARD, goToBoard) 18 | yield takeEvery(OPENED_BOARD, saveSaga) 19 | yield takeEvery(CLOSE_BOARD, closeBoard) 20 | 21 | yield takeEvery(ADD_POST, addPost) 22 | yield takeEvery(UPDATE_BOARD_METADATA, updateBoardMetadata) 23 | 24 | yield takeEvery(ORBITDB_WRITE, updateBoard) 25 | yield takeEvery(ORBITDB_REPLICATED, updateBoard) 26 | 27 | yield openPreviousBoards() 28 | } -------------------------------------------------------------------------------- /src/sagas/persistence.js: -------------------------------------------------------------------------------- 1 | import { call, put } from 'redux-saga/effects' 2 | import { openBoard } from '../actions/board' 3 | import { save, load } from '../utils/persistence' 4 | 5 | export function* openPreviousBoards() { 6 | const data = yield call(load) 7 | if (Array.isArray(data.addresses)) { 8 | for (const address of data.addresses) { 9 | yield put(openBoard({ address, redirect: false })) 10 | } 11 | } 12 | } 13 | 14 | export function* saveSaga() { 15 | yield call(save) 16 | } -------------------------------------------------------------------------------- /src/sagas/posts.js: -------------------------------------------------------------------------------- 1 | import { call } from 'redux-saga/effects' 2 | import { goToBoard } from './boards'; 3 | 4 | export function* addPost({ address, post }) { 5 | const db = window.dbs[address] 6 | const { title, text } = post 7 | yield call([db, db.addPost], { title, text }) 8 | yield goToBoard({ board: { address, redirect: true } }); 9 | // TODO: goto post 10 | } 11 | 12 | export function* editPost({ address, postId, post }) { 13 | const db = window.dbs[address] 14 | const { title, text } = post 15 | yield call([db, db.updatePost], postId, { title, text }) 16 | yield goToBoard({ board: { address, redirect: true } }); 17 | // TODO: goto post 18 | } -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import {createStore, compose, applyMiddleware} from 'redux'; 2 | import reduxImmutableStateInvariant from 'redux-immutable-state-invariant'; 3 | import createSagaMiddleware from 'redux-saga'; 4 | import saga from '../sagas'; 5 | import createHistory from 'history/createHashHistory'; 6 | import { routerMiddleware } from 'connected-react-router'; 7 | import createRootReducer from '../reducers'; 8 | 9 | const sagaMiddleware = createSagaMiddleware(); 10 | 11 | export const history = createHistory(); 12 | 13 | function configureStoreProd(initialState) { 14 | const middlewares = [ 15 | routerMiddleware(history), 16 | sagaMiddleware, 17 | ]; 18 | 19 | const store = createStore(createRootReducer(history), initialState, compose( 20 | applyMiddleware(...middlewares) 21 | ) 22 | ); 23 | 24 | sagaMiddleware.run(saga); 25 | 26 | return store; 27 | } 28 | 29 | function configureStoreDev(initialState) { 30 | const middlewares = [ 31 | // Redux middleware that spits an error on you when you try to mutate your state either inside a dispatch or between dispatches. 32 | reduxImmutableStateInvariant(), 33 | routerMiddleware(history), 34 | sagaMiddleware, 35 | ]; 36 | 37 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // add support for Redux dev tools 38 | const store = createStore(createRootReducer(history), initialState, composeEnhancers( 39 | applyMiddleware(...middlewares) 40 | ) 41 | ); 42 | 43 | let sagaTask = sagaMiddleware.run(saga); 44 | 45 | if (module.hot) { 46 | // Enable Webpack hot module replacement for reducers 47 | module.hot.accept('../reducers', () => { 48 | const nextReducer = require('../reducers').default; // eslint-disable-line global-require 49 | store.replaceReducer(nextReducer); 50 | }); 51 | // Enable Webpack hot module replacement for sagas 52 | module.hot.accept('../sagas', () => { 53 | const newSaga = require('../sagas').default; 54 | sagaTask.cancel(); 55 | sagaTask.done.then(() => { 56 | sagaTask = sagaMiddleware.run(newSaga); 57 | }); 58 | }); 59 | } 60 | 61 | return store; 62 | } 63 | 64 | const configureStore = process.env.NODE_ENV === 'production' ? configureStoreProd : configureStoreDev; 65 | 66 | export default configureStore; 67 | -------------------------------------------------------------------------------- /src/utils/ipfs.js: -------------------------------------------------------------------------------- 1 | export async function ipfsPut(content) { 2 | const obj = { 3 | content: Buffer.from(content), 4 | path: '/' 5 | } 6 | const response = await window.ipfs.files.add(obj) 7 | return response[0].hash 8 | } 9 | 10 | export async function readText(multihash) { 11 | const buffer = await window.ipfs.object.get(multihash) 12 | return buffer.toString('utf-8') 13 | } 14 | 15 | export async function getStats() { 16 | const ipfs = window.ipfs; 17 | const orbitDb = window.orbitDb 18 | const dbs = {} 19 | const stats = {} 20 | if (ipfs && ipfs.isOnline()) { 21 | stats.ipfsLoaded = true 22 | const peers = await ipfs.swarm.peers() 23 | const id = await ipfs.id() 24 | stats.peers = peers.map(p => p.peer.id._idB58String) 25 | stats.id = id.id 26 | } else { 27 | stats.ipfsLoaded = false 28 | } 29 | if (stats.ipfsLoaded && orbitDb) { 30 | stats.orbitDbLoaded = true 31 | stats.pubKey = await orbitDb.key.getPublic('hex') 32 | Object.values(window.dbs || {}).forEach(db => { 33 | let writeable = db.access.write.indexOf('*') >= 0 || db.access.write.indexOf(stats.pubKey) >= 0 34 | const dbInfo = { 35 | opLogLength: db._oplog.length, 36 | access: { 37 | admin: db.access.admin, 38 | read: db.access.read, 39 | write: db.access.write, 40 | writeable 41 | }, 42 | peers: [] 43 | } 44 | const subscription = orbitDb._pubsub._subscriptions[db.address] 45 | if (subscription && subscription.room) { 46 | dbInfo.peers = [...(subscription.room._peers || [])] 47 | } 48 | dbs[db.address] = dbInfo 49 | }) 50 | } else { 51 | stats.orbitDbLoaded = false 52 | } 53 | stats.dbs = dbs 54 | return stats 55 | } -------------------------------------------------------------------------------- /src/utils/orbitdb.js: -------------------------------------------------------------------------------- 1 | 2 | export function getBoardAddress(hash, name) { 3 | return '/orbitdb/' + hash + '/' + name 4 | } 5 | 6 | export function shortenAddress(address) { 7 | return address.replace(/^\/orbitdb/, '/b') 8 | } 9 | 10 | export function closeBoard(address) { 11 | const db = window.dbs[address] 12 | delete window.dbs[address] 13 | if (db && db.close) db.close() 14 | } -------------------------------------------------------------------------------- /src/utils/persistence.js: -------------------------------------------------------------------------------- 1 | 2 | export function save(){ 3 | const obj = { 4 | addresses: Object.keys(window.dbs || {}) 5 | } 6 | localStorage.setItem('ipfs-boards-v0', JSON.stringify(obj)) 7 | } 8 | 9 | export function load(){ 10 | const str = localStorage.getItem('ipfs-boards-v0') 11 | try { 12 | return JSON.parse(str) || {}; 13 | } catch (error) { 14 | return {} 15 | } 16 | } --------------------------------------------------------------------------------