mirror of https://gitlab.com/ecentrics/concordia
Apostolos Fanakis
4 years ago
23 changed files with 683 additions and 349 deletions
@ -1,167 +1,152 @@ |
|||
import React, { Children, Component } from 'react'; |
|||
import { connect } from 'react-redux'; |
|||
|
|||
import React, { Children } from 'react'; |
|||
import { breezeConstants } from '@ezerous/breeze'; |
|||
|
|||
import { useSelector } from 'react-redux'; |
|||
import LoadingComponent from './LoadingComponent'; |
|||
|
|||
// CSS |
|||
import '../assets/css/loading-component.css'; |
|||
|
|||
class LoadingContainer extends Component { |
|||
render() { |
|||
const { |
|||
web3: { |
|||
status, networkId, networkFailed, accountsFailed, |
|||
}, |
|||
drizzleStatus: { |
|||
initializing, |
|||
failed, |
|||
}, |
|||
contractInitialized, contractDeployed, ipfsStatus, orbitStatus, userFetched, children, |
|||
} = this.props; |
|||
|
|||
if ((status === 'initializing' || !networkId) |
|||
&& !networkFailed) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="Connecting to the Ethereum network..." |
|||
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account." |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
const LoadingContainer = ({ children }) => { |
|||
const initializing = useSelector((state) => state.drizzleStatus.initializing); |
|||
const failed = useSelector((state) => state.drizzleStatus.failed); |
|||
const ipfsStatus = useSelector((state) => state.ipfs.status); |
|||
const orbitStatus = useSelector((state) => state.orbit.status); |
|||
const web3Status = useSelector((state) => state.web3.status); |
|||
const web3NetworkId = useSelector((state) => state.web3.networkId); |
|||
const web3NetworkFailed = useSelector((state) => state.web3.networkFailed); |
|||
const web3AccountsFailed = useSelector((state) => state.web3.accountsFailed); |
|||
const contractInitialized = useSelector((state) => state.contracts.Forum.initialized); |
|||
const contractDeployed = useSelector((state) => state.contracts.Forum.deployed); |
|||
const userFetched = useSelector((state) => state.user.address); |
|||
|
|||
if ((web3Status === 'initializing' || !web3NetworkId) |
|||
&& !web3NetworkFailed) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="Connecting to the Ethereum network..." |
|||
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account." |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (status === 'failed' || networkFailed) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="No connection to the Ethereum network!" |
|||
message="Please make sure that:" |
|||
message_list={['MetaMask is unlocked and pointed to the correct, available network', |
|||
'The app has been granted the right to connect to your account']} |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
if (web3Status === 'failed' || web3NetworkFailed) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="No connection to the Ethereum network!" |
|||
message="Please make sure that:" |
|||
message_list={['MetaMask is unlocked and pointed to the correct, available network', |
|||
'The app has been granted the right to connect to your account']} |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (status === 'initialized' && accountsFailed) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="We can't find any Ethereum accounts!" |
|||
message="Please make sure that MetaMask is unlocked." |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
if (web3Status === 'initialized' && web3AccountsFailed) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="We can't find any Ethereum accounts!" |
|||
message="Please make sure that MetaMask is unlocked." |
|||
imageType="ethereum" |
|||
progress={20} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (initializing |
|||
if (initializing |
|||
|| (!failed && !contractInitialized && contractDeployed)) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="Initializing contracts..." |
|||
message="" |
|||
imageType="ethereum" |
|||
progress={40} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (!contractDeployed) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="No contracts found on the current network!" |
|||
message="Please make sure that you are connected to the correct network and the contracts are deployed." |
|||
imageType="ethereum" |
|||
progress={40} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
return ( |
|||
<LoadingComponent |
|||
title="Initializing contracts..." |
|||
message="" |
|||
imageType="ethereum" |
|||
progress={40} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="Initializing IPFS..." |
|||
message="" |
|||
imageType="ipfs" |
|||
progress={60} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
if (!contractDeployed) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="No contracts found on the current network!" |
|||
message="Please make sure that you are connected to the correct network and the contracts are deployed." |
|||
imageType="ethereum" |
|||
progress={40} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (ipfsStatus === breezeConstants.STATUS_FAILED) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="IPFS initialization failed!" |
|||
message="" |
|||
imageType="ipfs" |
|||
progress={60} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="Initializing IPFS..." |
|||
message="" |
|||
imageType="ipfs" |
|||
progress={60} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (orbitStatus === breezeConstants.STATUS_INITIALIZING) { |
|||
const message = process.env.NODE_ENV === 'development' |
|||
? 'If needed, please sign the transaction in MetaMask to create the databases.' |
|||
: 'Please sign the transaction in MetaMask to create the databases.'; |
|||
return ( |
|||
<LoadingComponent |
|||
title="Preparing OrbitDB..." |
|||
message={message} |
|||
imageType="orbit" |
|||
progress={80} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
if (ipfsStatus === breezeConstants.STATUS_FAILED) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="IPFS initialization failed!" |
|||
message="" |
|||
imageType="ipfs" |
|||
progress={60} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (orbitStatus === breezeConstants.STATUS_FAILED) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="OrbitDB initialization failed!" |
|||
message="" |
|||
imageType="orbit" |
|||
progress={80} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
if (orbitStatus === breezeConstants.STATUS_INITIALIZING) { |
|||
const message = process.env.NODE_ENV === 'development' |
|||
? 'If needed, please sign the transaction in MetaMask to create the databases.' |
|||
: 'Please sign the transaction in MetaMask to create the databases.'; |
|||
return ( |
|||
<LoadingComponent |
|||
title="Preparing OrbitDB..." |
|||
message={message} |
|||
imageType="orbit" |
|||
progress={80} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
if (!userFetched) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="Loading dapp..." |
|||
message="" |
|||
imageType="app" |
|||
progress={90} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
if (orbitStatus === breezeConstants.STATUS_FAILED) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="OrbitDB initialization failed!" |
|||
message="" |
|||
imageType="orbit" |
|||
progress={80} |
|||
progressType="error" |
|||
/> |
|||
); |
|||
} |
|||
|
|||
return Children.only(children); |
|||
if (!userFetched) { |
|||
return ( |
|||
<LoadingComponent |
|||
title="Loading dapp..." |
|||
message="" |
|||
imageType="app" |
|||
progress={90} |
|||
progressType="indicating" |
|||
/> |
|||
); |
|||
} |
|||
} |
|||
|
|||
const mapStateToProps = (state) => ({ |
|||
drizzleStatus: state.drizzleStatus, |
|||
breezeStatus: state.breezeStatus, |
|||
ipfsStatus: state.ipfs.status, |
|||
orbitStatus: state.orbit.status, |
|||
web3: state.web3, |
|||
accounts: state.accounts, |
|||
contractInitialized: state.contracts.Forum.initialized, |
|||
contractDeployed: state.contracts.Forum.deployed, |
|||
userFetched: state.user.address, |
|||
}); |
|||
return Children.only(children); |
|||
}; |
|||
|
|||
export default connect(mapStateToProps)(LoadingContainer); |
|||
export default LoadingContainer; |
|||
|
@ -1,49 +0,0 @@ |
|||
import React from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { List } from 'semantic-ui-react'; |
|||
import { PLACEHOLDER_TYPE_POST, PLACEHOLDER_TYPE_TOPIC } from '../../constants/PlaceholderTypes'; |
|||
|
|||
const Placeholder = (props) => { |
|||
const { placeholderType, extra } = props; |
|||
|
|||
switch (placeholderType) { |
|||
case PLACEHOLDER_TYPE_TOPIC: |
|||
return ( |
|||
<> |
|||
<List.Header> |
|||
<List.Icon name="right triangle" /> |
|||
topicSubject |
|||
</List.Header> |
|||
<List.Content> |
|||
username |
|||
Number of Replies |
|||
timestamp |
|||
</List.Content> |
|||
</> |
|||
); |
|||
case PLACEHOLDER_TYPE_POST: |
|||
return ( |
|||
<div>LOADING POST</div> |
|||
); |
|||
default: |
|||
return <div />; |
|||
} |
|||
}; |
|||
|
|||
const TopicPlaceholderExtra = PropTypes.PropTypes.shape({ |
|||
topicId: PropTypes.number.isRequired, |
|||
}); |
|||
|
|||
const PostPlaceholderExtra = PropTypes.PropTypes.shape({ |
|||
postIndex: PropTypes.number.isRequired, |
|||
}); |
|||
|
|||
Placeholder.propTypes = { |
|||
placeholderType: PropTypes.string.isRequired, |
|||
extra: PropTypes.oneOfType([ |
|||
TopicPlaceholderExtra.isRequired, |
|||
PostPlaceholderExtra.isRequired, |
|||
]), |
|||
}; |
|||
|
|||
export default Placeholder; |
@ -0,0 +1,103 @@ |
|||
import React, { |
|||
memo, useEffect, useMemo, useState, |
|||
} from 'react'; |
|||
import { |
|||
Dimmer, Grid, List, Loader, Placeholder, |
|||
} from 'semantic-ui-react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import moment from 'moment'; |
|||
import { useHistory } from 'react-router'; |
|||
import { useDispatch, useSelector } from 'react-redux'; |
|||
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; |
|||
import { breeze } from '../../../redux/store'; |
|||
import './styles.css'; |
|||
|
|||
const { orbit } = breeze; |
|||
|
|||
const PostListRow = (props) => { |
|||
const { id: postId, postCallHash, loading } = props; |
|||
const getPostResults = useSelector((state) => state.contracts.Forum.getPost); |
|||
const [postAuthorAddress, setPostAuthorAddress] = useState(null); |
|||
const [postAuthor, setPostAuthor] = useState(null); |
|||
const [timeAgo, setTimeAgo] = useState(null); |
|||
const [postSubject, setPostSubject] = useState(null); |
|||
const [postMessage, setPostMessage] = useState(null); |
|||
const userAddress = useSelector((state) => state.user.address); |
|||
const posts = useSelector((state) => state.orbitData.posts); |
|||
const dispatch = useDispatch(); |
|||
const history = useHistory(); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
if (!loading && postCallHash && getPostResults[postCallHash] !== undefined) { |
|||
setPostAuthorAddress(getPostResults[postCallHash].value[0]); |
|||
setPostAuthor(getPostResults[postCallHash].value[1]); |
|||
setTimeAgo(moment(getPostResults[postCallHash].value[2] * 1000).fromNow()); |
|||
} |
|||
}, [getPostResults, loading, postCallHash]); |
|||
|
|||
useEffect(() => { |
|||
if (postAuthorAddress && userAddress !== postAuthorAddress) { |
|||
dispatch({ |
|||
type: FETCH_USER_DATABASE, |
|||
orbit, |
|||
dbName: 'posts', |
|||
userAddress: postAuthorAddress, |
|||
}); |
|||
} |
|||
}, [dispatch, postAuthorAddress, userAddress]); |
|||
|
|||
useEffect(() => { |
|||
const postFound = posts |
|||
.find((post) => post.id === postId); |
|||
|
|||
if (postFound) { |
|||
setPostSubject(postFound.subject); |
|||
setPostMessage(postFound.message); |
|||
} |
|||
}, [postId, posts]); |
|||
|
|||
return useMemo(() => ( |
|||
<Dimmer.Dimmable as={List.Item} blurring dimmed={loading} className="list-item"> |
|||
<List.Icon name="user circle" size="big" inverted color="black" verticalAlign="middle" /> |
|||
<List.Content> |
|||
<List.Header> |
|||
<Grid> |
|||
<Grid.Column floated="left" width={14}> |
|||
{postSubject !== null |
|||
? postSubject |
|||
: <Placeholder><Placeholder.Line length="very long" /></Placeholder>} |
|||
</Grid.Column> |
|||
<Grid.Column floated="right" width={2} textAlign="right"> |
|||
<span className="post-metadata"> |
|||
{t('post.list.row.post.id', { id: postId })} |
|||
</span> |
|||
</Grid.Column> |
|||
</Grid> |
|||
</List.Header> |
|||
<List.Description> |
|||
<Grid verticalAlign="middle"> |
|||
<Grid.Column floated="left" width={14}> |
|||
{postAuthor !== null && timeAgo !== null |
|||
? t('post.list.row.author.date', { author: postAuthor, timeAgo }) |
|||
: <Placeholder><Placeholder.Line length="long" /></Placeholder>} |
|||
</Grid.Column> |
|||
</Grid> |
|||
</List.Description> |
|||
</List.Content> |
|||
</Dimmer.Dimmable> |
|||
), [loading, postAuthor, postId, postSubject, t, timeAgo]); |
|||
}; |
|||
|
|||
PostListRow.defaultProps = { |
|||
loading: false, |
|||
}; |
|||
|
|||
PostListRow.propTypes = { |
|||
id: PropTypes.number.isRequired, |
|||
postCallHash: PropTypes.string, |
|||
loading: PropTypes.bool, |
|||
}; |
|||
|
|||
export default memo(PostListRow); |
@ -0,0 +1,8 @@ |
|||
.post-metadata { |
|||
font-size: 12px !important; |
|||
font-weight: initial; |
|||
} |
|||
|
|||
.list-item { |
|||
text-align: start; |
|||
} |
@ -0,0 +1,70 @@ |
|||
import React, { |
|||
useEffect, useMemo, useState, |
|||
} from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useSelector } from 'react-redux'; |
|||
import { Dimmer, List, Loader } from 'semantic-ui-react'; |
|||
import PostListRow from './PostListRow'; |
|||
import { drizzle } from '../../redux/store'; |
|||
|
|||
const { contracts: { Forum: { methods: { getPost: { cacheCall: getPostChainData } } } } } = drizzle; |
|||
|
|||
const PostList = (props) => { |
|||
const { postIds, loading } = props; |
|||
const [getPostCallHashes, setGetPostCallHashes] = useState([]); |
|||
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized); |
|||
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed); |
|||
|
|||
useEffect(() => { |
|||
if (drizzleInitialized && !drizzleInitializationFailed && !loading) { |
|||
const newPostsFound = postIds |
|||
.filter((postId) => !getPostCallHashes |
|||
.map((getPostCallHash) => getPostCallHash.id) |
|||
.includes(postId)); |
|||
|
|||
if (newPostsFound.length > 0) { |
|||
setGetPostCallHashes([ |
|||
...getPostCallHashes, |
|||
...newPostsFound |
|||
.map((postId) => ({ |
|||
id: postId, |
|||
hash: getPostChainData(postId), |
|||
})), |
|||
]); |
|||
} |
|||
} |
|||
}, [drizzleInitializationFailed, drizzleInitialized, getPostCallHashes, loading, postIds]); |
|||
|
|||
const posts = useMemo(() => { |
|||
if (loading) { |
|||
return null; |
|||
} |
|||
return postIds |
|||
.map((postId) => { |
|||
const postHash = getPostCallHashes.find((getPostCallHash) => getPostCallHash.id === postId); |
|||
|
|||
return ( |
|||
<PostListRow |
|||
id={postId} |
|||
key={postId} |
|||
postCallHash={postHash && postHash.hash} |
|||
loading={postHash === undefined} |
|||
/> |
|||
); |
|||
}); |
|||
}, [getPostCallHashes, loading, postIds]); |
|||
|
|||
return ( |
|||
<Dimmer.Dimmable as={List} blurring dimmed={loading} selection divided id="post-list" size="big"> |
|||
<Loader active={loading} /> |
|||
{posts} |
|||
</Dimmer.Dimmable> |
|||
); |
|||
}; |
|||
|
|||
PostList.propTypes = { |
|||
postIds: PropTypes.arrayOf(PropTypes.number).isRequired, |
|||
loading: PropTypes.bool, |
|||
}; |
|||
|
|||
export default PostList; |
@ -0,0 +1,3 @@ |
|||
#post-list{ |
|||
height: 100%; |
|||
} |
@ -1,64 +1,118 @@ |
|||
import React, { useContext, useEffect, useState } from 'react'; |
|||
import { List } from 'semantic-ui-react'; |
|||
import React, { |
|||
memo, useEffect, useMemo, useState, |
|||
} from 'react'; |
|||
import { |
|||
Dimmer, Grid, List, Placeholder, |
|||
} from 'semantic-ui-react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import moment from 'moment'; |
|||
import { useHistory } from 'react-router'; |
|||
import { useDispatch, useSelector } from 'react-redux'; |
|||
import AppContext from '../../AppContext'; |
|||
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; |
|||
import { breeze } from '../../../redux/store'; |
|||
import './styles.css'; |
|||
|
|||
const { orbit } = breeze; |
|||
|
|||
const TopicListRow = (props) => { |
|||
const { topicData, topicId } = props; |
|||
const { breeze: { orbit } } = useContext(AppContext.Context); |
|||
const [topicSubject, setTopicSubject] = useState(); |
|||
const { id: topicId, topicCallHash, loading } = props; |
|||
const getTopicResults = useSelector((state) => state.contracts.Forum.getTopic); |
|||
const [numberOfReplies, setNumberOfReplies] = useState(null); |
|||
const [topicAuthorAddress, setTopicAuthorAddress] = useState(null); |
|||
const [topicAuthor, setTopicAuthor] = useState(null); |
|||
const [timeAgo, setTimeAgo] = useState(null); |
|||
const [topicSubject, setTopicSubject] = useState(null); |
|||
const userAddress = useSelector((state) => state.user.address); |
|||
const topics = useSelector((state) => state.orbitData.topics); |
|||
const dispatch = useDispatch(); |
|||
const history = useHistory(); |
|||
const { t } = useTranslation(); |
|||
|
|||
useEffect(() => { |
|||
if (!loading && topicCallHash && getTopicResults[topicCallHash] !== undefined) { |
|||
setTopicAuthorAddress(getTopicResults[topicCallHash].value[0]); |
|||
setTopicAuthor(getTopicResults[topicCallHash].value[1]); |
|||
setTimeAgo(moment(getTopicResults[topicCallHash].value[2] * 1000).fromNow()); |
|||
setNumberOfReplies(getTopicResults[topicCallHash].value[3].length); |
|||
} |
|||
}, [getTopicResults, loading, topicCallHash]); |
|||
|
|||
useEffect(() => { |
|||
if (userAddress !== topicData.userAddress) { |
|||
if (topicAuthorAddress && userAddress !== topicAuthorAddress) { |
|||
dispatch({ |
|||
type: FETCH_USER_DATABASE, |
|||
orbit, |
|||
userAddress: topicData.userAddress, |
|||
dbName: 'topics', |
|||
userAddress: topicAuthorAddress, |
|||
}); |
|||
} |
|||
}, [dispatch, orbit, topicData.userAddress, topicId, userAddress]); |
|||
}, [dispatch, topicAuthorAddress, userAddress]); |
|||
|
|||
useEffect(() => { |
|||
const topicFound = topics |
|||
.find((topic) => topic.id === topicId); |
|||
|
|||
if (topicFound) { |
|||
setTopicSubject(topicFound); |
|||
setTopicSubject(topicFound.subject); |
|||
} |
|||
}, [topicId, topics]); |
|||
|
|||
return ( |
|||
<> |
|||
<List.Header> |
|||
<List.Icon name="right triangle" /> |
|||
{topicSubject && topicSubject.subject} |
|||
</List.Header> |
|||
<List.Content> |
|||
{topicData.username} |
|||
{topicData.numberOfReplies} |
|||
{' '} |
|||
replies |
|||
timestamp |
|||
</List.Content> |
|||
</> |
|||
); |
|||
return useMemo(() => { |
|||
const handleTopicClick = () => { |
|||
history.push(`/topics/${topicId}`); |
|||
}; |
|||
|
|||
return ( |
|||
<Dimmer.Dimmable as={List.Item} onClick={handleTopicClick} blurring dimmed={loading} className="list-item"> |
|||
<List.Icon name="user circle" size="big" inverted color="black" verticalAlign="middle" /> |
|||
<List.Content> |
|||
<List.Header> |
|||
<Grid> |
|||
<Grid.Column floated="left" width={14}> |
|||
{topicSubject !== null |
|||
? topicSubject |
|||
: <Placeholder><Placeholder.Line length="very long" /></Placeholder>} |
|||
</Grid.Column> |
|||
<Grid.Column floated="right" width={2} textAlign="right"> |
|||
<span className="topic-metadata"> |
|||
{t('topic.list.row.topic.id', { id: topicId })} |
|||
</span> |
|||
</Grid.Column> |
|||
</Grid> |
|||
</List.Header> |
|||
<List.Description> |
|||
<Grid verticalAlign="middle"> |
|||
<Grid.Column floated="left" width={14}> |
|||
{topicAuthor !== null && timeAgo !== null |
|||
? t('topic.list.row.author.date', { author: topicAuthor, timeAgo }) |
|||
: <Placeholder><Placeholder.Line length="long" /></Placeholder>} |
|||
</Grid.Column> |
|||
<Grid.Column floated="right" width={2} textAlign="right"> |
|||
{numberOfReplies !== null |
|||
? ( |
|||
<span className="topic-metadata"> |
|||
{t('topic.list.row.number.of.replies', { numberOfReplies })} |
|||
</span> |
|||
) |
|||
: <Placeholder fluid><Placeholder.Line /></Placeholder>} |
|||
</Grid.Column> |
|||
</Grid> |
|||
</List.Description> |
|||
</List.Content> |
|||
</Dimmer.Dimmable> |
|||
); |
|||
}, [history, loading, numberOfReplies, t, timeAgo, topicAuthor, topicId, topicSubject]); |
|||
}; |
|||
|
|||
const TopicData = PropTypes.PropTypes.shape({ |
|||
userAddress: PropTypes.string.isRequired, |
|||
username: PropTypes.string.isRequired, |
|||
timestamp: PropTypes.number.isRequired, |
|||
numberOfReplies: PropTypes.number.isRequired, |
|||
}); |
|||
TopicListRow.defaultProps = { |
|||
loading: false, |
|||
}; |
|||
|
|||
TopicListRow.propTypes = { |
|||
topicData: TopicData.isRequired, |
|||
topicId: PropTypes.number.isRequired, |
|||
id: PropTypes.number.isRequired, |
|||
topicCallHash: PropTypes.string, |
|||
loading: PropTypes.bool, |
|||
}; |
|||
|
|||
export default TopicListRow; |
|||
export default memo(TopicListRow); |
|||
|
@ -0,0 +1,8 @@ |
|||
.topic-metadata { |
|||
font-size: 12px !important; |
|||
font-weight: initial; |
|||
} |
|||
|
|||
.list-item { |
|||
text-align: start; |
|||
} |
@ -1,7 +1,3 @@ |
|||
#topic-list{ |
|||
height: 100%; |
|||
} |
|||
|
|||
.list-item { |
|||
text-align: start; |
|||
} |
@ -0,0 +1,71 @@ |
|||
* { |
|||
outline: 2px dotted red |
|||
} |
|||
|
|||
* * { |
|||
outline: 2px dotted green |
|||
} |
|||
|
|||
* * * { |
|||
outline: 2px dotted orange |
|||
} |
|||
|
|||
* * * * { |
|||
outline: 2px dotted blue |
|||
} |
|||
|
|||
* * * * * { |
|||
outline: 1px solid red |
|||
} |
|||
|
|||
* * * * * * { |
|||
outline: 1px solid green |
|||
} |
|||
|
|||
* * * * * * * { |
|||
outline: 1px solid orange |
|||
} |
|||
|
|||
* * * * * * * * { |
|||
outline: 1px solid blue |
|||
} |
|||
|
|||
/* Solid Green */ |
|||
* *:hover { |
|||
border: 2px solid #89A81E |
|||
} |
|||
|
|||
/* Solid Orange */ |
|||
* * *:hover { |
|||
border: 2px solid #F34607 |
|||
} |
|||
|
|||
/* Solid Blue */ |
|||
* * * *:hover { |
|||
border: 2px solid #5984C3 |
|||
} |
|||
|
|||
/* Solid Red */ |
|||
* * * * *:hover { |
|||
border: 2px solid #CD1821 |
|||
} |
|||
|
|||
/* Dotted Green */ |
|||
* * * * * *:hover { |
|||
border: 2px dotted #89A81E |
|||
} |
|||
|
|||
/* Dotted Orange */ |
|||
* * * * * * *:hover { |
|||
border: 2px dotted #F34607 |
|||
} |
|||
|
|||
/* Dotted Blue */ |
|||
* * * * * * * *:hover { |
|||
border: 2px dotted #5984C3 |
|||
} |
|||
|
|||
/* Dotted Red */ |
|||
* * * * * * * * *:hover { |
|||
border: 2px dotted #CD1821 |
|||
} |
@ -1,31 +1,32 @@ |
|||
import React, { |
|||
useContext, useEffect, useMemo, useState, |
|||
memo, useEffect, useMemo, useState, |
|||
} from 'react'; |
|||
import { Container } from 'semantic-ui-react'; |
|||
import { useSelector } from 'react-redux'; |
|||
import AppContext from '../../components/AppContext'; |
|||
import Board from './Board'; |
|||
import './styles.css'; |
|||
import { drizzle } from '../../redux/store'; |
|||
|
|||
const { contracts: { Forum: { methods: { getNumberOfTopics } } } } = drizzle; |
|||
|
|||
const Home = () => { |
|||
const { drizzle: { contracts: { Forum: { methods: { getNumberOfTopics } } } } } = useContext(AppContext.Context); |
|||
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState(''); |
|||
const getNumberOfTopicsResults = useSelector((state) => state.contracts.Forum.getNumberOfTopics); |
|||
|
|||
useEffect(() => { |
|||
setNumberOfTopicsCallHash(getNumberOfTopics.cacheCall()); |
|||
}, [getNumberOfTopics]); |
|||
}, []); |
|||
|
|||
const numberOfTopics = useMemo(() => (getNumberOfTopicsResults[numberOfTopicsCallHash] !== undefined |
|||
? parseInt(getNumberOfTopicsResults[numberOfTopicsCallHash].value, 10) |
|||
: null), |
|||
[getNumberOfTopicsResults, numberOfTopicsCallHash]); |
|||
|
|||
return ( |
|||
return useMemo(() => ( |
|||
<Container id="home-container" textAlign="center"> |
|||
{numberOfTopics !== null && <Board numberOfTopics={numberOfTopics} />} |
|||
</Container> |
|||
); |
|||
), [numberOfTopics]); |
|||
}; |
|||
|
|||
export default Home; |
|||
export default memo(Home); |
|||
|
@ -1,18 +1,132 @@ |
|||
import React from 'react'; |
|||
import React, { useEffect, useState } from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useDispatch, useSelector } from 'react-redux'; |
|||
import { |
|||
Container, Dimmer, Icon, Placeholder, Step, |
|||
} from 'semantic-ui-react'; |
|||
import moment from 'moment'; |
|||
import { breeze, drizzle } from '../../../redux/store'; |
|||
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; |
|||
import './styles.css'; |
|||
import PostList from '../../../components/PostList'; |
|||
|
|||
const { contracts: { Forum: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle; |
|||
const { orbit } = breeze; |
|||
|
|||
const TopicView = (props) => { |
|||
const { topicId } = props; |
|||
const { |
|||
topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor, |
|||
timestamp: initialTimestamp, postIds: initialPostIds, |
|||
} = props; |
|||
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized); |
|||
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed); |
|||
const userAddress = useSelector((state) => state.user.address); |
|||
const getTopicResults = useSelector((state) => state.contracts.Forum.getTopic); |
|||
const topics = useSelector((state) => state.orbitData.topics); |
|||
const [getTopicCallHash, setGetTopicCallHash] = useState([]); |
|||
const [topicAuthorAddress, setTopicAuthorAddress] = useState(initialTopicAuthorAddress || null); |
|||
const [topicAuthor, setTopicAuthor] = useState(initialTopicAuthor || null); |
|||
const [timestamp, setTimestamp] = useState(initialTimestamp || null); |
|||
const [postIds, setPostIds] = useState(initialPostIds || null); |
|||
const [topicSubject, setTopicSubject] = useState(null); |
|||
|
|||
const dispatch = useDispatch(); |
|||
|
|||
useEffect(() => { |
|||
const shouldGetTopicDataFromChain = topicAuthorAddress === null |
|||
|| topicAuthor === null |
|||
|| timestamp === null |
|||
|| postIds === null; |
|||
|
|||
if (drizzleInitialized && !drizzleInitializationFailed && shouldGetTopicDataFromChain) { |
|||
setGetTopicCallHash(getTopicChainData(topicId)); |
|||
} |
|||
}, [ |
|||
drizzleInitializationFailed, drizzleInitialized, postIds, timestamp, topicAuthor, topicAuthorAddress, topicId, |
|||
]); |
|||
|
|||
useEffect(() => { |
|||
if (getTopicCallHash && getTopicResults && getTopicResults[getTopicCallHash]) { |
|||
setTopicAuthorAddress(getTopicResults[getTopicCallHash].value[0]); |
|||
setTopicAuthor(getTopicResults[getTopicCallHash].value[1]); |
|||
setTimestamp(getTopicResults[getTopicCallHash].value[2]); |
|||
setPostIds(getTopicResults[getTopicCallHash].value[3].map((postId) => parseInt(postId, 10))); |
|||
|
|||
const topicFound = topics |
|||
.find((topic) => topic.id === topicId); |
|||
|
|||
if (topicFound === undefined && userAddress !== getTopicResults[getTopicCallHash].value[0]) { |
|||
dispatch({ |
|||
type: FETCH_USER_DATABASE, |
|||
orbit, |
|||
dbName: 'topics', |
|||
userAddress: getTopicResults[getTopicCallHash].value[0], |
|||
}); |
|||
} |
|||
} |
|||
}, [dispatch, getTopicCallHash, getTopicResults, topicId, topics, userAddress]); |
|||
|
|||
useEffect(() => { |
|||
const topicFound = topics |
|||
.find((topic) => topic.id === topicId); |
|||
|
|||
if (topicFound) { |
|||
setTopicSubject(topicFound.subject); |
|||
} |
|||
}, [topicId, topics]); |
|||
|
|||
return ( |
|||
<div> |
|||
TODO |
|||
</div> |
|||
<Container id="topic-container" textAlign="center"> |
|||
<Dimmer.Dimmable |
|||
blurring |
|||
dimmed={topicAuthorAddress === null && topicAuthor === null && timestamp === null} |
|||
> |
|||
<Step.Group fluid> |
|||
<Step key="topic-header-step-user"> |
|||
<Icon name="user circle" size="big" inverted color="black" /> |
|||
<Step.Content> |
|||
<Step.Title> |
|||
{topicAuthor || ( |
|||
<Placeholder id="author-placeholder" inverted> |
|||
<Placeholder.Line length="full" /> |
|||
</Placeholder> |
|||
)} |
|||
</Step.Title> |
|||
</Step.Content> |
|||
</Step> |
|||
<Step key="topic-header-step-title"> |
|||
<Step.Content> |
|||
<Step.Title> |
|||
{topicSubject || ( |
|||
<Placeholder id="subject-placeholder"> |
|||
<Placeholder.Line length="full" /> |
|||
</Placeholder> |
|||
)} |
|||
</Step.Title> |
|||
<Step.Description> |
|||
{timestamp |
|||
? moment(timestamp * 1000).fromNow() |
|||
: ( |
|||
<Placeholder id="date-placeholder"> |
|||
<Placeholder.Line length="full" /> |
|||
</Placeholder> |
|||
)} |
|||
</Step.Description> |
|||
</Step.Content> |
|||
</Step> |
|||
</Step.Group> |
|||
</Dimmer.Dimmable> |
|||
<PostList postIds={postIds || []} loading={postIds === null} /> |
|||
</Container> |
|||
); |
|||
}; |
|||
|
|||
TopicView.propTypes = { |
|||
topicId: PropTypes.number.isRequired, |
|||
topicAuthorAddress: PropTypes.string, |
|||
topicAuthor: PropTypes.string, |
|||
timestamp: PropTypes.number, |
|||
postIds: PropTypes.arrayOf(PropTypes.number), |
|||
}; |
|||
|
|||
export default TopicView; |
|||
|
@ -0,0 +1,12 @@ |
|||
#author-placeholder { |
|||
width: 150px !important; |
|||
} |
|||
|
|||
#subject-placeholder { |
|||
width: 250px !important; |
|||
} |
|||
|
|||
#date-placeholder { |
|||
width: 150px !important; |
|||
margin: 0 auto; |
|||
} |
Loading…
Reference in new issue