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 React, { Children } from 'react'; |
||||
import { connect } from 'react-redux'; |
|
||||
|
|
||||
import { breezeConstants } from '@ezerous/breeze'; |
import { breezeConstants } from '@ezerous/breeze'; |
||||
|
import { useSelector } from 'react-redux'; |
||||
import LoadingComponent from './LoadingComponent'; |
import LoadingComponent from './LoadingComponent'; |
||||
|
|
||||
// CSS |
// CSS |
||||
import '../assets/css/loading-component.css'; |
import '../assets/css/loading-component.css'; |
||||
|
|
||||
class LoadingContainer extends Component { |
const LoadingContainer = ({ children }) => { |
||||
render() { |
const initializing = useSelector((state) => state.drizzleStatus.initializing); |
||||
const { |
const failed = useSelector((state) => state.drizzleStatus.failed); |
||||
web3: { |
const ipfsStatus = useSelector((state) => state.ipfs.status); |
||||
status, networkId, networkFailed, accountsFailed, |
const orbitStatus = useSelector((state) => state.orbit.status); |
||||
}, |
const web3Status = useSelector((state) => state.web3.status); |
||||
drizzleStatus: { |
const web3NetworkId = useSelector((state) => state.web3.networkId); |
||||
initializing, |
const web3NetworkFailed = useSelector((state) => state.web3.networkFailed); |
||||
failed, |
const web3AccountsFailed = useSelector((state) => state.web3.accountsFailed); |
||||
}, |
const contractInitialized = useSelector((state) => state.contracts.Forum.initialized); |
||||
contractInitialized, contractDeployed, ipfsStatus, orbitStatus, userFetched, children, |
const contractDeployed = useSelector((state) => state.contracts.Forum.deployed); |
||||
} = this.props; |
const userFetched = useSelector((state) => state.user.address); |
||||
|
|
||||
if ((status === 'initializing' || !networkId) |
if ((web3Status === 'initializing' || !web3NetworkId) |
||||
&& !networkFailed) { |
&& !web3NetworkFailed) { |
||||
return ( |
return ( |
||||
<LoadingComponent |
<LoadingComponent |
||||
title="Connecting to the Ethereum network..." |
title="Connecting to the Ethereum network..." |
||||
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account." |
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account." |
||||
imageType="ethereum" |
imageType="ethereum" |
||||
progress={20} |
progress={20} |
||||
progressType="indicating" |
progressType="indicating" |
||||
/> |
/> |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
if (status === 'failed' || networkFailed) { |
if (web3Status === 'failed' || web3NetworkFailed) { |
||||
return ( |
return ( |
||||
<LoadingComponent |
<LoadingComponent |
||||
title="No connection to the Ethereum network!" |
title="No connection to the Ethereum network!" |
||||
message="Please make sure that:" |
message="Please make sure that:" |
||||
message_list={['MetaMask is unlocked and pointed to the correct, available network', |
message_list={['MetaMask is unlocked and pointed to the correct, available network', |
||||
'The app has been granted the right to connect to your account']} |
'The app has been granted the right to connect to your account']} |
||||
imageType="ethereum" |
imageType="ethereum" |
||||
progress={20} |
progress={20} |
||||
progressType="error" |
progressType="error" |
||||
/> |
/> |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
if (status === 'initialized' && accountsFailed) { |
if (web3Status === 'initialized' && web3AccountsFailed) { |
||||
return ( |
return ( |
||||
<LoadingComponent |
<LoadingComponent |
||||
title="We can't find any Ethereum accounts!" |
title="We can't find any Ethereum accounts!" |
||||
message="Please make sure that MetaMask is unlocked." |
message="Please make sure that MetaMask is unlocked." |
||||
imageType="ethereum" |
imageType="ethereum" |
||||
progress={20} |
progress={20} |
||||
progressType="error" |
progressType="error" |
||||
/> |
/> |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
if (initializing |
if (initializing |
||||
|| (!failed && !contractInitialized && contractDeployed)) { |
|| (!failed && !contractInitialized && contractDeployed)) { |
||||
return ( |
return ( |
||||
<LoadingComponent |
<LoadingComponent |
||||
title="Initializing contracts..." |
title="Initializing contracts..." |
||||
message="" |
message="" |
||||
imageType="ethereum" |
imageType="ethereum" |
||||
progress={40} |
progress={40} |
||||
progressType="indicating" |
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_INITIALIZING) { |
if (!contractDeployed) { |
||||
return ( |
return ( |
||||
<LoadingComponent |
<LoadingComponent |
||||
title="Initializing IPFS..." |
title="No contracts found on the current network!" |
||||
message="" |
message="Please make sure that you are connected to the correct network and the contracts are deployed." |
||||
imageType="ipfs" |
imageType="ethereum" |
||||
progress={60} |
progress={40} |
||||
progressType="indicating" |
progressType="error" |
||||
/> |
/> |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
if (ipfsStatus === breezeConstants.STATUS_FAILED) { |
if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) { |
||||
return ( |
return ( |
||||
<LoadingComponent |
<LoadingComponent |
||||
title="IPFS initialization failed!" |
title="Initializing IPFS..." |
||||
message="" |
message="" |
||||
imageType="ipfs" |
imageType="ipfs" |
||||
progress={60} |
progress={60} |
||||
progressType="error" |
progressType="indicating" |
||||
/> |
/> |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
if (orbitStatus === breezeConstants.STATUS_INITIALIZING) { |
if (ipfsStatus === breezeConstants.STATUS_FAILED) { |
||||
const message = process.env.NODE_ENV === 'development' |
return ( |
||||
? 'If needed, please sign the transaction in MetaMask to create the databases.' |
<LoadingComponent |
||||
: 'Please sign the transaction in MetaMask to create the databases.'; |
title="IPFS initialization failed!" |
||||
return ( |
message="" |
||||
<LoadingComponent |
imageType="ipfs" |
||||
title="Preparing OrbitDB..." |
progress={60} |
||||
message={message} |
progressType="error" |
||||
imageType="orbit" |
/> |
||||
progress={80} |
); |
||||
progressType="indicating" |
} |
||||
/> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
if (orbitStatus === breezeConstants.STATUS_FAILED) { |
if (orbitStatus === breezeConstants.STATUS_INITIALIZING) { |
||||
return ( |
const message = process.env.NODE_ENV === 'development' |
||||
<LoadingComponent |
? 'If needed, please sign the transaction in MetaMask to create the databases.' |
||||
title="OrbitDB initialization failed!" |
: 'Please sign the transaction in MetaMask to create the databases.'; |
||||
message="" |
return ( |
||||
imageType="orbit" |
<LoadingComponent |
||||
progress={80} |
title="Preparing OrbitDB..." |
||||
progressType="error" |
message={message} |
||||
/> |
imageType="orbit" |
||||
); |
progress={80} |
||||
} |
progressType="indicating" |
||||
|
/> |
||||
|
); |
||||
|
} |
||||
|
|
||||
if (!userFetched) { |
if (orbitStatus === breezeConstants.STATUS_FAILED) { |
||||
return ( |
return ( |
||||
<LoadingComponent |
<LoadingComponent |
||||
title="Loading dapp..." |
title="OrbitDB initialization failed!" |
||||
message="" |
message="" |
||||
imageType="app" |
imageType="orbit" |
||||
progress={90} |
progress={80} |
||||
progressType="indicating" |
progressType="error" |
||||
/> |
/> |
||||
); |
); |
||||
} |
} |
||||
|
|
||||
return Children.only(children); |
if (!userFetched) { |
||||
|
return ( |
||||
|
<LoadingComponent |
||||
|
title="Loading dapp..." |
||||
|
message="" |
||||
|
imageType="app" |
||||
|
progress={90} |
||||
|
progressType="indicating" |
||||
|
/> |
||||
|
); |
||||
} |
} |
||||
} |
|
||||
|
|
||||
const mapStateToProps = (state) => ({ |
return Children.only(children); |
||||
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, |
|
||||
}); |
|
||||
|
|
||||
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 React, { |
||||
import { List } from 'semantic-ui-react'; |
memo, useEffect, useMemo, useState, |
||||
|
} from 'react'; |
||||
|
import { |
||||
|
Dimmer, Grid, List, Placeholder, |
||||
|
} from 'semantic-ui-react'; |
||||
import PropTypes from 'prop-types'; |
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 { useDispatch, useSelector } from 'react-redux'; |
||||
import AppContext from '../../AppContext'; |
|
||||
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; |
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; |
||||
|
import { breeze } from '../../../redux/store'; |
||||
|
import './styles.css'; |
||||
|
|
||||
|
const { orbit } = breeze; |
||||
|
|
||||
const TopicListRow = (props) => { |
const TopicListRow = (props) => { |
||||
const { topicData, topicId } = props; |
const { id: topicId, topicCallHash, loading } = props; |
||||
const { breeze: { orbit } } = useContext(AppContext.Context); |
const getTopicResults = useSelector((state) => state.contracts.Forum.getTopic); |
||||
const [topicSubject, setTopicSubject] = useState(); |
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 userAddress = useSelector((state) => state.user.address); |
||||
const topics = useSelector((state) => state.orbitData.topics); |
const topics = useSelector((state) => state.orbitData.topics); |
||||
const dispatch = useDispatch(); |
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(() => { |
useEffect(() => { |
||||
if (userAddress !== topicData.userAddress) { |
if (topicAuthorAddress && userAddress !== topicAuthorAddress) { |
||||
dispatch({ |
dispatch({ |
||||
type: FETCH_USER_DATABASE, |
type: FETCH_USER_DATABASE, |
||||
orbit, |
orbit, |
||||
userAddress: topicData.userAddress, |
dbName: 'topics', |
||||
|
userAddress: topicAuthorAddress, |
||||
}); |
}); |
||||
} |
} |
||||
}, [dispatch, orbit, topicData.userAddress, topicId, userAddress]); |
}, [dispatch, topicAuthorAddress, userAddress]); |
||||
|
|
||||
useEffect(() => { |
useEffect(() => { |
||||
const topicFound = topics |
const topicFound = topics |
||||
.find((topic) => topic.id === topicId); |
.find((topic) => topic.id === topicId); |
||||
|
|
||||
if (topicFound) { |
if (topicFound) { |
||||
setTopicSubject(topicFound); |
setTopicSubject(topicFound.subject); |
||||
} |
} |
||||
}, [topicId, topics]); |
}, [topicId, topics]); |
||||
|
|
||||
return ( |
return useMemo(() => { |
||||
<> |
const handleTopicClick = () => { |
||||
<List.Header> |
history.push(`/topics/${topicId}`); |
||||
<List.Icon name="right triangle" /> |
}; |
||||
{topicSubject && topicSubject.subject} |
|
||||
</List.Header> |
return ( |
||||
<List.Content> |
<Dimmer.Dimmable as={List.Item} onClick={handleTopicClick} blurring dimmed={loading} className="list-item"> |
||||
{topicData.username} |
<List.Icon name="user circle" size="big" inverted color="black" verticalAlign="middle" /> |
||||
{topicData.numberOfReplies} |
<List.Content> |
||||
{' '} |
<List.Header> |
||||
replies |
<Grid> |
||||
timestamp |
<Grid.Column floated="left" width={14}> |
||||
</List.Content> |
{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({ |
TopicListRow.defaultProps = { |
||||
userAddress: PropTypes.string.isRequired, |
loading: false, |
||||
username: PropTypes.string.isRequired, |
}; |
||||
timestamp: PropTypes.number.isRequired, |
|
||||
numberOfReplies: PropTypes.number.isRequired, |
|
||||
}); |
|
||||
|
|
||||
TopicListRow.propTypes = { |
TopicListRow.propTypes = { |
||||
topicData: TopicData.isRequired, |
id: PropTypes.number.isRequired, |
||||
topicId: 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{ |
#topic-list{ |
||||
height: 100%; |
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, { |
import React, { |
||||
useContext, useEffect, useMemo, useState, |
memo, useEffect, useMemo, useState, |
||||
} from 'react'; |
} from 'react'; |
||||
import { Container } from 'semantic-ui-react'; |
import { Container } from 'semantic-ui-react'; |
||||
import { useSelector } from 'react-redux'; |
import { useSelector } from 'react-redux'; |
||||
import AppContext from '../../components/AppContext'; |
|
||||
import Board from './Board'; |
import Board from './Board'; |
||||
import './styles.css'; |
import './styles.css'; |
||||
|
import { drizzle } from '../../redux/store'; |
||||
|
|
||||
|
const { contracts: { Forum: { methods: { getNumberOfTopics } } } } = drizzle; |
||||
|
|
||||
const Home = () => { |
const Home = () => { |
||||
const { drizzle: { contracts: { Forum: { methods: { getNumberOfTopics } } } } } = useContext(AppContext.Context); |
|
||||
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState(''); |
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState(''); |
||||
const getNumberOfTopicsResults = useSelector((state) => state.contracts.Forum.getNumberOfTopics); |
const getNumberOfTopicsResults = useSelector((state) => state.contracts.Forum.getNumberOfTopics); |
||||
|
|
||||
useEffect(() => { |
useEffect(() => { |
||||
setNumberOfTopicsCallHash(getNumberOfTopics.cacheCall()); |
setNumberOfTopicsCallHash(getNumberOfTopics.cacheCall()); |
||||
}, [getNumberOfTopics]); |
}, []); |
||||
|
|
||||
const numberOfTopics = useMemo(() => (getNumberOfTopicsResults[numberOfTopicsCallHash] !== undefined |
const numberOfTopics = useMemo(() => (getNumberOfTopicsResults[numberOfTopicsCallHash] !== undefined |
||||
? parseInt(getNumberOfTopicsResults[numberOfTopicsCallHash].value, 10) |
? parseInt(getNumberOfTopicsResults[numberOfTopicsCallHash].value, 10) |
||||
: null), |
: null), |
||||
[getNumberOfTopicsResults, numberOfTopicsCallHash]); |
[getNumberOfTopicsResults, numberOfTopicsCallHash]); |
||||
|
|
||||
return ( |
return useMemo(() => ( |
||||
<Container id="home-container" textAlign="center"> |
<Container id="home-container" textAlign="center"> |
||||
{numberOfTopics !== null && <Board numberOfTopics={numberOfTopics} />} |
{numberOfTopics !== null && <Board numberOfTopics={numberOfTopics} />} |
||||
</Container> |
</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 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 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 ( |
return ( |
||||
<div> |
<Container id="topic-container" textAlign="center"> |
||||
TODO |
<Dimmer.Dimmable |
||||
</div> |
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 = { |
TopicView.propTypes = { |
||||
topicId: PropTypes.number.isRequired, |
topicId: PropTypes.number.isRequired, |
||||
|
topicAuthorAddress: PropTypes.string, |
||||
|
topicAuthor: PropTypes.string, |
||||
|
timestamp: PropTypes.number, |
||||
|
postIds: PropTypes.arrayOf(PropTypes.number), |
||||
}; |
}; |
||||
|
|
||||
export default TopicView; |
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