1
0
mirror of https://github.com/fazo96/ipfs-boards synced 2025-01-09 12:19:49 +01:00

WIP prototype

This commit is contained in:
Enrico Fasoli 2018-02-07 18:50:32 +01:00
parent 7483c683a4
commit 502244dbe1
No known key found for this signature in database
GPG Key ID: 1238873C5F27DB4D
20 changed files with 182 additions and 152 deletions

View File

@ -1,9 +1,8 @@
export const ADD_POST = 'ADD_POST' export const ADD_POST = 'ADD_POST'
export const CREATE_BOARD = 'CREATE_BOARD' export const OPEN_BOARD = 'OPEN_BOARD'
export const CREATING_BOARD = 'CREATING_BOARD' export const OPENED_BOARD = 'OPENED_BOARD'
export const CREATED_BOARD = 'CREATED_BOARD'
export const UPDATE_BOARD = 'UPDATE_BOARD' export const UPDATE_BOARD = 'UPDATE_BOARD'

View File

@ -1,27 +1,19 @@
import { import {
CREATE_BOARD, OPEN_BOARD,
CREATING_BOARD, OPENED_BOARD,
CREATED_BOARD,
BOARD_ERROR BOARD_ERROR
} from './actionTypes' } from './actionTypes'
export function createBoard(board) { export function openBoard(board) {
return { return {
type: CREATE_BOARD, type: OPEN_BOARD,
board
}
}
export function creatingBoard(board) {
return {
type: CREATING_BOARD,
board board
} }
} }
export function createdBoard(board) { export function createdBoard(board) {
return { return {
type: CREATED_BOARD, type: OPENED_BOARD,
board board
} }
} }

View File

@ -1,9 +1,9 @@
import { ADD_POST } from './actionTypes' import { ADD_POST } from './actionTypes'
export function addPost(boardId, post) { export function addPost(address, post) {
return { return {
type: ADD_POST, type: ADD_POST,
post, post,
boardId address
} }
} }

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Switch, Route, withRouter } from 'react-router-dom' import { Switch, Route, withRouter } from 'react-router-dom'
import Boards from '../containers/Boards' import Boards from '../containers/Boards'
import PostEditor from '../containers/PostEditor'
import BoardEditor from '../containers/BoardEditor' import BoardEditor from '../containers/BoardEditor'
import Board from '../containers/Board' import WithBoard from '../containers/WithBoard'
import BoardPage from '../components/BoardPage'
import 'semantic-ui-css/semantic.css' import 'semantic-ui-css/semantic.css'
class App extends Component { class App extends Component {
@ -11,8 +11,7 @@ class App extends Component {
return ( return (
<Switch> <Switch>
<Route path='/b/new' component={BoardEditor} /> <Route path='/b/new' component={BoardEditor} />
<Route path='/b/:boardId/p/new' component={PostEditor} /> <Route path='/b/:hash/:name/' component={withRouter(WithBoard(BoardPage))} />
<Route path='/b/:boardId' component={Board} />
<Route path='/' component={Boards} /> <Route path='/' component={Boards} />
</Switch> </Switch>
) )

View File

@ -2,13 +2,14 @@ import React from 'react'
import Post from './Post' import Post from './Post'
import { Segment, Button } from 'semantic-ui-react' import { Segment, Button } from 'semantic-ui-react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { shortenAddress } from '../utils/orbitdb';
export default function Board({ id, posts }) { export default function Board({ address, posts }) {
return <div> return <div>
<Segment>{id}</Segment> <Segment>{address}</Segment>
<Segment><Button as={Link} to={'p/new'}>New Post</Button></Segment> <Segment><Button as={Link} to={shortenAddress(address)+'/p/new'}>New Post</Button></Segment>
<Segment> <Segment>
<ul>{Object.keys(posts).map(i => <Post key={posts[i].multihash} {...posts[i]}/>)}</ul> <ul>{Object.keys(posts || {}).map(i => <Post key={posts[i].multihash} {...posts[i]}/>)}</ul>
</Segment> </Segment>
</div> </div>
} }

View File

@ -5,29 +5,30 @@ export default class BoardForm extends Component {
constructor(props){ constructor(props){
super(props) super(props)
this.state = { this.state = {
title: props.title || '' address: props.address || ''
} }
} }
updateTitle(event) { updateAddress(event) {
this.setState({ title: event.target.value }) const address = event.target.value
this.setState({ address })
} }
render() { render() {
const { title, content } = this.state const { address } = this.state
const { onSave, creating } = this.props const { onSave, creating } = this.props
return <Form> return <Form>
<Form.Field> <Form.Field>
<label>Title</label> <label>Address</label>
<input <input
placeholder="What's this board about?" placeholder="Paste an existing address or write your new board ID"
value={title} value={address}
onChange={this.updateTitle.bind(this)} onChange={this.updateAddress.bind(this)}
/> />
</Form.Field> </Form.Field>
<Button <Button
type='submit' type='submit'
onClick={() => onSave({ title, content })} onClick={() => onSave({ address })}
disabled={creating} disabled={creating}
>Create</Button> >Create</Button>
{creating ? 'Creating the board...' : ''} {creating ? 'Creating the board...' : ''}

View File

@ -0,0 +1,13 @@
import React from 'react'
import { Switch, Route } from 'react-router-dom'
import Board from '../containers/Board'
import PostEditor from '../containers/PostEditor'
function BoardPage({ match, address, posts, metadata }) {
return <Switch>
<Route path={match.path+'p/new'} component={PostEditor} />
<Route path={match.path} component={Board} />
</Switch>
}
export default BoardPage

View File

@ -1,11 +1,12 @@
import React from 'react' import React from 'react'
import { List } from 'semantic-ui-react' import { Button, Card } from 'semantic-ui-react'
import BoardsItem from './BoardsItem' import BoardsItem from './BoardsItem'
import { Button } from 'semantic-ui-react'
export default function Boards({ boards, createBoard }) { export default function Boards({ boards, createBoard }) {
return <List divided relaxed> return <div>
{Object.values(boards).map(board => <BoardsItem key={board.id} {...board} />)} <Card.Group>
{Object.values(boards).map(board => <BoardsItem key={board.address} {...board} />)}
</Card.Group>
<Button onClick={createBoard}>New Board</Button> <Button onClick={createBoard}>New Board</Button>
</List> </div>
} }

View File

@ -1,13 +1,23 @@
import React from 'react' import React from 'react'
import { List } from 'semantic-ui-react' import { Button, Card } from 'semantic-ui-react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { shortenAddress } from '../utils/orbitdb'
export default function BoardsItem({ id, title }) { export default function BoardsItem({ address, title }) {
return <List.Item as={Link} to={'/b/'+id+'/'}> return <Card>
<List.Icon name='comments' size='large' verticalAlign='middle' /> <Card.Content>
<List.Content> <Card.Header>
<List.Header>{title}</List.Header> { title || 'Untitled board' }
<List.Description>Experimental</List.Description> </Card.Header>
</List.Content> <Card.Meta>
</List.Item> Board
</Card.Meta>
<Card.Description style={{wordBreak:'break-all'}}>
{address}
</Card.Description>
</Card.Content>
<Card.Content extra>
<Button as={Link} to={shortenAddress(address)} basic fluid>View</Button>
</Card.Content>
</Card>
} }

View File

@ -1,28 +1,11 @@
import React, { Component } from 'react' import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { push } from 'react-router-redux'
import BoardComponent from '../components/Board' import BoardComponent from '../components/Board'
import { createBoard } from '../actions/board' import { getBoardAddress } from '../utils/orbitdb'
class Board extends Component { function Board({ location, match, boards }) {
const { hash, name } = match.params
componentDidMount() { return <BoardComponent {...boards[getBoardAddress(hash, name)]} />
const { boards, match } = this.props
if (!boards[match.params.boardId]) {
this.props.openBoard(match.params.boardId)
}
}
render() {
const { boards, match } = this.props
const id = match.params.boardId
const board = boards[id]
if (board) {
return <BoardComponent {...board} />
} else {
return <div>Opening this board...</div>
}
}
} }
function mapStateToProps(state){ function mapStateToProps(state){
@ -31,13 +14,6 @@ function mapStateToProps(state){
} }
} }
function mapDispatchToProps(dispatch){
return {
openBoard: id => dispatch(createBoard(id))
}
}
export default connect( export default connect(
mapStateToProps, mapStateToProps
mapDispatchToProps
)(Board) )(Board)

View File

@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import BoardForm from '../components/BoardForm' import BoardForm from '../components/BoardForm'
import { createBoard } from '../actions/board' import { openBoard } from '../actions/board'
function BoardEditor({ board, createBoard }) { function BoardEditor({ board, openBoard }) {
return <BoardForm board={board} onSave={createBoard} /> return <BoardForm board={board} onSave={openBoard} />
} }
function mapStateToProps(state){ function mapStateToProps(state){
@ -15,7 +15,7 @@ function mapStateToProps(state){
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
createBoard: board => dispatch(createBoard(board)) openBoard: board => dispatch(openBoard(board))
} }
} }

View File

@ -2,12 +2,13 @@ import React, { Component } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import PostForm from '../components/PostForm' import PostForm from '../components/PostForm'
import { addPost } from '../actions/post' import { addPost } from '../actions/post'
import { getBoardAddress } from '../utils/orbitdb';
class PostEditor extends Component { class PostEditor extends Component {
render() { render() {
const { post, addPost, match } = this.props const { post, addPost, match } = this.props
const { boardId } = match.params const address = getBoardAddress(match.params.hash, match.params.name)
return <PostForm post={post} onSave={p => addPost(boardId, p)} /> return <PostForm post={post} onSave={p => addPost(address, p)} />
} }
} }
@ -19,7 +20,7 @@ function mapStateToProps(state){
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
addPost: (boardId, post) => dispatch(addPost(boardId, post)) addPost: (address, post) => dispatch(addPost(address, post))
} }
} }

View File

@ -0,0 +1,53 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { openBoard } from '../actions/board'
import { getBoardAddress } from '../utils/orbitdb'
function mapStateToProps(state){
return {
boards: state.boards.boards
}
}
function mapDispatchToProps(dispatch){
return {
openBoard: address => dispatch(openBoard({ address }))
}
}
export default function WithBoard(WrappedComponent) {
class ToExport extends Component {
componentDidMount() {
const { boards, match } = this.props
const address = getBoardAddress(match.params.hash, match.params.name)
if (!boards[address]) {
this.props.openBoard(address)
}
}
componentWillReceiveProps({ match, boards }) {
const address = getBoardAddress(match.params.hash, match.params.name)
if (!boards[address]) {
this.props.openBoard(address)
}
}
render() {
const { boards, match } = this.props
const address = getBoardAddress(match.params.hash, match.params.name)
const board = boards[address]
if (board) {
return <WrappedComponent {...board} {...this.props} />
} else {
return <div>Opening this board...</div>
}
}
}
return connect(
mapStateToProps,
mapDispatchToProps
)(ToExport)
}

View File

@ -11,7 +11,7 @@ export function isValidID(id) {
return false return false
} }
export async function open(id, metadata, options = {}) { export async function open(address, metadata) {
if (!window.ipfs) { if (!window.ipfs) {
const IPFS = await import('ipfs') const IPFS = await import('ipfs')
window.ipfs = new IPFS({ window.ipfs = new IPFS({
@ -38,24 +38,18 @@ export async function open(id, metadata, options = {}) {
OrbitDB.addDatabaseType(BoardStore.type, BoardStore) OrbitDB.addDatabaseType(BoardStore.type, BoardStore)
window.orbitDb = new OrbitDB(window.ipfs) window.orbitDb = new OrbitDB(window.ipfs)
} }
const defaultOptions = { const options = {
create: id === undefined, type: BoardStore.type,
type: BoardStore.type create: true
}
let address
if (!id) {
address = 'board-v0'
} else if (!isValidID(id)) {
throw new Error('invalid address')
} }
try { try {
const db = await window.orbitDb.open(address, Object.assign(defaultOptions, options)) const db = await window.orbitDb.open(address, options)
await db.load() await db.load()
if (metadata && defaultOptions.create) { if (metadata) {
await db.updateMetadata(metadata) await db.updateMetadata(metadata)
} }
if (!window.dbs) window.dbs = {} if (!window.dbs) window.dbs = {}
window.dbs[getBoardIdFromAddress(db.address.toString())] = db window.dbs[db.address.toString()] = db
return db return db
} catch (error) { } catch (error) {
console.log(error) console.log(error)
@ -66,7 +60,7 @@ export function connectDb(db, dispatch) {
db.events.on('write', (dbname, hash, entry) => { db.events.on('write', (dbname, hash, entry) => {
dispatch({ dispatch({
type: 'ORBITDB_WRITE', type: 'ORBITDB_WRITE',
id: getBoardIdFromAddress(db.address.toString()), address: db.address.toString(),
hash, hash,
entry entry
}) })
@ -74,13 +68,13 @@ export function connectDb(db, dispatch) {
db.events.on('replicated', address => { db.events.on('replicated', address => {
dispatch({ dispatch({
type: 'ORBITDB_REPLICATED', type: 'ORBITDB_REPLICATED',
id: getBoardIdFromAddress(db.address.toString()) address: db.address.toString()
}) })
}) })
db.events.on('replicate.progress', (address, hash, entry, progress, have) => { db.events.on('replicate.progress', (address, hash, entry, progress, have) => {
dispatch({ dispatch({
type: 'ORBITDB_REPLICATE_PROGRESS', type: 'ORBITDB_REPLICATE_PROGRESS',
id: getBoardIdFromAddress(db.address.toString()), address: db.address.toString(),
hash, hash,
entry, entry,
progress, progress,
@ -90,25 +84,25 @@ export function connectDb(db, dispatch) {
db.events.on('replicate', address => { db.events.on('replicate', address => {
dispatch({ dispatch({
type: 'ORBITDB_REPLICATE', type: 'ORBITDB_REPLICATE',
id: getBoardIdFromAddress(db.address.toString()) address: db.address.toString()
}) })
}) })
db.events.on('close', address => { db.events.on('close', address => {
dispatch({ dispatch({
type: 'ORBITDB_CLOSE', type: 'ORBITDB_CLOSE',
id: getBoardIdFromAddress(db.address.toString()) address: db.address.toString()
}) })
}) })
db.events.on('load', address => { db.events.on('load', address => {
dispatch({ dispatch({
type: 'ORBITDB_LOAD', type: 'ORBITDB_LOAD',
id: getBoardIdFromAddress(db.address.toString()) address: db.address.toString()
}) })
}) })
db.events.on('load.progress', (address, hash, entry, progress, total) => { db.events.on('load.progress', (address, hash, entry, progress, total) => {
dispatch({ dispatch({
type: 'ORBITDB_LOAD_PROGRESS', type: 'ORBITDB_LOAD_PROGRESS',
id: getBoardIdFromAddress(db.address.toString()), address: db.address.toString(),
hash, hash,
entry, entry,
progress, progress,

View File

@ -1,13 +1,12 @@
import { import {
CREATE_BOARD, OPEN_BOARD,
CREATING_BOARD, OPENED_BOARD
CREATED_BOARD
} from '../actions/actionTypes' } from '../actions/actionTypes'
function getInitialState() { function getInitialState() {
return { return {
board: { board: {
title: '' name: ''
}, },
creating: false creating: false
} }
@ -15,12 +14,10 @@ function getInitialState() {
export default function BoardEditorReducer(state = getInitialState(), action) { export default function BoardEditorReducer(state = getInitialState(), action) {
switch (action.type) { switch (action.type) {
case CREATE_BOARD: case OPEN_BOARD:
return Object.assign({}, state, { board: action.board, creating: false }) return Object.assign({}, state, { board: action.board, opening: true })
case CREATING_BOARD: case OPENED_BOARD:
return Object.assign({}, state, { creating: true }) return Object.assign({}, state, { opening: false, board: action.board })
case CREATED_BOARD:
return Object.assign({}, state, { creating: false })
default: default:
return state return state
} }

View File

@ -1,4 +1,4 @@
import { CREATED_BOARD, UPDATE_BOARD } from '../actions/actionTypes' import { OPENED_BOARD, UPDATE_BOARD } from '../actions/actionTypes'
import { getBoardIdFromAddress } from '../utils/orbitdb' import { getBoardIdFromAddress } from '../utils/orbitdb'
function getInitialState() { function getInitialState() {
@ -8,22 +8,17 @@ function getInitialState() {
} }
export default function BoardsReducer(state = getInitialState(), action) { export default function BoardsReducer(state = getInitialState(), action) {
let id, newBoards let address, newBoards
switch (action.type) { switch (action.type) {
case CREATED_BOARD: case OPENED_BOARD:
id = getBoardIdFromAddress(action.board.address) address = action.board.address
const board = { newBoards = Object.assign({}, state.boards, { [address]: action.board })
id,
posts: {}
}
newBoards = Object.assign({}, state.boards, { [id]: board })
return Object.assign({}, state, { boards: newBoards }) return Object.assign({}, state, { boards: newBoards })
case UPDATE_BOARD: case UPDATE_BOARD:
id = action.boardId address = action.address
let { posts } = action let { posts, metadata } = action
console.log(state, action)
newBoards = Object.assign({}, state.boards, { newBoards = Object.assign({}, state.boards, {
[id]: Object.assign({}, state.boards[id], { posts }) [address]: Object.assign({}, state.boards[address], { posts, metadata })
}) })
return Object.assign({}, state, { boards: newBoards }) return Object.assign({}, state, { boards: newBoards })
default: default:

View File

@ -6,36 +6,32 @@ import { creatingBoard, createdBoard, boardError } from '../actions/board'
import { getBoardIdFromAddress } from '../utils/orbitdb' import { getBoardIdFromAddress } from '../utils/orbitdb'
import { UPDATE_BOARD } from '../actions/actionTypes' import { UPDATE_BOARD } from '../actions/actionTypes'
export function* updateBoard({ id }){ export function* updateBoard({ address }){
const db = window.dbs[id] const db = window.dbs[address]
yield put({ yield put({
type: UPDATE_BOARD, type: UPDATE_BOARD,
boardId: id, address,
posts: db.posts, posts: db.posts,
metadata: db.metadata metadata: db.metadata
}) })
} }
export function* createBoard({ board }) { export function* openBoard({ board }) {
yield put(creatingBoard(board))
let db let db
try { try {
db = yield call(open, board.id, { const metadata = board.title ? { title: board.title } : null
title: board.title db = yield call(open, board.address, metadata)
})
} catch (error) { } catch (error) {
yield put(boardError(error)) yield put(boardError(error))
} }
if (db) { if (db) {
const address = db.address.toString() const address = db.address.toString()
const dbInfo = { const dbInfo = { address }
id: getBoardIdFromAddress(address), dbInfo.posts = db.posts
address dbInfo.metadata = db.metadata
}
const channel = yield call(createDbEventChannel, db) const channel = yield call(createDbEventChannel, db)
yield fork(watchDb, channel) yield fork(watchDb, channel)
yield put(createdBoard(Object.assign({}, board, dbInfo))) yield put(createdBoard(Object.assign({}, board, dbInfo)))
yield put(push('/b/' + dbInfo.id + '/'))
} }
} }

View File

@ -1,10 +1,10 @@
import { takeEvery } from 'redux-saga/effects' import { takeEvery } from 'redux-saga/effects'
import { CREATE_BOARD, ADD_POST, ORBITDB_REPLICATED, ORBITDB_WRITE } from '../actions/actionTypes' import { OPEN_BOARD, ADD_POST, ORBITDB_REPLICATED, ORBITDB_WRITE } from '../actions/actionTypes'
import { createBoard, updateBoard } from './boards' import { openBoard, updateBoard } from './boards'
import { addPost } from './posts' import { addPost } from './posts'
export default function* saga(){ export default function* saga(){
yield takeEvery(CREATE_BOARD, createBoard) yield takeEvery(OPEN_BOARD, openBoard)
yield takeEvery(ADD_POST, addPost) yield takeEvery(ADD_POST, addPost)
yield takeEvery(ORBITDB_WRITE, updateBoard) yield takeEvery(ORBITDB_WRITE, updateBoard)
yield takeEvery(ORBITDB_REPLICATED, updateBoard) yield takeEvery(ORBITDB_REPLICATED, updateBoard)

View File

@ -1,8 +1,8 @@
import { put, apply, call } from 'redux-saga/effects' import { apply, call } from 'redux-saga/effects'
import { ipfsPut } from '../utils/ipfs' import { ipfsPut } from '../utils/ipfs'
export function* addPost({ boardId, post }) { export function* addPost({ address, post }) {
const db = window.dbs[boardId] const db = window.dbs[address]
const { title, content } = post const { title, content } = post
const multihash = yield call(ipfsPut, content) const multihash = yield call(ipfsPut, content)
yield apply(db, db.addPost, [title, multihash]) yield apply(db, db.addPost, [title, multihash])

View File

@ -1,6 +1,8 @@
export function getBoardIdFromAddress(address) { export function getBoardAddress(hash, name) {
const match = /\/orbitdb\/(.+)\//.exec(address) return '/orbitdb/' + hash + '/' + name
if (match[1]) return match[1] }
return undefined
export function shortenAddress(address) {
return address.replace(/^\/orbitdb/, '/b')
} }