Browse Source

Merge branch 'implement-ui' into 'peer-db-replication-sagas'

# Conflicts:
#   packages/concordia-app/package.json
develop
Apostolos Fanakis 4 years ago
parent
commit
7dae198628
  1. 1
      packages/concordia-app/package.json
  2. 11
      packages/concordia-app/public/locales/en/translation.json
  3. 57
      packages/concordia-app/src/components/LoadingContainer.jsx
  4. 49
      packages/concordia-app/src/components/Placeholder/index.jsx
  5. 103
      packages/concordia-app/src/components/PostList/PostListRow/index.jsx
  6. 8
      packages/concordia-app/src/components/PostList/PostListRow/styles.css
  7. 70
      packages/concordia-app/src/components/PostList/index.jsx
  8. 3
      packages/concordia-app/src/components/PostList/styles.css
  9. 112
      packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx
  10. 8
      packages/concordia-app/src/components/TopicList/TopicListRow/styles.css
  11. 80
      packages/concordia-app/src/components/TopicList/index.jsx
  12. 4
      packages/concordia-app/src/components/TopicList/styles.css
  13. 10
      packages/concordia-app/src/index.jsx
  14. 1
      packages/concordia-app/src/layouts/MainLayout/index.jsx
  15. 4
      packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js
  16. 10
      packages/concordia-app/src/redux/store.js
  17. 71
      packages/concordia-app/src/utils/styles.debug.css
  18. 15
      packages/concordia-app/src/views/Home/index.jsx
  19. 30
      packages/concordia-app/src/views/Topic/TopicCreate/index.jsx
  20. 124
      packages/concordia-app/src/views/Topic/TopicView/index.jsx
  21. 12
      packages/concordia-app/src/views/Topic/TopicView/styles.css
  22. 2
      packages/concordia-app/src/views/Topic/index.jsx
  23. 5
      yarn.lock

1
packages/concordia-app/package.json

@ -34,6 +34,7 @@
"i18next-browser-languagedetector": "^6.0.1", "i18next-browser-languagedetector": "^6.0.1",
"i18next-http-backend": "^1.0.21", "i18next-http-backend": "^1.0.21",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"moment": "^2.29.1",
"prop-types": "~15.7.2", "prop-types": "~15.7.2",
"react": "~16.13.1", "react": "~16.13.1",
"react-dom": "~16.13.1", "react-dom": "~16.13.1",

11
packages/concordia-app/public/locales/en/translation.json

@ -2,6 +2,8 @@
"board.header.no.topics.message": "There are no topics yet!", "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.guest": "Sign up and be the first to post.",
"board.sub.header.no.topics.user": "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.card.header": "Sign Up",
"register.form.button.back": "Back", "register.form.button.back": "Back",
"register.form.button.guest": "Continue as guest", "register.form.button.guest": "Continue as guest",
@ -15,9 +17,12 @@
"topbar.button.create.topic": "Create topic", "topbar.button.create.topic": "Create topic",
"topbar.button.profile": "Profile", "topbar.button.profile": "Profile",
"topbar.button.register": "Sign Up", "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.label": "First post message",
"topic.create.form.message.field.placeholder": "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}}"
} }

57
packages/concordia-app/src/components/LoadingContainer.jsx

@ -1,28 +1,26 @@
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..."
@ -34,7 +32,7 @@ class LoadingContainer extends Component {
); );
} }
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!"
@ -48,7 +46,7 @@ class LoadingContainer extends Component {
); );
} }
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!"
@ -149,19 +147,6 @@ class LoadingContainer extends Component {
} }
return Children.only(children); return Children.only(children);
} };
}
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,
});
export default connect(mapStateToProps)(LoadingContainer); export default LoadingContainer;

49
packages/concordia-app/src/components/Placeholder/index.jsx

@ -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;

103
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(() => (
<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);

8
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;
}

70
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 (
<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;

3
packages/concordia-app/src/components/PostList/styles.css

@ -0,0 +1,3 @@
#post-list{
height: 100%;
}

112
packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx

@ -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(() => { useEffect(() => {
if (userAddress !== topicData.userAddress) { 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 (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 useMemo(() => {
const handleTopicClick = () => {
history.push(`/topics/${topicId}`);
};
return ( 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> <List.Header>
<List.Icon name="right triangle" /> <Grid>
{topicSubject && topicSubject.subject} <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.Header>
<List.Content> <List.Description>
{topicData.username} <Grid verticalAlign="middle">
{topicData.numberOfReplies} <Grid.Column floated="left" width={14}>
{' '} {topicAuthor !== null && timeAgo !== null
replies ? t('topic.list.row.author.date', { author: topicAuthor, timeAgo })
timestamp : <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> </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);

8
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;
}

80
packages/concordia-app/src/components/TopicList/index.jsx

@ -1,85 +1,53 @@
import React, { import React, {
useCallback, useEffect, useMemo, useState,
useContext, useEffect, useMemo, useState,
} from 'react'; } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { List } from 'semantic-ui-react'; import { List } from 'semantic-ui-react';
import { useHistory } from 'react-router';
import AppContext from '../AppContext';
import TopicListRow from './TopicListRow'; import TopicListRow from './TopicListRow';
import { PLACEHOLDER_TYPE_TOPIC } from '../../constants/PlaceholderTypes'; import { drizzle } from '../../redux/store';
import Placeholder from '../Placeholder';
import './styles.css'; const { contracts: { Forum: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle;
const TopicList = (props) => { const TopicList = (props) => {
const { topicIds } = props; const { topicIds } = props;
const { drizzle: { contracts: { Forum: { methods: { getTopic } } } } } = useContext(AppContext.Context);
const [getTopicCallHashes, setGetTopicCallHashes] = useState([]); const [getTopicCallHashes, setGetTopicCallHashes] = useState([]);
const getTopicResults = useSelector((state) => state.contracts.Forum.getTopic); const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleStatus = useSelector((state) => state.drizzleStatus); const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
const history = useHistory();
useEffect(() => { useEffect(() => {
// TODO: is the drizzleStatus check necessary? if (drizzleInitialized && !drizzleInitializationFailed) {
if (drizzleStatus.initialized && !drizzleStatus.failed) { const newTopicsFound = topicIds
const newTopicPosted = topicIds .filter((topicId) => !getTopicCallHashes
.some((topicId) => !getTopicCallHashes
.map((getTopicCallHash) => getTopicCallHash.id) .map((getTopicCallHash) => getTopicCallHash.id)
.includes(topicId)); .includes(topicId));
if (newTopicPosted) { if (newTopicsFound.length > 0) {
setGetTopicCallHashes(topicIds.map((topicId) => { setGetTopicCallHashes([
const foundGetTopicCallHash = getTopicCallHashes.find((getTopicCallHash) => getTopicCallHash.id === topicId); ...getTopicCallHashes,
...newTopicsFound
if (foundGetTopicCallHash !== undefined) { .map((topicId) => ({
return ({ ...foundGetTopicCallHash });
}
return ({
id: topicId, id: topicId,
hash: getTopic.cacheCall(topicId), hash: getTopicChainData(topicId),
}); })),
})); ]);
} }
} }
}, [drizzleStatus.failed, drizzleStatus.initialized, getTopic, getTopicCallHashes, topicIds]); }, [drizzleInitializationFailed, drizzleInitialized, getTopicCallHashes, topicIds]);
const handleTopicClick = useCallback((topicId) => {
history.push(`/topics/${topicId}`);
}, [history]);
const topics = useMemo(() => topicIds const topics = useMemo(() => topicIds
.map((topicId) => { .map((topicId) => {
const getTopicHash = getTopicCallHashes.find((getTopicCallHash) => getTopicCallHash.id === topicId); const topicHash = 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 ( return (
<List.Item key={topicId} className="list-item" name={topicId} onClick={() => handleTopicClick(topicId)}>
<TopicListRow <TopicListRow
topicData={topicData} id={topicId}
topicId={topicId} key={topicId}
/> topicCallHash={topicHash && topicHash.hash}
</List.Item> loading={topicHash === undefined}
);
}
return (
<List.Item key={topicId} className="list-item" name={topicId} onClick={() => handleTopicClick(topicId)}>
<Placeholder
placeholderType={PLACEHOLDER_TYPE_TOPIC}
extra={{ topicId }}
/> />
</List.Item>
); );
}), [getTopicCallHashes, getTopicResults, handleTopicClick, topicIds]); }), [getTopicCallHashes, topicIds]);
return ( return (
<List selection divided id="topic-list" size="big"> <List selection divided id="topic-list" size="big">

4
packages/concordia-app/src/components/TopicList/styles.css

@ -1,7 +1,3 @@
#topic-list{ #topic-list{
height: 100%; height: 100%;
} }
.list-item {
text-align: start;
}

10
packages/concordia-app/src/index.jsx

@ -1,24 +1,14 @@
import './utils/wdyr'; import './utils/wdyr';
import React, { Suspense } from 'react'; import React, { Suspense } from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { Drizzle } from '@ezerous/drizzle';
import { Breeze } from '@ezerous/breeze';
import App from './App'; import App from './App';
import store from './redux/store'; 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 * as serviceWorker from './utils/serviceWorker';
import LoadingScreen from './components/LoadingScreen'; import LoadingScreen from './components/LoadingScreen';
const drizzle = new Drizzle(drizzleOptions, store);
const breeze = new Breeze(breezeOptions, store);
render( render(
<Suspense fallback={<LoadingScreen />}> <Suspense fallback={<LoadingScreen />}>
<AppContext.Provider drizzle={drizzle} breeze={breeze}>
<App store={store} /> <App store={store} />
</AppContext.Provider>
</Suspense>, </Suspense>,
document.getElementById('root'), document.getElementById('root'),
); );

1
packages/concordia-app/src/layouts/MainLayout/index.jsx

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import MainLayoutMenu from './MainLayoutMenu'; import MainLayoutMenu from './MainLayoutMenu';
const MainLayout = (props) => { const MainLayout = (props) => {

4
packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js

@ -10,9 +10,9 @@ import {
import determineKVAddress from '../../utils/orbitUtils'; import determineKVAddress from '../../utils/orbitUtils';
import { FETCH_USER_DATABASE, UPDATE_ORBIT_DATA } from '../actions/peerDbReplicationActions'; import { FETCH_USER_DATABASE, UPDATE_ORBIT_DATA } from '../actions/peerDbReplicationActions';
function* fetchUserDb({ orbit, userAddress }) { function* fetchUserDb({ orbit, userAddress, dbName }) {
const peerDbAddress = yield call(determineKVAddress, { const peerDbAddress = yield call(determineKVAddress, {
orbit, dbName: 'topics', userAddress, orbit, dbName, userAddress,
}); });
yield put(addOrbitDB({ address: peerDbAddress, type: 'keyvalue' })); yield put(addOrbitDB({ address: peerDbAddress, type: 'keyvalue' }));

10
packages/concordia-app/src/redux/store.js

@ -1,11 +1,14 @@
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit';
import { drizzleReducers, drizzleMiddlewares, generateContractsInitialState } from '@ezerous/drizzle'; import {
import { breezeReducers } from '@ezerous/breeze'; drizzleReducers, drizzleMiddlewares, generateContractsInitialState, Drizzle,
} from '@ezerous/drizzle';
import { Breeze, breezeReducers } from '@ezerous/breeze';
import createSagaMiddleware from 'redux-saga'; import createSagaMiddleware from 'redux-saga';
import userReducer from './reducers/userReducer'; import userReducer from './reducers/userReducer';
import rootSaga from './sagas/rootSaga'; import rootSaga from './sagas/rootSaga';
import drizzleOptions from '../options/drizzleOptions'; import drizzleOptions from '../options/drizzleOptions';
import peerDbReplicationReducer from './reducers/peerDbReplicationReducer'; import peerDbReplicationReducer from './reducers/peerDbReplicationReducer';
import breezeOptions from '../options/breezeOptions';
const initialState = { const initialState = {
contracts: generateContractsInitialState(drizzleOptions), contracts: generateContractsInitialState(drizzleOptions),
@ -24,5 +27,8 @@ const store = configureStore({
preloadedState: initialState, preloadedState: initialState,
}); });
export const drizzle = new Drizzle(drizzleOptions, store);
export const breeze = new Breeze(breezeOptions, store);
sagaMiddleware.run(rootSaga); sagaMiddleware.run(rootSaga);
export default store; export default store;

71
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
}

15
packages/concordia-app/src/views/Home/index.jsx

@ -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);

30
packages/concordia-app/src/views/Topic/TopicCreate/index.jsx

@ -1,5 +1,5 @@
import React, { import React, {
useCallback, useContext, useEffect, useState, useCallback, useEffect, useState,
} from 'react'; } from 'react';
import { import {
Button, Container, Form, Icon, Input, TextArea, Button, Container, Form, Icon, Input, TextArea,
@ -7,30 +7,16 @@ import {
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import AppContext from '../../../components/AppContext';
import './styles.css'; import './styles.css';
import { drizzle, breeze } from '../../../redux/store';
const { contracts: { Forum: { methods: { createTopic } } } } = drizzle;
const { orbit: { stores } } = breeze;
const TopicCreate = (props) => { const TopicCreate = (props) => {
const { account } = props; const { account } = props;
const {
drizzle: {
contracts: {
Forum: {
methods: { createTopic },
},
},
},
breeze: {
orbit: {
stores,
},
},
} = useContext(AppContext.Context);
const transactionStack = useSelector((state) => state.transactionStack); const transactionStack = useSelector((state) => state.transactionStack);
const transactions = useSelector((state) => state.transactions); const transactions = useSelector((state) => state.transactions);
const [subjectInput, setSubjectInput] = useState(''); const [subjectInput, setSubjectInput] = useState('');
const [messageInput, setMessageInput] = useState(''); const [messageInput, setMessageInput] = useState('');
const [topicSubjectInputEmptySubmit, setTopicSubjectInputEmptySubmit] = useState(false); const [topicSubjectInputEmptySubmit, setTopicSubjectInputEmptySubmit] = useState(false);
@ -95,9 +81,7 @@ const TopicCreate = (props) => {
}); });
} }
} }
}, [ }, [createTopicCacheSendStackId, history, messageInput, posting, subjectInput, transactionStack, transactions]);
transactions, transactionStack, history, posting, createTopicCacheSendStackId, subjectInput, messageInput, stores,
]);
const validateAndPost = useCallback(() => { const validateAndPost = useCallback(() => {
if (subjectInput === '') { if (subjectInput === '') {
@ -112,7 +96,7 @@ const TopicCreate = (props) => {
setPosting(true); setPosting(true);
setCreateTopicCacheSendStackId(createTopic.cacheSend(...[], { from: account })); setCreateTopicCacheSendStackId(createTopic.cacheSend(...[], { from: account }));
}, [account, createTopic, messageInput, subjectInput]); }, [account, messageInput, subjectInput]);
return ( return (
<Container> <Container>

124
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 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;

12
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;
}

2
packages/concordia-app/src/views/Topic/index.jsx

@ -12,7 +12,7 @@ const Topic = () => {
<TopicCreate /> <TopicCreate />
) )
: ( : (
<TopicView topicId={topicId} /> <TopicView topicId={parseInt(topicId, 10)} />
); );
}; };

5
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" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.13.0.tgz#31c02263673ec3789f90eb7b6963676aa407a598"
integrity sha512-DD0vOdofJdoaRNtnWcrXe6RQbpHkPPmtqGq14uRX0F8ZKJ5nv89CVTYl/BZdppDxBDaV0hl75htg3abpEWlPZA== 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: mortice@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/mortice/-/mortice-2.0.0.tgz#7be171409c2115561ba3fc035e4527f9082eefde" resolved "https://registry.yarnpkg.com/mortice/-/mortice-2.0.0.tgz#7be171409c2115561ba3fc035e4527f9082eefde"

Loading…
Cancel
Save