├── .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 |
26 | You need to enable JavaScript to run this app.
27 |
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 |
92 | Boards
93 |
94 |
95 | Edit
96 |
97 |
98 | New Post
99 |
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 |
34 | Title
35 |
39 |
40 |
41 |
42 | Website
43 |
47 |
48 |
49 | Email
50 |
55 |
56 |
57 |
58 |
59 | Back
60 |
61 | updateBoardMetadata(address, this.state)}>
62 | Save
63 |
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 |
56 | GitHub
57 |
58 |
59 | Add Board
60 |
61 |
62 | Leave Feedback
63 |
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 | closeBoard(address)} basic>
35 | Close
36 |
37 |
38 | View
39 |
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 |
36 |
41 |
42 |
43 |
44 | Back
45 |
46 | openBoard({ address, redirect: true })}>
47 | Open
48 |
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 |
45 | Title
46 |
51 |
52 |
53 | Content
54 |
59 |
60 |
61 | Board
62 |
63 | onSave({ title, text: content })}>
64 | Submit
65 |
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 | }
--------------------------------------------------------------------------------