diff --git a/packages/concordia-app/package.json b/packages/concordia-app/package.json index f2bcbf9..26b8b62 100644 --- a/packages/concordia-app/package.json +++ b/packages/concordia-app/package.json @@ -34,6 +34,7 @@ "i18next-browser-languagedetector": "^6.0.1", "i18next-http-backend": "^1.0.21", "lodash": "^4.17.20", + "moment": "^2.29.1", "prop-types": "~15.7.2", "react": "~16.13.1", "react-dom": "~16.13.1", diff --git a/packages/concordia-app/public/locales/en/translation.json b/packages/concordia-app/public/locales/en/translation.json index c55754f..c4834b6 100644 --- a/packages/concordia-app/public/locales/en/translation.json +++ b/packages/concordia-app/public/locales/en/translation.json @@ -2,6 +2,8 @@ "board.header.no.topics.message": "There are no topics yet!", "board.sub.header.no.topics.guest": "Sign up and be the first to post.", "board.sub.header.no.topics.user": "Be the first to post.", + "post.list.row.author.date": "Posted by {{author}}, {{timeAgo}}", + "post.list.row.post.id": "#{{id}}", "register.card.header": "Sign Up", "register.form.button.back": "Back", "register.form.button.guest": "Continue as guest", @@ -15,9 +17,12 @@ "topbar.button.create.topic": "Create topic", "topbar.button.profile": "Profile", "topbar.button.register": "Sign Up", - "topic.create.form.subject.field.label": "Topic subject", - "topic.create.form.subject.field.placeholder": "Subject", "topic.create.form.message.field.label": "First post message", "topic.create.form.message.field.placeholder": "Message", - "topic.create.form.post.button": "Post" + "topic.create.form.post.button": "Post", + "topic.create.form.subject.field.label": "Topic subject", + "topic.create.form.subject.field.placeholder": "Subject", + "topic.list.row.author.date": "Created by {{author}}, {{timeAgo}}", + "topic.list.row.number.of.replies": "{{numberOfReplies}} replies", + "topic.list.row.topic.id": "#{{id}}" } \ No newline at end of file diff --git a/packages/concordia-app/src/components/LoadingContainer.jsx b/packages/concordia-app/src/components/LoadingContainer.jsx index 0260132..f8cda5f 100644 --- a/packages/concordia-app/src/components/LoadingContainer.jsx +++ b/packages/concordia-app/src/components/LoadingContainer.jsx @@ -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 ( - - ); - } +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 ( + + ); + } - if (status === 'failed' || networkFailed) { - return ( - - ); - } + if (web3Status === 'failed' || web3NetworkFailed) { + return ( + + ); + } - if (status === 'initialized' && accountsFailed) { - return ( - - ); - } + if (web3Status === 'initialized' && web3AccountsFailed) { + return ( + + ); + } - if (initializing + if (initializing || (!failed && !contractInitialized && contractDeployed)) { - return ( - - ); - } - - if (!contractDeployed) { - return ( - - ); - } + return ( + + ); + } - if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) { - return ( - - ); - } + if (!contractDeployed) { + return ( + + ); + } - if (ipfsStatus === breezeConstants.STATUS_FAILED) { - return ( - - ); - } + if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) { + return ( + + ); + } - 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 ( - - ); - } + if (ipfsStatus === breezeConstants.STATUS_FAILED) { + return ( + + ); + } - if (orbitStatus === breezeConstants.STATUS_FAILED) { - return ( - - ); - } + 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 ( + + ); + } - if (!userFetched) { - return ( - - ); - } + if (orbitStatus === breezeConstants.STATUS_FAILED) { + return ( + + ); + } - return Children.only(children); + if (!userFetched) { + return ( + + ); } -} -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; diff --git a/packages/concordia-app/src/components/Placeholder/index.jsx b/packages/concordia-app/src/components/Placeholder/index.jsx deleted file mode 100644 index 5c9394e..0000000 --- a/packages/concordia-app/src/components/Placeholder/index.jsx +++ /dev/null @@ -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 ( - <> - - - topicSubject - - - username - Number of Replies - timestamp - - - ); - case PLACEHOLDER_TYPE_POST: - return ( -
LOADING POST
- ); - default: - return
; - } -}; - -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; diff --git a/packages/concordia-app/src/components/PostList/PostListRow/index.jsx b/packages/concordia-app/src/components/PostList/PostListRow/index.jsx new file mode 100644 index 0000000..b03d475 --- /dev/null +++ b/packages/concordia-app/src/components/PostList/PostListRow/index.jsx @@ -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(() => ( + + + + + + + {postSubject !== null + ? postSubject + : } + + + + {t('post.list.row.post.id', { id: postId })} + + + + + + + + {postAuthor !== null && timeAgo !== null + ? t('post.list.row.author.date', { author: postAuthor, timeAgo }) + : } + + + + + + ), [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); diff --git a/packages/concordia-app/src/components/PostList/PostListRow/styles.css b/packages/concordia-app/src/components/PostList/PostListRow/styles.css new file mode 100644 index 0000000..0058f79 --- /dev/null +++ b/packages/concordia-app/src/components/PostList/PostListRow/styles.css @@ -0,0 +1,8 @@ +.post-metadata { + font-size: 12px !important; + font-weight: initial; +} + +.list-item { + text-align: start; +} diff --git a/packages/concordia-app/src/components/PostList/index.jsx b/packages/concordia-app/src/components/PostList/index.jsx new file mode 100644 index 0000000..d961233 --- /dev/null +++ b/packages/concordia-app/src/components/PostList/index.jsx @@ -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 ( + + ); + }); + }, [getPostCallHashes, loading, postIds]); + + return ( + + + {posts} + + ); +}; + +PostList.propTypes = { + postIds: PropTypes.arrayOf(PropTypes.number).isRequired, + loading: PropTypes.bool, +}; + +export default PostList; diff --git a/packages/concordia-app/src/components/PostList/styles.css b/packages/concordia-app/src/components/PostList/styles.css new file mode 100644 index 0000000..baf2856 --- /dev/null +++ b/packages/concordia-app/src/components/PostList/styles.css @@ -0,0 +1,3 @@ +#post-list{ + height: 100%; +} diff --git a/packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx b/packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx index 2fea75c..758cb68 100644 --- a/packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx +++ b/packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx @@ -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 ( - <> - - - {topicSubject && topicSubject.subject} - - - {topicData.username} - {topicData.numberOfReplies} - {' '} - replies - timestamp - - - ); + return useMemo(() => { + const handleTopicClick = () => { + history.push(`/topics/${topicId}`); + }; + + return ( + + + + + + + {topicSubject !== null + ? topicSubject + : } + + + + {t('topic.list.row.topic.id', { id: topicId })} + + + + + + + + {topicAuthor !== null && timeAgo !== null + ? t('topic.list.row.author.date', { author: topicAuthor, timeAgo }) + : } + + + {numberOfReplies !== null + ? ( + + {t('topic.list.row.number.of.replies', { numberOfReplies })} + + ) + : } + + + + + + ); + }, [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); diff --git a/packages/concordia-app/src/components/TopicList/TopicListRow/styles.css b/packages/concordia-app/src/components/TopicList/TopicListRow/styles.css new file mode 100644 index 0000000..8e808b6 --- /dev/null +++ b/packages/concordia-app/src/components/TopicList/TopicListRow/styles.css @@ -0,0 +1,8 @@ +.topic-metadata { + font-size: 12px !important; + font-weight: initial; +} + +.list-item { + text-align: start; +} diff --git a/packages/concordia-app/src/components/TopicList/index.jsx b/packages/concordia-app/src/components/TopicList/index.jsx index 4450ccd..511ae2b 100644 --- a/packages/concordia-app/src/components/TopicList/index.jsx +++ b/packages/concordia-app/src/components/TopicList/index.jsx @@ -1,85 +1,53 @@ import React, { - useCallback, - useContext, useEffect, useMemo, useState, + useEffect, useMemo, useState, } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { List } from 'semantic-ui-react'; -import { useHistory } from 'react-router'; -import AppContext from '../AppContext'; import TopicListRow from './TopicListRow'; -import { PLACEHOLDER_TYPE_TOPIC } from '../../constants/PlaceholderTypes'; -import Placeholder from '../Placeholder'; -import './styles.css'; +import { drizzle } from '../../redux/store'; + +const { contracts: { Forum: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle; const TopicList = (props) => { const { topicIds } = props; - const { drizzle: { contracts: { Forum: { methods: { getTopic } } } } } = useContext(AppContext.Context); const [getTopicCallHashes, setGetTopicCallHashes] = useState([]); - const getTopicResults = useSelector((state) => state.contracts.Forum.getTopic); - const drizzleStatus = useSelector((state) => state.drizzleStatus); - const history = useHistory(); + const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized); + const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed); useEffect(() => { - // TODO: is the drizzleStatus check necessary? - if (drizzleStatus.initialized && !drizzleStatus.failed) { - const newTopicPosted = topicIds - .some((topicId) => !getTopicCallHashes + if (drizzleInitialized && !drizzleInitializationFailed) { + const newTopicsFound = topicIds + .filter((topicId) => !getTopicCallHashes .map((getTopicCallHash) => getTopicCallHash.id) .includes(topicId)); - if (newTopicPosted) { - setGetTopicCallHashes(topicIds.map((topicId) => { - const foundGetTopicCallHash = getTopicCallHashes.find((getTopicCallHash) => getTopicCallHash.id === topicId); - - if (foundGetTopicCallHash !== undefined) { - return ({ ...foundGetTopicCallHash }); - } - - return ({ - id: topicId, - hash: getTopic.cacheCall(topicId), - }); - })); + if (newTopicsFound.length > 0) { + setGetTopicCallHashes([ + ...getTopicCallHashes, + ...newTopicsFound + .map((topicId) => ({ + id: topicId, + hash: getTopicChainData(topicId), + })), + ]); } } - }, [drizzleStatus.failed, drizzleStatus.initialized, getTopic, getTopicCallHashes, topicIds]); - - const handleTopicClick = useCallback((topicId) => { - history.push(`/topics/${topicId}`); - }, [history]); + }, [drizzleInitializationFailed, drizzleInitialized, getTopicCallHashes, topicIds]); const topics = useMemo(() => topicIds .map((topicId) => { - const getTopicHash = getTopicCallHashes.find((getTopicCallHash) => getTopicCallHash.id === topicId); - - if (getTopicHash && getTopicResults[getTopicHash.hash] !== undefined) { - const topicData = { - userAddress: getTopicResults[getTopicHash.hash].value[0], - username: getTopicResults[getTopicHash.hash].value[1], - timestamp: getTopicResults[getTopicHash.hash].value[2] * 1000, - numberOfReplies: getTopicResults[getTopicHash.hash].value[3].length, - }; - - return ( - handleTopicClick(topicId)}> - - - ); - } + const topicHash = getTopicCallHashes.find((getTopicCallHash) => getTopicCallHash.id === topicId); return ( - handleTopicClick(topicId)}> - - + ); - }), [getTopicCallHashes, getTopicResults, handleTopicClick, topicIds]); + }), [getTopicCallHashes, topicIds]); return ( diff --git a/packages/concordia-app/src/components/TopicList/styles.css b/packages/concordia-app/src/components/TopicList/styles.css index 5e461d1..ac3c53c 100644 --- a/packages/concordia-app/src/components/TopicList/styles.css +++ b/packages/concordia-app/src/components/TopicList/styles.css @@ -1,7 +1,3 @@ #topic-list{ height: 100%; } - -.list-item { - text-align: start; -} \ No newline at end of file diff --git a/packages/concordia-app/src/index.jsx b/packages/concordia-app/src/index.jsx index 536fd4c..0e89763 100644 --- a/packages/concordia-app/src/index.jsx +++ b/packages/concordia-app/src/index.jsx @@ -1,24 +1,14 @@ import './utils/wdyr'; import React, { Suspense } from 'react'; import { render } from 'react-dom'; -import { Drizzle } from '@ezerous/drizzle'; -import { Breeze } from '@ezerous/breeze'; import App from './App'; import store from './redux/store'; -import AppContext from './components/AppContext'; -import drizzleOptions from './options/drizzleOptions'; -import breezeOptions from './options/breezeOptions'; import * as serviceWorker from './utils/serviceWorker'; import LoadingScreen from './components/LoadingScreen'; -const drizzle = new Drizzle(drizzleOptions, store); -const breeze = new Breeze(breezeOptions, store); - render( }> - - - + , document.getElementById('root'), ); diff --git a/packages/concordia-app/src/layouts/MainLayout/index.jsx b/packages/concordia-app/src/layouts/MainLayout/index.jsx index 0864457..6236c70 100644 --- a/packages/concordia-app/src/layouts/MainLayout/index.jsx +++ b/packages/concordia-app/src/layouts/MainLayout/index.jsx @@ -1,6 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; - import MainLayoutMenu from './MainLayoutMenu'; const MainLayout = (props) => { diff --git a/packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js b/packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js index dd5b70c..a7713d9 100644 --- a/packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js +++ b/packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js @@ -10,9 +10,9 @@ import { import determineKVAddress from '../../utils/orbitUtils'; import { FETCH_USER_DATABASE, UPDATE_ORBIT_DATA } from '../actions/peerDbReplicationActions'; -function* fetchUserDb({ orbit, userAddress }) { +function* fetchUserDb({ orbit, userAddress, dbName }) { const peerDbAddress = yield call(determineKVAddress, { - orbit, dbName: 'topics', userAddress, + orbit, dbName, userAddress, }); yield put(addOrbitDB({ address: peerDbAddress, type: 'keyvalue' })); diff --git a/packages/concordia-app/src/redux/store.js b/packages/concordia-app/src/redux/store.js index d0f75f0..8ed1b9e 100644 --- a/packages/concordia-app/src/redux/store.js +++ b/packages/concordia-app/src/redux/store.js @@ -1,11 +1,14 @@ import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; -import { drizzleReducers, drizzleMiddlewares, generateContractsInitialState } from '@ezerous/drizzle'; -import { breezeReducers } from '@ezerous/breeze'; +import { + drizzleReducers, drizzleMiddlewares, generateContractsInitialState, Drizzle, +} from '@ezerous/drizzle'; +import { Breeze, breezeReducers } from '@ezerous/breeze'; import createSagaMiddleware from 'redux-saga'; import userReducer from './reducers/userReducer'; import rootSaga from './sagas/rootSaga'; import drizzleOptions from '../options/drizzleOptions'; import peerDbReplicationReducer from './reducers/peerDbReplicationReducer'; +import breezeOptions from '../options/breezeOptions'; const initialState = { contracts: generateContractsInitialState(drizzleOptions), @@ -24,5 +27,8 @@ const store = configureStore({ preloadedState: initialState, }); +export const drizzle = new Drizzle(drizzleOptions, store); +export const breeze = new Breeze(breezeOptions, store); + sagaMiddleware.run(rootSaga); export default store; diff --git a/packages/concordia-app/src/utils/styles.debug.css b/packages/concordia-app/src/utils/styles.debug.css new file mode 100644 index 0000000..2488391 --- /dev/null +++ b/packages/concordia-app/src/utils/styles.debug.css @@ -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 +} \ No newline at end of file diff --git a/packages/concordia-app/src/views/Home/index.jsx b/packages/concordia-app/src/views/Home/index.jsx index a919f97..ce9decf 100644 --- a/packages/concordia-app/src/views/Home/index.jsx +++ b/packages/concordia-app/src/views/Home/index.jsx @@ -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(() => ( {numberOfTopics !== null && } - ); + ), [numberOfTopics]); }; -export default Home; +export default memo(Home); diff --git a/packages/concordia-app/src/views/Topic/TopicCreate/index.jsx b/packages/concordia-app/src/views/Topic/TopicCreate/index.jsx index 2870d1d..4df7a2e 100644 --- a/packages/concordia-app/src/views/Topic/TopicCreate/index.jsx +++ b/packages/concordia-app/src/views/Topic/TopicCreate/index.jsx @@ -1,5 +1,5 @@ import React, { - useCallback, useContext, useEffect, useState, + useCallback, useEffect, useState, } from 'react'; import { Button, Container, Form, Icon, Input, TextArea, @@ -7,30 +7,16 @@ import { import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router'; import { useSelector } from 'react-redux'; -import AppContext from '../../../components/AppContext'; import './styles.css'; +import { drizzle, breeze } from '../../../redux/store'; + +const { contracts: { Forum: { methods: { createTopic } } } } = drizzle; +const { orbit: { stores } } = breeze; const TopicCreate = (props) => { const { account } = props; - - const { - drizzle: { - contracts: { - Forum: { - methods: { createTopic }, - }, - }, - }, - breeze: { - orbit: { - stores, - }, - }, - } = useContext(AppContext.Context); - const transactionStack = useSelector((state) => state.transactionStack); const transactions = useSelector((state) => state.transactions); - const [subjectInput, setSubjectInput] = useState(''); const [messageInput, setMessageInput] = useState(''); const [topicSubjectInputEmptySubmit, setTopicSubjectInputEmptySubmit] = useState(false); @@ -95,9 +81,7 @@ const TopicCreate = (props) => { }); } } - }, [ - transactions, transactionStack, history, posting, createTopicCacheSendStackId, subjectInput, messageInput, stores, - ]); + }, [createTopicCacheSendStackId, history, messageInput, posting, subjectInput, transactionStack, transactions]); const validateAndPost = useCallback(() => { if (subjectInput === '') { @@ -112,7 +96,7 @@ const TopicCreate = (props) => { setPosting(true); setCreateTopicCacheSendStackId(createTopic.cacheSend(...[], { from: account })); - }, [account, createTopic, messageInput, subjectInput]); + }, [account, messageInput, subjectInput]); return ( diff --git a/packages/concordia-app/src/views/Topic/TopicView/index.jsx b/packages/concordia-app/src/views/Topic/TopicView/index.jsx index 56fd2dc..c5e4687 100644 --- a/packages/concordia-app/src/views/Topic/TopicView/index.jsx +++ b/packages/concordia-app/src/views/Topic/TopicView/index.jsx @@ -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 ( -
- TODO -
+ + + + + + + + {topicAuthor || ( + + + + )} + + + + + + + {topicSubject || ( + + + + )} + + + {timestamp + ? moment(timestamp * 1000).fromNow() + : ( + + + + )} + + + + + + + ); }; TopicView.propTypes = { topicId: PropTypes.number.isRequired, + topicAuthorAddress: PropTypes.string, + topicAuthor: PropTypes.string, + timestamp: PropTypes.number, + postIds: PropTypes.arrayOf(PropTypes.number), }; export default TopicView; diff --git a/packages/concordia-app/src/views/Topic/TopicView/styles.css b/packages/concordia-app/src/views/Topic/TopicView/styles.css new file mode 100644 index 0000000..8cd3cd2 --- /dev/null +++ b/packages/concordia-app/src/views/Topic/TopicView/styles.css @@ -0,0 +1,12 @@ +#author-placeholder { + width: 150px !important; +} + +#subject-placeholder { + width: 250px !important; +} + +#date-placeholder { + width: 150px !important; + margin: 0 auto; +} \ No newline at end of file diff --git a/packages/concordia-app/src/views/Topic/index.jsx b/packages/concordia-app/src/views/Topic/index.jsx index 9f5b033..0e1900f 100644 --- a/packages/concordia-app/src/views/Topic/index.jsx +++ b/packages/concordia-app/src/views/Topic/index.jsx @@ -12,7 +12,7 @@ const Topic = () => { ) : ( - + ); }; diff --git a/yarn.lock b/yarn.lock index af511d6..74c45ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11467,6 +11467,11 @@ mock-fs@^4.1.0: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.13.0.tgz#31c02263673ec3789f90eb7b6963676aa407a598" integrity sha512-DD0vOdofJdoaRNtnWcrXe6RQbpHkPPmtqGq14uRX0F8ZKJ5nv89CVTYl/BZdppDxBDaV0hl75htg3abpEWlPZA== +moment@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + mortice@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mortice/-/mortice-2.0.0.tgz#7be171409c2115561ba3fc035e4527f9082eefde"