1
0
mirror of https://github.com/fazo96/ipfs-boards synced 2025-01-24 14:44:19 +01:00

implement commenting and posting

This commit is contained in:
Enrico Fasoli 2019-11-13 01:02:56 +01:00
parent 2670ab2298
commit 1711807308
No known key found for this signature in database
GPG Key ID: 1238873C5F27DB4D
15 changed files with 1112 additions and 427 deletions

43
components/AddComment.js Normal file
View File

@ -0,0 +1,43 @@
import React from 'react'
import { TextField, InputAdornment, IconButton } from '@material-ui/core'
import SendIcon from '@material-ui/icons/Send'
import { openBoard } from './system'
class AddComment extends React.PureComponent {
state = { text: '' }
render() {
const { text } = this.state
return (
<TextField
variant="outlined"
label="Comment"
placeholder="What's on your mind?"
value={text}
onChange={event => this.setState({ text: event.target.value })}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={this.submit} disabled={!text}>
<SendIcon />
</IconButton>
</InputAdornment>
)
}}
/>
)
}
submit = async () => {
const { boardId, postId, parentId, afterSend } = this.props
const { text } = this.state
const board = await openBoard(boardId)
const comment = { text }
await board.commentPost(postId, comment, parentId)
if (afterSend) afterSend()
}
}
export default AddComment

View File

@ -37,7 +37,7 @@ export default function Board({ boardId, posts }){
<CardHeader
avatar={<Avatar><EmptyIcon /></Avatar>}
title="No Posts Yet"
subheader="Why don't you break the ice?"
subheader="Don't panic. Your device will keep looking for new posts in the network."
/>
</Card>}
<Button

82
components/Comments.js Normal file
View File

@ -0,0 +1,82 @@
import React from 'react'
import { List, ListItem, ListItemText, ListItemSecondaryAction, IconButton, Button } from '@material-ui/core'
import AddComment from './AddComment'
import CommentIcon from '@material-ui/icons/Comment'
import { openBoard } from './system'
class Comments extends React.PureComponent {
state = {
comments: [],
replying: false
}
componentDidMount() {
this.refreshComments()
}
componentDidUpdate(prevProps) {
const { boardId, postId, parentId } = this.props
if (prevProps.boardId !== boardId || prevProps.postId !== postId || prevProps.parentId !== parentId) {
this.refreshComments()
}
}
async refreshComments() {
const { boardId, postId, parentId } = this.props
const board = await openBoard(boardId)
const comments = await board.getComments(postId, parentId)
this.setState({ comments })
}
toggleReplying = () => this.setState({ replying: !this.state.replying })
afterCommenting = () => {
this.setState({ replying: false })
this.refreshComments()
}
render() {
const { boardId, postId, parentId, ...others } = this.props
const { comments = [], replying } = this.state
return (
<List {...others}>
{comments.length === 0 && !parentId && (
<ListItem>
<ListItemText>No comments yet</ListItemText>
</ListItem>
)}
{replying && (
<AddComment
boardId={boardId}
postId={postId}
parentId={parentId}
afterSend={this.afterCommenting}
/>
)}
<Button
color="primary"
onClick={this.toggleReplying}
>
<CommentIcon style={{ marginRight: '8px' }} />
{parentId ? 'Reply' : 'Add Comment'}
</Button>
{comments.map(c => (
<React.Fragment>
<ListItem>
<ListItemText>{c.text}</ListItemText>
</ListItem>
<Comments
boardId={boardId}
postId={postId}
parentId={c.multihash}
style={{ marginLeft: '32px' }}
/>
</React.Fragment>
))}
</List>
)
}
}
export default Comments

27
components/Container.js Normal file
View File

@ -0,0 +1,27 @@
import React from 'react'
import { makeStyles } from '@material-ui/core/styles'
const useStyles = makeStyles(theme => ({
container: {
padding: theme.spacing(2),
display: 'flex',
justifyContent: 'center',
},
content: {
maxWidth: '600px',
flexGrow: 2
}
}))
const Container = ({ children }) => {
const styles = useStyles()
return (
<div className={styles.container}>
<div className={styles.content}>
{children}
</div>
</div>
)
}
export default Container

37
components/Post.js Normal file
View File

@ -0,0 +1,37 @@
import React from 'react'
import { Fab, Card, CardContent, CardHeader, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import Comments from './Comments'
import AddIcon from '@material-ui/icons/Add'
import ProfileIcon from '@material-ui/icons/AccountCircle'
const Post = ({ post = {}, boardId }) => {
const found = Boolean(post.multihash)
return (
<React.Fragment>
<Card>
<CardHeader
title={found ? post.title || 'Untitled Post' : 'Not Found'}
subheader="Last Activity X Time Ago"
/>
<CardContent>
{post.text || post.multihash || '(This post is empty)'}
</CardContent>
</Card>
<List>
<ListItem>
<ListItemAvatar><ProfileIcon /></ListItemAvatar>
<ListItemText
primary="Username"
secondary="Discovered X Time Ago"
/>
</ListItem>
</List>
{boardId && post.multihash && (
<Comments boardId={boardId} postId={post.multihash} />
)}
</React.Fragment>
)
}
export default Post

View File

@ -28,11 +28,12 @@ class Status extends React.PureComponent {
let statusText = 'Pre-Rendered'
if (!info.isServer) {
statusText = 'Offline'
if (info.ipfsLoading) statusText = 'Starting IPFS'
if (info.ipfsReady) {
statusText = 'Starting DB'
Icon = ConnectedIcon
statusText = `${info.ipfsPeers.length} Peers`
}
if (info.orbitDbReady) statusText = `${info.ipfsPeers.length} Peers`
if (info.orbitDbLoading) statusText = 'Starting DB'
}
return <Button color="inherit">

View File

@ -21,12 +21,43 @@ export function getGlobalData() {
return scope.ipfsBoards
}
async function getIPFSOptions() {
const common = {
libp2p: {
config: {
pubsub: { enabled: true }
}
}
}
if (isServer()) {
return {
relay: { enabled: true, hop: { enabled: true, active: true } },
...common
}
} else {
const serverInfo = await getServerInfo()
let additionalOptions = {}
if (serverInfo) {
additionalOptions = {
config: {
Bootstrap: [ ...serverInfo.multiaddrs ]
}
}
}
return {
...common,
...additionalOptions
}
}
}
export async function getIPFS() {
const data = getGlobalData()
if (data.ipfs) return data.ipfs
const IPFS = await import(/* webpackChunkName: "ipfs" */ 'ipfs')
const options = await getIPFSOptions()
if (!data.ipfsPromise) {
const IPFS = await import(/* webpackChunkName: "ipfs" */ 'ipfs')
data.ipfsPromise = IPFS.create()
data.ipfsPromise = IPFS.create(options)
}
data.ipfs = await data.ipfsPromise
delete data.ipfsPromise
@ -34,19 +65,24 @@ export async function getIPFS() {
}
export async function getOrbitDB() {
const data = getGlobalData()
if (data.orbitDb) return data.orbitDb
const ipfs = await getIPFS()
if (!data.orbitDbPromise) {
try {
const data = getGlobalData()
if (data.orbitDb) return data.orbitDb
const ipfs = await getIPFS()
const OrbitDB = await import(/* webpackChunkName: "orbit-db" */ 'orbit-db').then(m => m.default)
const BoardStore = await import(/* webpackChunkName: "orbit-db-discussion-board" */ 'orbit-db-discussion-board').then(m => m.default)
OrbitDB.addDatabaseType(BoardStore.type, BoardStore)
data.orbitDbPromise = OrbitDB.createInstance(ipfs)
if (!data.orbitDbPromise) {
OrbitDB.addDatabaseType(BoardStore.type, BoardStore)
data.orbitDbPromise = OrbitDB.createInstance(ipfs)
}
data.orbitDb = await data.orbitDbPromise
delete data.orbitDbPromise
if (!data.boards) data.boards = {}
return data.orbitDb
} catch (error) {
console.log('FATAL: COULD NOT LOAD ORBITDB', error)
throw error
}
data.orbitDb = await data.orbitDbPromise
delete data.orbitDbPromise
if (!data.boards) data.boards = {}
return data.orbitDb
}
export async function openBoard(id) {
@ -106,13 +142,43 @@ export function getInfo() {
export async function refreshInfo() {
const data = getGlobalData()
const ipfsReady = Boolean(data.ipfs)
const multiaddrs = ipfsReady ? data.ipfs.libp2p.peerInfo.multiaddrs.toArray().map(m => m.toJSON()) : []
data.info = {
isServer: isServer(),
ipfsReady: Boolean(data.ipfs),
ipfsReady,
ipfsLoading: Boolean(data.ipfsPromise),
orbitDbReady: Boolean(data.orbitDb),
orbitDbPromise: Boolean(data.orbitDbPromise),
openBoards: Object.keys(data.boards || {}),
ipfsPeers: await getIPFSPeers(),
pubsub: await getPubsubInfo()
pubsub: await getPubsubInfo(),
multiaddrs
}
return data.info
}
export async function getServerInfo() {
const response = await fetch('/api/status')
if (response.status === 200) {
return response.json()
}
return null
}
export async function connectoToIPFSMultiaddr(multiaddr) {
const ipfs = await getIPFS()
try {
// console.log(`Connecting to ${multiaddr}...`)
await ipfs.swarm.connect(multiaddr)
console.log(`Connected to ${multiaddr}!`)
} catch (error) {
// console.log(`Connection to ${multiaddr} failed:`, error.message)
}
}
export async function connectIPFSToBackend() {
const serverInfo = await getServerInfo()
const addresses = serverInfo.multiaddrs
return Promise.race(addresses.map(connectoToIPFSMultiaddr))
}

999
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
"@material-ui/core": "^4.5.1",
"@material-ui/icons": "^4.5.1",
"@material-ui/styles": "^4.5.0",
"ipfs": "~0.38.0",
"ipfs": "~0.39.0",
"next": "9.1.1",
"orbit-db": "~0.22.0",
"orbit-db-discussion-board": "https://github.com/fazo96/orbit-db-discussion-board.git",

View File

@ -5,6 +5,8 @@ import { ThemeProvider } from '@material-ui/styles';
import AppBar from '../components/AppBar'
import CssBaseline from '@material-ui/core/CssBaseline';
import theme from '../components/theme';
import Container from '../components/Container'
import { connectIPFSToBackend } from '../components/system';
export default class MyApp extends App {
componentDidMount() {
@ -13,6 +15,8 @@ export default class MyApp extends App {
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles);
}
// Connect IPFS to backend server to improve connectivity
connectIPFSToBackend().catch(error => console.log('connectIPFSToBackend failed', error))
}
render() {
@ -27,7 +31,9 @@ export default class MyApp extends App {
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<AppBar />
<Component {...pageProps} />
<Container>
<Component {...pageProps} />
</Container>
</ThemeProvider>
</React.Fragment>
);

8
pages/api/status.js Normal file
View File

@ -0,0 +1,8 @@
import { refreshInfo } from '../../components/system'
export default async (req, res) => {
const info = await refreshInfo()
res.setHeader('Content-Type', 'application/json')
res.statusCode = 200
res.end(JSON.stringify(info))
}

View File

@ -1,33 +1,46 @@
import React from 'react'
import { Fab, Card, CardContent, CardContentText, CardHeader, List, ListItem, ListItemAvatar, ListItemText } from '@material-ui/core'
import AddIcon from '@material-ui/icons/Add'
import ProfileIcon from '@material-ui/icons/AccountCircle'
import { openBoard } from '../../../../components/system'
import Post from '../../../../components/Post'
const Post = () => {
return (
<React.Fragment>
<Card>
<CardHeader
title="First Post"
subheader="Last Activity X Time Ago"
/>
<CardContent>
<CardContentText>Lorem Ipsum...</CardContentText>
</CardContent>
</Card>
<List>
<ListItem>
<ListItemAvatar><ProfileIcon /></ListItemAvatar>
<ListItemText
primary="Username"
secondary="Discovered X Time Ago"
/>
</ListItem>
</List>
<Fab color="primary"><AddIcon /></Fab>
</React.Fragment>
)
const findPost = (posts, id) => {
const results = posts.filter(p => p.multihash === id)
if (results.length > 0) return results[0]
return undefined
}
export default Post
class PostPage extends React.PureComponent {
state = {
post: undefined
}
componentDidMount() {
this.refreshPost()
}
async refreshPost() {
const { boardId, postId } = this.props
const board = await openBoard(boardId)
const post = findPost(board.posts, postId)
this.setState({ post })
}
render() {
const { post: postProp, boardId } = this.props
const { post } = this.state
return <Post
post={post || postProp}
boardId={boardId}
/>
}
}
PostPage.getInitialProps = async ({ query }) => {
const board = await openBoard(query.board)
return {
post: findPost(board.posts, query.post),
boardId: query.board,
postId: query.post
}
}
export default PostPage

View File

@ -2,10 +2,25 @@ import React, { useState } from 'react'
import { openBoard } from '../../../../components/system'
import Router from 'next/router'
import { makeStyles } from '@material-ui/core/styles'
import { Card, CardHeader, CardContent, TextField, Button, Avatar } from '@material-ui/core'
import { Typography, Box, Card, CardHeader, Divider, CardActions, CardContent, TextField, Button, Avatar, Tabs, Tab } from '@material-ui/core'
import SendIcon from '@material-ui/icons/Send'
import AddIcon from '@material-ui/icons/Add'
function TabPanel(props) {
const { children, value, index, ...other } = props
return (
<Typography
component="div"
role="tabpanel"
hidden={value !== index}
{...other}
>
<Box p={3}>{children}</Box>
</Typography>
)
}
const useStyles = makeStyles(theme => ({
card: {
maxWidth: 600,
@ -22,7 +37,18 @@ const useStyles = makeStyles(theme => ({
},
buttonIcon: {
marginLeft: theme.spacing(1)
}
},
tabContainer: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper,
display: 'flex',
},
tabPanel: {
flexGrow: 1
},
tabs: {
borderRight: `1px solid ${theme.palette.divider}`,
},
}))
async function createPost(boardId, postData) {
@ -33,7 +59,21 @@ async function createPost(boardId, postData) {
export default function CreatePost({ boardId }) {
const classes = useStyles()
// State
const [title, setTitle] = useState('')
const [cid, setCID] = useState('')
const [content, setContent] = useState('')
const [tab, setTab] = useState(0)
// Actions
const submitPost = () => {
const payload = { title }
if (tab === 0) payload.text = content
if (tab === 2) payload.multihash = cid
return createPost(boardId, payload)
}
return <Card className={classes.card}>
<CardHeader
avatar={<Avatar><AddIcon /></Avatar>}
@ -53,25 +93,68 @@ export default function CreatePost({ boardId }) {
autoFocus
fullWidth
/>
<TextField
className={classes.field}
variant="filled"
label="CID"
placeholder="Paste the IPFS CID or your post content (WIP)"
value={title}
onChange={e => setTitle(e.target.value)}
fullWidth
/>
<Button variant="contained" color="primary" onClick={() => createPost(boardId, { title })}>
Submit <SendIcon className={classes.buttonIcon} />
</Button>
<Button className={classes.secondaryButton} onClick={() => Router.push(`/b/${boardId}`)}>
</CardContent>
<Divider />
<div className={classes.tabContainer}>
<Tabs
orientation="vertical"
value={tab}
onChange={(event, value) => setTab(value)}
className={classes.tabs}
>
<Tab label="Text" />
<Tab label="Media" />
<Tab label="IPFS CID" />
</Tabs>
<TabPanel value={tab} index={0} className={classes.tabPanel}>
<TextField
multiline
fullWidth
placeholder="What's on your mind?"
rows={3}
variant="outlined"
label="Post"
value={content}
onChange={event => setContent(event.target.value)}
/>
</TabPanel>
<TabPanel value={tab} index={1} className={classes.tabPanel}>
WIP
</TabPanel>
<TabPanel value={tab} index={2} className={classes.tabPanel}>
<TextField
className={classes.field}
variant="filled"
label="CID"
placeholder="Paste the IPFS CID or your post content (WIP)"
value={cid}
onChange={e => setCID(e.target.value)}
helperText="Enter the CID or Multihash of existing IPFS content"
fullWidth
/>
</TabPanel>
</div>
<Divider />
<CardActions>
<Button
className={classes.secondaryButton}
onClick={() => Router.push(`/b/${boardId}`)}
style={{ marginLeft: 'auto' }}
>
Cancel
</Button>
</CardContent>
<Button
variant="contained"
color="primary"
onClick={submitPost}
>
Submit <SendIcon className={classes.buttonIcon} />
</Button>
</CardActions>
</Card>
}
CreatePost.getInitialProps = ({ query }) => {
return { boardId: query.board }
}
}

View File

@ -1,19 +1,44 @@
import React, { useState } from 'react'
import { Card, CardContent, TextField, Button } from '@material-ui/core'
import { Card, CardContent, TextField, Button, Typography, Divider} from '@material-ui/core'
import OpenIcon from '@material-ui/icons/Add'
import Router from 'next/router'
import { makeStyles } from '@material-ui/styles'
const useStyles = makeStyles(theme => ({
button: {
marginTop: theme.spacing(2),
marginLeft: 'auto',
}
}))
export default function OpenBoard() {
const [name, setName] = useState('')
const styles = useStyles()
return <Card>
<CardContent>
<Typography variant="h5">
Open a Board
</Typography>
<Typography variant="subtitle1">
IPFS Boards is a work in progress. Thank you for testing the app!
</Typography>
</CardContent>
<Divider />
<CardContent>
<TextField
label="Board"
placeholder="Type a name..."
value={name}
onChange={e => setName(e.target.value)}
autoFocus
fullWidth
/>
<Button variant="contained" color="primary">
<Button
variant="contained"
color="primary"
className={styles.button}
disabled={!name}
onClick={() => Router.push(`/b/${name}`)}
>
<OpenIcon /> Open
</Button>
</CardContent>

View File

@ -1,12 +1,21 @@
import React from 'react'
import { Fab, Card, CardActions, CardHeader, Button } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import AddIcon from '@material-ui/icons/Add'
import ViewIcon from '@material-ui/icons/Visibility'
import DeleteIcon from '@material-ui/icons/Delete'
import Link from 'next/link'
import Router from 'next/router'
const useStyles = makeStyles(theme => ({
fab: {
float: 'right',
marginTop: theme.spacing(2)
},
}))
const Home = () => {
const styles = useStyles()
return (
<React.Fragment>
<Card>
@ -24,7 +33,9 @@ const Home = () => {
</CardActions>
</Card>
<Link href="/b/open">
<Fab color="primary"><AddIcon /></Fab>
<Fab color="primary" className={styles.fab}>
<AddIcon />
</Fab>
</Link>
</React.Fragment>
)