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:
parent
81a553f5a2
commit
65678a1521
@ -5,6 +5,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"ipfs": "^0.27.7",
|
||||
"moment": "^2.20.1",
|
||||
"orbit-db": "^0.19.4",
|
||||
"orbit-db-discussion-board": "https://github.com/fazo96/orbit-db-discussion-board.git",
|
||||
"react": "^16.2.0",
|
||||
|
@ -1,12 +1,23 @@
|
||||
import React from 'react'
|
||||
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 { 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 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}>
|
||||
<Grid.Column width={8}>
|
||||
<Header size='large' style={{marginTop:'.5em'}}>
|
||||
@ -18,19 +29,41 @@ export default function Board({ address, posts, metadata, replicating }) {
|
||||
<List.Item>
|
||||
<List.Icon name='linkify' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Board Address</List.Header>
|
||||
<List.Header>Address</List.Header>
|
||||
<List.Content>
|
||||
<a href={url}>{address}</a>
|
||||
{address}
|
||||
</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='users' size="large" verticalAlign="middle"/>
|
||||
<List.Icon name='disk outline' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Users</List.Header>
|
||||
<List.Content>?</List.Content>
|
||||
<List.Header>Size</List.Header>
|
||||
<List.Content>{stats.opLogLength || 0} Entries</List.Content>
|
||||
</List.Content>
|
||||
</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.Icon name='file text outline' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
@ -38,13 +71,6 @@ export default function Board({ address, posts, metadata, replicating }) {
|
||||
<List.Content>{Object.values(posts || {}).length}</List.Content>
|
||||
</List.Content>
|
||||
</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.Icon name='globe' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
@ -61,13 +87,13 @@ export default function Board({ address, posts, metadata, replicating }) {
|
||||
</List.Item>
|
||||
</List>
|
||||
<div className='ui three buttons basic'>
|
||||
<Button>
|
||||
<Button as={Link} to={'/'}>
|
||||
<Icon name='left arrow'/> Boards
|
||||
</Button>
|
||||
<Button>
|
||||
<Button disabled={!writeable}>
|
||||
<Icon name='pencil'/> Edit
|
||||
</Button>
|
||||
<Button as={Link} to={shortenAddress(address)+'/p/new'}>
|
||||
<Button disabled={!writeable} as={Link} to={shortenAddress(address)+'/p/new'}>
|
||||
<Icon name='plus'/> New Post
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -1,7 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import { Container, Card, Form, Button } from 'semantic-ui-react'
|
||||
import { shortenAddress } from '../utils/orbitdb'
|
||||
|
||||
export default class BoardForm extends Component {
|
||||
constructor(props){
|
||||
|
@ -2,11 +2,12 @@ import React from 'react'
|
||||
import { Switch, Route } from 'react-router-dom'
|
||||
import Board from '../containers/Board'
|
||||
import PostEditor from '../containers/PostEditor'
|
||||
import WithStats from '../containers/WithStats'
|
||||
|
||||
function BoardPage({ match, address, posts, metadata }) {
|
||||
return <Switch>
|
||||
<Route path={match.path+'p/new'} component={PostEditor} />
|
||||
<Route path={match.path} component={Board} />
|
||||
<Route path={match.path} component={WithStats(Board)} />
|
||||
</Switch>
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { List, Icon, Segment, Divider, Grid, Header, Button, Card } from 'semant
|
||||
import { Link } from 'react-router-dom'
|
||||
import BoardsItem from './BoardsItem'
|
||||
|
||||
export default function Boards({ boards, createBoard }) {
|
||||
export default function Boards({ stats, boards, createBoard }) {
|
||||
return <Grid container divided colums={2}>
|
||||
<Grid.Column width={8}>
|
||||
<Header size='large' style={{marginTop:'.5em'}}>
|
||||
@ -13,43 +13,43 @@ export default function Boards({ boards, createBoard }) {
|
||||
<Divider />
|
||||
<List relaxed>
|
||||
<List.Item>
|
||||
<List.Icon name='file text outline' size="large" verticalAlign="middle"/>
|
||||
<List.Icon name='leaf' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Boards</List.Header>
|
||||
<List.Content>{Object.keys(boards).length}</List.Content>
|
||||
<List.Header>Seeding</List.Header>
|
||||
<List.Content>{Object.keys(boards).length} Boards</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='wifi' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Connected Peers</List.Header>
|
||||
<List.Content>?</List.Content>
|
||||
<List.Content>{stats.peers.length}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='disk outline' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>Used Space</List.Header>
|
||||
<List.Content>?</List.Content>
|
||||
<List.Content>Not Supported Yet</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='user' size="large" verticalAlign="middle"/>
|
||||
<List.Icon name='user circle' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>IPFS ID</List.Header>
|
||||
<List.Content>?</List.Content>
|
||||
<List.Content>{stats.id}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<List.Icon name='key' size="large" verticalAlign="middle"/>
|
||||
<List.Content>
|
||||
<List.Header>OrbitDB Public Key</List.Header>
|
||||
<List.Content>?</List.Content>
|
||||
<List.Content style={{wordBreak:'break-all'}}>{stats.pubKey}</List.Content>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
</List>
|
||||
<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
|
||||
</Button>
|
||||
<Button as={Link} to={'/b/new'}>
|
||||
|
@ -1,7 +1,7 @@
|
||||
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>
|
||||
<Card.Content>
|
||||
<Card.Header>
|
||||
@ -10,10 +10,31 @@ export default function Post({ title, multihash}) {
|
||||
<Card.Meta>Post</Card.Meta>
|
||||
</Card.Content>
|
||||
<Card.Content style={{wordBreak:'break-all'}}>
|
||||
<Icon name="chain"/> <a href={'//ipfs.io/ipfs/'+multihash}>View</a>
|
||||
</Card.Content>
|
||||
<Card.Content extra>
|
||||
<Icon name="comments"/> Comments not supported yet
|
||||
<List>
|
||||
<List.Item>
|
||||
<List.Icon name="key" verticalAlign="middle"/>
|
||||
<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>
|
||||
}
|
@ -3,9 +3,11 @@ import { connect } from 'react-redux'
|
||||
import BoardComponent from '../components/Board'
|
||||
import { getBoardAddress } from '../utils/orbitdb'
|
||||
|
||||
function Board({ location, match, boards }) {
|
||||
function Board({ stats, location, match, boards }) {
|
||||
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){
|
||||
|
@ -2,9 +2,12 @@ import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { push } from 'react-router-redux'
|
||||
import BoardsComponent from '../components/Boards'
|
||||
import WithStats from './WithStats'
|
||||
|
||||
const WrappedComponent = WithStats(BoardsComponent)
|
||||
|
||||
function Boards({ boards, createBoard }) {
|
||||
return <BoardsComponent boards={boards} createBoard={createBoard} />
|
||||
return <WrappedComponent boards={boards} createBoard={createBoard} />
|
||||
}
|
||||
|
||||
function mapStateToProps(state){
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Loader, Dimmer } from 'semantic-ui-react'
|
||||
import { Dimmer } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { openBoard } from '../actions/board'
|
||||
import { getBoardAddress } from '../utils/orbitdb'
|
||||
|
44
src/containers/WithStats.js
Normal file
44
src/containers/WithStats.js
Normal 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} />
|
||||
}
|
||||
}
|
||||
}
|
10
src/index.js
10
src/index.js
@ -7,6 +7,7 @@ import App from './components/App'
|
||||
import registerServiceWorker from './registerServiceWorker'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ConnectedRouter } from 'react-router-redux'
|
||||
import { start } from './orbitdb'
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
@ -23,7 +24,7 @@ render(
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept('./components/App', () => {
|
||||
const NewApp = require('./components/App').default;
|
||||
const NewApp = require('./components/App').default
|
||||
render(
|
||||
<AppContainer>
|
||||
<Provider store={store}>
|
||||
@ -33,8 +34,9 @@ if (module.hot) {
|
||||
</Provider>
|
||||
</AppContainer>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
registerServiceWorker();
|
||||
registerServiceWorker()
|
||||
start(store.dispatch)
|
@ -2,7 +2,6 @@ import IPFS from 'ipfs'
|
||||
import OrbitDB from 'orbit-db'
|
||||
import BoardStore from 'orbit-db-discussion-board'
|
||||
import multihashes from 'multihashes'
|
||||
import { getBoardIdFromAddress } from '../utils/orbitdb'
|
||||
|
||||
export function isValidID(id) {
|
||||
try {
|
||||
@ -13,7 +12,7 @@ export function isValidID(id) {
|
||||
return false
|
||||
}
|
||||
|
||||
export async function open(address, metadata) {
|
||||
export async function start() {
|
||||
if (!window.ipfs) {
|
||||
window.ipfs = new IPFS({
|
||||
repo: 'ipfs-v6-boards-v0',
|
||||
@ -38,23 +37,23 @@ export async function open(address, metadata) {
|
||||
OrbitDB.addDatabaseType(BoardStore.type, BoardStore)
|
||||
window.orbitDb = new OrbitDB(window.ipfs)
|
||||
}
|
||||
}
|
||||
|
||||
export async function open(address, metadata) {
|
||||
await start()
|
||||
const options = {
|
||||
type: BoardStore.type,
|
||||
create: true,
|
||||
write: ['*']
|
||||
}
|
||||
try {
|
||||
const db = await window.orbitDb.open(address, options)
|
||||
await db.load()
|
||||
if (metadata) {
|
||||
await db.updateMetadata(metadata)
|
||||
}
|
||||
if (!window.dbs) window.dbs = {}
|
||||
window.dbs[db.address.toString()] = db
|
||||
return db
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
const db = await window.orbitDb.open(address, options)
|
||||
await db.load()
|
||||
if (metadata) {
|
||||
await db.updateMetadata(metadata)
|
||||
}
|
||||
if (!window.dbs) window.dbs = {}
|
||||
window.dbs[db.address.toString()] = db
|
||||
return db
|
||||
}
|
||||
|
||||
export function connectDb(db, dispatch) {
|
||||
@ -83,10 +82,7 @@ export function connectDb(db, dispatch) {
|
||||
progress,
|
||||
have,
|
||||
time: Date.now(),
|
||||
replicationInfo: {
|
||||
progress: db._replicationInfo.progress,
|
||||
max: db._replicationInfo.max
|
||||
}
|
||||
replicationInfo: Object.assign({}, db._replicationInfo)
|
||||
})
|
||||
})
|
||||
db.events.on('replicate', address => {
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
ORBITDB_REPLICATED,
|
||||
ORBITDB_REPLICATE
|
||||
} from '../actions/actionTypes'
|
||||
import { getBoardIdFromAddress } from '../utils/orbitdb'
|
||||
|
||||
function getInitialState() {
|
||||
return {
|
||||
@ -43,7 +42,8 @@ export default function BoardsReducer(state = getInitialState(), action) {
|
||||
case ORBITDB_REPLICATED:
|
||||
address = action.address
|
||||
return Object.assign({}, state, { boards: updateBoard(state.boards, address, {
|
||||
replicating: false
|
||||
replicating: false,
|
||||
lastReplicated: action.time
|
||||
})})
|
||||
default:
|
||||
return state;
|
||||
|
@ -2,8 +2,8 @@ import { put, call, fork, take } from 'redux-saga/effects'
|
||||
import { eventChannel } from 'redux-saga'
|
||||
import { push } from 'react-router-redux'
|
||||
import { open, connectDb } from '../orbitdb'
|
||||
import { creatingBoard, createdBoard, boardError } from '../actions/board'
|
||||
import { getBoardIdFromAddress, shortenAddress } from '../utils/orbitdb'
|
||||
import { createdBoard, boardError } from '../actions/board'
|
||||
import { shortenAddress } from '../utils/orbitdb'
|
||||
import { UPDATE_BOARD } from '../actions/actionTypes'
|
||||
|
||||
export function* goToBoard({ board }){
|
||||
@ -35,9 +35,13 @@ export function* openBoard({ board }) {
|
||||
const dbInfo = { address }
|
||||
dbInfo.posts = db.posts
|
||||
dbInfo.metadata = db.metadata
|
||||
const channel = yield call(createDbEventChannel, db)
|
||||
yield fork(watchDb, channel)
|
||||
yield put(createdBoard(Object.assign({}, board, dbInfo)))
|
||||
try {
|
||||
const channel = yield call(createDbEventChannel, db)
|
||||
yield fork(watchDb, channel)
|
||||
yield put(createdBoard(Object.assign({}, board, dbInfo)))
|
||||
} catch (error) {
|
||||
yield put({ type: 'ERROR', error })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,4 +5,45 @@ export async function ipfsPut(content) {
|
||||
}
|
||||
const response = await window.ipfs.files.add(obj)
|
||||
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
|
||||
}
|
@ -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:
|
||||
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:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/moving-average/-/moving-average-1.0.0.tgz#b1247ba8dd2d7927c619f1eac8036cf933d65adc"
|
||||
|
Loading…
Reference in New Issue
Block a user