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

huge usability improvements

This commit is contained in:
Enrico Fasoli 2018-02-08 23:52:06 +01:00
parent 81a553f5a2
commit 65678a1521
No known key found for this signature in database
GPG Key ID: 1238873C5F27DB4D
16 changed files with 212 additions and 69 deletions

View File

@ -5,6 +5,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"ipfs": "^0.27.7", "ipfs": "^0.27.7",
"moment": "^2.20.1",
"orbit-db": "^0.19.4", "orbit-db": "^0.19.4",
"orbit-db-discussion-board": "https://github.com/fazo96/orbit-db-discussion-board.git", "orbit-db-discussion-board": "https://github.com/fazo96/orbit-db-discussion-board.git",
"react": "^16.2.0", "react": "^16.2.0",

View File

@ -1,12 +1,23 @@
import React from 'react' import React from 'react'
import Post from './Post' import Post from './Post'
import { Divider, Icon, Grid, Segment, Header, List, Button, Card } from 'semantic-ui-react' import { Divider, Icon, Grid, Header, List, Button, Card } from 'semantic-ui-react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { shortenAddress } from '../utils/orbitdb'; import { shortenAddress } from '../utils/orbitdb';
import moment from 'moment'
export default function Board({ address, posts, metadata, replicating }) { export default function Board({ address, posts, metadata, replicating, stats, replicationInfo, lastReplicated }) {
const { email, website, title } = metadata || {} const { email, website, title } = metadata || {}
const url = window.location.href const peerCount = (stats.peers || []).length
const online = peerCount > 0
const writeable = stats.access ? (stats.access.writeable ? 'Yes' : 'No') : '?'
let replicationMessage = lastReplicated ? ('Last Activity at ' + moment(lastReplicated).format('H:mm')) : 'No Activity'
if (replicating) {
if (replicationInfo && replicationInfo.max !== undefined) {
replicationMessage = 'Progress: ' + (replicationInfo.progress || 0) + '/' + replicationInfo.max
} else {
replicationMessage = 'Initializing Transfer'
}
}
return <Grid container divided colums={2}> return <Grid container divided colums={2}>
<Grid.Column width={8}> <Grid.Column width={8}>
<Header size='large' style={{marginTop:'.5em'}}> <Header size='large' style={{marginTop:'.5em'}}>
@ -18,19 +29,41 @@ export default function Board({ address, posts, metadata, replicating }) {
<List.Item> <List.Item>
<List.Icon name='linkify' size="large" verticalAlign="middle"/> <List.Icon name='linkify' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
<List.Header>Board Address</List.Header> <List.Header>Address</List.Header>
<List.Content> <List.Content>
<a href={url}>{address}</a> {address}
</List.Content> </List.Content>
</List.Content> </List.Content>
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Icon name='users' size="large" verticalAlign="middle"/> <List.Icon name='disk outline' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
<List.Header>Users</List.Header> <List.Header>Size</List.Header>
<List.Content>?</List.Content> <List.Content>{stats.opLogLength || 0} Entries</List.Content>
</List.Content> </List.Content>
</List.Item> </List.Item>
<List.Item>
<List.Icon name={online ? 'heart' : 'heartbeat'} size="large" verticalAlign="middle"/>
<List.Content>
<List.Header>{online ? 'Online' : 'Offline'}</List.Header>
<List.Content>{online ? peerCount + ' Connections' : 'No Connections'}</List.Content>
</List.Content>
</List.Item>
<List.Item>
<List.Icon color={replicating ? 'green' : null} name='feed' size="large" verticalAlign="middle"/>
<List.Content>
<List.Header>{replicating ? 'Downloading' : 'Download'}</List.Header>
<List.Content>{replicationMessage}</List.Content>
</List.Content>
</List.Item>
<List.Item>
<List.Icon name='edit' size="large" verticalAlign="middle"/>
<List.Content>
<List.Header>Write Access</List.Header>
<List.Content>{writeable}</List.Content>
</List.Content>
</List.Item>
<Divider/>
<List.Item> <List.Item>
<List.Icon name='file text outline' size="large" verticalAlign="middle"/> <List.Icon name='file text outline' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
@ -38,13 +71,6 @@ export default function Board({ address, posts, metadata, replicating }) {
<List.Content>{Object.values(posts || {}).length}</List.Content> <List.Content>{Object.values(posts || {}).length}</List.Content>
</List.Content> </List.Content>
</List.Item> </List.Item>
<List.Item>
<List.Icon color={replicating ? 'green' : null} name='wifi' size="large" verticalAlign="middle"/>
<List.Content>
<List.Header>Replication</List.Header>
<List.Content>{replicating ? 'Receiving Content...' : 'Idle'}</List.Content>
</List.Content>
</List.Item>
<List.Item> <List.Item>
<List.Icon name='globe' size="large" verticalAlign="middle"/> <List.Icon name='globe' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
@ -61,13 +87,13 @@ export default function Board({ address, posts, metadata, replicating }) {
</List.Item> </List.Item>
</List> </List>
<div className='ui three buttons basic'> <div className='ui three buttons basic'>
<Button> <Button as={Link} to={'/'}>
<Icon name='left arrow'/> Boards <Icon name='left arrow'/> Boards
</Button> </Button>
<Button> <Button disabled={!writeable}>
<Icon name='pencil'/> Edit <Icon name='pencil'/> Edit
</Button> </Button>
<Button as={Link} to={shortenAddress(address)+'/p/new'}> <Button disabled={!writeable} as={Link} to={shortenAddress(address)+'/p/new'}>
<Icon name='plus'/> New Post <Icon name='plus'/> New Post
</Button> </Button>
</div> </div>

View File

@ -1,7 +1,5 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Redirect } from 'react-router-dom'
import { Container, Card, Form, Button } from 'semantic-ui-react' import { Container, Card, Form, Button } from 'semantic-ui-react'
import { shortenAddress } from '../utils/orbitdb'
export default class BoardForm extends Component { export default class BoardForm extends Component {
constructor(props){ constructor(props){

View File

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

View File

@ -3,7 +3,7 @@ import { List, Icon, Segment, Divider, Grid, Header, Button, Card } from 'semant
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import BoardsItem from './BoardsItem' import BoardsItem from './BoardsItem'
export default function Boards({ boards, createBoard }) { export default function Boards({ stats, boards, createBoard }) {
return <Grid container divided colums={2}> return <Grid container divided colums={2}>
<Grid.Column width={8}> <Grid.Column width={8}>
<Header size='large' style={{marginTop:'.5em'}}> <Header size='large' style={{marginTop:'.5em'}}>
@ -13,43 +13,43 @@ export default function Boards({ boards, createBoard }) {
<Divider /> <Divider />
<List relaxed> <List relaxed>
<List.Item> <List.Item>
<List.Icon name='file text outline' size="large" verticalAlign="middle"/> <List.Icon name='leaf' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
<List.Header>Boards</List.Header> <List.Header>Seeding</List.Header>
<List.Content>{Object.keys(boards).length}</List.Content> <List.Content>{Object.keys(boards).length} Boards</List.Content>
</List.Content> </List.Content>
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Icon name='wifi' size="large" verticalAlign="middle"/> <List.Icon name='wifi' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
<List.Header>Connected Peers</List.Header> <List.Header>Connected Peers</List.Header>
<List.Content>?</List.Content> <List.Content>{stats.peers.length}</List.Content>
</List.Content> </List.Content>
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Icon name='disk outline' size="large" verticalAlign="middle"/> <List.Icon name='disk outline' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
<List.Header>Used Space</List.Header> <List.Header>Used Space</List.Header>
<List.Content>?</List.Content> <List.Content>Not Supported Yet</List.Content>
</List.Content> </List.Content>
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Icon name='user' size="large" verticalAlign="middle"/> <List.Icon name='user circle' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
<List.Header>IPFS ID</List.Header> <List.Header>IPFS ID</List.Header>
<List.Content>?</List.Content> <List.Content>{stats.id}</List.Content>
</List.Content> </List.Content>
</List.Item> </List.Item>
<List.Item> <List.Item>
<List.Icon name='key' size="large" verticalAlign="middle"/> <List.Icon name='key' size="large" verticalAlign="middle"/>
<List.Content> <List.Content>
<List.Header>OrbitDB Public Key</List.Header> <List.Header>OrbitDB Public Key</List.Header>
<List.Content>?</List.Content> <List.Content style={{wordBreak:'break-all'}}>{stats.pubKey}</List.Content>
</List.Content> </List.Content>
</List.Item> </List.Item>
</List> </List>
<div className="ui two buttons"> <div className="ui two buttons">
<Button as='a' href="https://github.com/fazo96/ipfs-boards"> <Button as='a' href="https://github.com/fazo96/ipfs-boards" target="__blank" >
<Icon name="github"/> GitHub <Icon name="github"/> GitHub
</Button> </Button>
<Button as={Link} to={'/b/new'}> <Button as={Link} to={'/b/new'}>

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { Card, Icon } from 'semantic-ui-react' import { List, Card } from 'semantic-ui-react'
export default function Post({ title, multihash}) { export default function Post({ title, multihash, pubKey }) {
return <Card fluid> return <Card fluid>
<Card.Content> <Card.Content>
<Card.Header> <Card.Header>
@ -10,10 +10,31 @@ export default function Post({ title, multihash}) {
<Card.Meta>Post</Card.Meta> <Card.Meta>Post</Card.Meta>
</Card.Content> </Card.Content>
<Card.Content style={{wordBreak:'break-all'}}> <Card.Content style={{wordBreak:'break-all'}}>
<Icon name="chain"/> <a href={'//ipfs.io/ipfs/'+multihash}>View</a> <List>
</Card.Content> <List.Item>
<Card.Content extra> <List.Icon name="key" verticalAlign="middle"/>
<Icon name="comments"/> Comments not supported yet <List.Content>
<List.Header>Signed By</List.Header>
<List.Content>{pubKey || 'Unknown'}</List.Content>
</List.Content>
</List.Item>
<List.Item>
<List.Icon name="comments" verticalAlign="middle"/>
<List.Content>
<List.Header>Comments</List.Header>
<List.Content>Not Supported Yet</List.Content>
</List.Content>
</List.Item>
<List.Item>
<List.Icon name="chain" verticalAlign="middle"/>
<List.Content>
<List.Header>Content</List.Header>
<List.Content>
<a href={'//ipfs.io/ipfs/'+multihash}>{multihash}</a>
</List.Content>
</List.Content>
</List.Item>
</List>
</Card.Content> </Card.Content>
</Card> </Card>
} }

View File

@ -3,9 +3,11 @@ import { connect } from 'react-redux'
import BoardComponent from '../components/Board' import BoardComponent from '../components/Board'
import { getBoardAddress } from '../utils/orbitdb' import { getBoardAddress } from '../utils/orbitdb'
function Board({ location, match, boards }) { function Board({ stats, location, match, boards }) {
const { hash, name } = match.params const { hash, name } = match.params
return <BoardComponent {...boards[getBoardAddress(hash, name)]} /> const address = getBoardAddress(hash, name)
const boardStats = stats.dbs[address] || {}
return <BoardComponent stats={boardStats} {...boards[address]} />
} }
function mapStateToProps(state){ function mapStateToProps(state){

View File

@ -2,9 +2,12 @@ import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { push } from 'react-router-redux' import { push } from 'react-router-redux'
import BoardsComponent from '../components/Boards' import BoardsComponent from '../components/Boards'
import WithStats from './WithStats'
const WrappedComponent = WithStats(BoardsComponent)
function Boards({ boards, createBoard }) { function Boards({ boards, createBoard }) {
return <BoardsComponent boards={boards} createBoard={createBoard} /> return <WrappedComponent boards={boards} createBoard={createBoard} />
} }
function mapStateToProps(state){ function mapStateToProps(state){

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import { Loader, Dimmer } from 'semantic-ui-react' import { Dimmer } from 'semantic-ui-react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { openBoard } from '../actions/board' import { openBoard } from '../actions/board'
import { getBoardAddress } from '../utils/orbitdb' import { getBoardAddress } from '../utils/orbitdb'

View File

@ -0,0 +1,44 @@
import React, { Component } from 'react'
import { getStats } from '../utils/ipfs'
export default function(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props)
this.state = {
stats: {
id: '?',
peers: [],
pubKey: '?',
dbs: {}
},
interval: null
}
}
async refresh(loop = true) {
const newStats = await getStats()
const stats = Object.assign({}, this.state.stats, newStats)
this.setState({ stats }, loop ? this.refreshDelayed.bind(this) : undefined)
}
refreshDelayed() {
const timeout = setTimeout(() => {
this.refresh()
}, 2000)
this.setState({ timeout })
}
componentDidMount() {
this.refresh()
}
componentWillUnmount() {
if (this.state.interval) clearInterval(this.state.interval)
}
render() {
return <WrappedComponent stats={this.state.stats} {...this.props} />
}
}
}

View File

@ -7,6 +7,7 @@ import App from './components/App'
import registerServiceWorker from './registerServiceWorker' import registerServiceWorker from './registerServiceWorker'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { ConnectedRouter } from 'react-router-redux' import { ConnectedRouter } from 'react-router-redux'
import { start } from './orbitdb'
const store = configureStore(); const store = configureStore();
@ -23,7 +24,7 @@ render(
if (module.hot) { if (module.hot) {
module.hot.accept('./components/App', () => { module.hot.accept('./components/App', () => {
const NewApp = require('./components/App').default; const NewApp = require('./components/App').default
render( render(
<AppContainer> <AppContainer>
<Provider store={store}> <Provider store={store}>
@ -33,8 +34,9 @@ if (module.hot) {
</Provider> </Provider>
</AppContainer>, </AppContainer>,
document.getElementById('root') document.getElementById('root')
); )
}); })
} }
registerServiceWorker(); registerServiceWorker()
start(store.dispatch)

View File

@ -2,7 +2,6 @@ import IPFS from 'ipfs'
import OrbitDB from 'orbit-db' import OrbitDB from 'orbit-db'
import BoardStore from 'orbit-db-discussion-board' import BoardStore from 'orbit-db-discussion-board'
import multihashes from 'multihashes' import multihashes from 'multihashes'
import { getBoardIdFromAddress } from '../utils/orbitdb'
export function isValidID(id) { export function isValidID(id) {
try { try {
@ -13,7 +12,7 @@ export function isValidID(id) {
return false return false
} }
export async function open(address, metadata) { export async function start() {
if (!window.ipfs) { if (!window.ipfs) {
window.ipfs = new IPFS({ window.ipfs = new IPFS({
repo: 'ipfs-v6-boards-v0', repo: 'ipfs-v6-boards-v0',
@ -38,23 +37,23 @@ export async function open(address, metadata) {
OrbitDB.addDatabaseType(BoardStore.type, BoardStore) OrbitDB.addDatabaseType(BoardStore.type, BoardStore)
window.orbitDb = new OrbitDB(window.ipfs) window.orbitDb = new OrbitDB(window.ipfs)
} }
}
export async function open(address, metadata) {
await start()
const options = { const options = {
type: BoardStore.type, type: BoardStore.type,
create: true, create: true,
write: ['*'] write: ['*']
} }
try { const db = await window.orbitDb.open(address, options)
const db = await window.orbitDb.open(address, options) await db.load()
await db.load() if (metadata) {
if (metadata) { await db.updateMetadata(metadata)
await db.updateMetadata(metadata)
}
if (!window.dbs) window.dbs = {}
window.dbs[db.address.toString()] = db
return db
} catch (error) {
console.log(error)
} }
if (!window.dbs) window.dbs = {}
window.dbs[db.address.toString()] = db
return db
} }
export function connectDb(db, dispatch) { export function connectDb(db, dispatch) {
@ -83,10 +82,7 @@ export function connectDb(db, dispatch) {
progress, progress,
have, have,
time: Date.now(), time: Date.now(),
replicationInfo: { replicationInfo: Object.assign({}, db._replicationInfo)
progress: db._replicationInfo.progress,
max: db._replicationInfo.max
}
}) })
}) })
db.events.on('replicate', address => { db.events.on('replicate', address => {

View File

@ -5,7 +5,6 @@ import {
ORBITDB_REPLICATED, ORBITDB_REPLICATED,
ORBITDB_REPLICATE ORBITDB_REPLICATE
} from '../actions/actionTypes' } from '../actions/actionTypes'
import { getBoardIdFromAddress } from '../utils/orbitdb'
function getInitialState() { function getInitialState() {
return { return {
@ -43,7 +42,8 @@ export default function BoardsReducer(state = getInitialState(), action) {
case ORBITDB_REPLICATED: case ORBITDB_REPLICATED:
address = action.address address = action.address
return Object.assign({}, state, { boards: updateBoard(state.boards, address, { return Object.assign({}, state, { boards: updateBoard(state.boards, address, {
replicating: false replicating: false,
lastReplicated: action.time
})}) })})
default: default:
return state; return state;

View File

@ -2,8 +2,8 @@ import { put, call, fork, take } from 'redux-saga/effects'
import { eventChannel } from 'redux-saga' import { eventChannel } from 'redux-saga'
import { push } from 'react-router-redux' import { push } from 'react-router-redux'
import { open, connectDb } from '../orbitdb' import { open, connectDb } from '../orbitdb'
import { creatingBoard, createdBoard, boardError } from '../actions/board' import { createdBoard, boardError } from '../actions/board'
import { getBoardIdFromAddress, shortenAddress } from '../utils/orbitdb' import { shortenAddress } from '../utils/orbitdb'
import { UPDATE_BOARD } from '../actions/actionTypes' import { UPDATE_BOARD } from '../actions/actionTypes'
export function* goToBoard({ board }){ export function* goToBoard({ board }){
@ -35,9 +35,13 @@ export function* openBoard({ board }) {
const dbInfo = { address } const dbInfo = { address }
dbInfo.posts = db.posts dbInfo.posts = db.posts
dbInfo.metadata = db.metadata dbInfo.metadata = db.metadata
const channel = yield call(createDbEventChannel, db) try {
yield fork(watchDb, channel) const channel = yield call(createDbEventChannel, db)
yield put(createdBoard(Object.assign({}, board, dbInfo))) yield fork(watchDb, channel)
yield put(createdBoard(Object.assign({}, board, dbInfo)))
} catch (error) {
yield put({ type: 'ERROR', error })
}
} }
} }

View File

@ -5,4 +5,45 @@ export async function ipfsPut(content) {
} }
const response = await window.ipfs.files.add(obj) const response = await window.ipfs.files.add(obj)
return response[0].hash return response[0].hash
}
export async function readText(multihash) {
const buffer = await window.ipfs.object.get(multihash)
return buffer.toString('utf-8')
}
export async function getStats() {
const ipfs = window.ipfs;
const orbitDb = window.orbitDb
const dbs = {}
const stats = {}
if (ipfs) {
const peers = await ipfs.swarm.peers()
const id = await ipfs.id()
stats.peers = peers.map(p => p.peer.id._idB58String)
stats.id = id.id
}
if (orbitDb) {
stats.pubKey = await orbitDb.key.getPublic('hex')
Object.values(window.dbs || {}).forEach(db => {
let writeable = db.access.write.indexOf('*') >= 0 || db.access.write.indexOf(stats.pubKey) >= 0
const dbInfo = {
opLogLength: db._oplog.length,
access: {
admin: db.access.admin,
read: db.access.read,
write: db.access.write,
writeable
},
peers: []
}
const subscription = orbitDb._pubsub._subscriptions
if (subscription && subscription.room) {
dbInfo.peers = subscription.room._peers
}
dbs[db.address] = dbInfo
})
}
stats.dbs = dbs
return stats
} }

View File

@ -5862,6 +5862,10 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdi
dependencies: dependencies:
minimist "0.0.8" minimist "0.0.8"
moment@^2.20.1:
version "2.20.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
moving-average@^1.0.0: moving-average@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/moving-average/-/moving-average-1.0.0.tgz#b1247ba8dd2d7927c619f1eac8036cf933d65adc" resolved "https://registry.yarnpkg.com/moving-average/-/moving-average-1.0.0.tgz#b1247ba8dd2d7927c619f1eac8036cf933d65adc"