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. 281
      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. 122
      packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx
  10. 8
      packages/concordia-app/src/components/TopicList/TopicListRow/styles.css
  11. 86
      packages/concordia-app/src/components/TopicList/index.jsx
  12. 4
      packages/concordia-app/src/components/TopicList/styles.css
  13. 12
      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-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",

11
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}}"
}

281
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 (
<LoadingComponent
title="Connecting to the Ethereum network..."
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account."
imageType="ethereum"
progress={20}
progressType="indicating"
/>
);
}
const LoadingContainer = ({ children }) => {
const initializing = useSelector((state) => state.drizzleStatus.initializing);
const failed = useSelector((state) => state.drizzleStatus.failed);
const ipfsStatus = useSelector((state) => state.ipfs.status);
const orbitStatus = useSelector((state) => state.orbit.status);
const web3Status = useSelector((state) => state.web3.status);
const web3NetworkId = useSelector((state) => state.web3.networkId);
const web3NetworkFailed = useSelector((state) => state.web3.networkFailed);
const web3AccountsFailed = useSelector((state) => state.web3.accountsFailed);
const contractInitialized = useSelector((state) => state.contracts.Forum.initialized);
const contractDeployed = useSelector((state) => state.contracts.Forum.deployed);
const userFetched = useSelector((state) => state.user.address);
if ((web3Status === 'initializing' || !web3NetworkId)
&& !web3NetworkFailed) {
return (
<LoadingComponent
title="Connecting to the Ethereum network..."
message="Please make sure to unlock MetaMask and grant the app the right to connect to your account."
imageType="ethereum"
progress={20}
progressType="indicating"
/>
);
}
if (status === 'failed' || networkFailed) {
return (
<LoadingComponent
title="No connection to the Ethereum network!"
message="Please make sure that:"
message_list={['MetaMask is unlocked and pointed to the correct, available network',
'The app has been granted the right to connect to your account']}
imageType="ethereum"
progress={20}
progressType="error"
/>
);
}
if (web3Status === 'failed' || web3NetworkFailed) {
return (
<LoadingComponent
title="No connection to the Ethereum network!"
message="Please make sure that:"
message_list={['MetaMask is unlocked and pointed to the correct, available network',
'The app has been granted the right to connect to your account']}
imageType="ethereum"
progress={20}
progressType="error"
/>
);
}
if (status === 'initialized' && accountsFailed) {
return (
<LoadingComponent
title="We can't find any Ethereum accounts!"
message="Please make sure that MetaMask is unlocked."
imageType="ethereum"
progress={20}
progressType="error"
/>
);
}
if (web3Status === 'initialized' && web3AccountsFailed) {
return (
<LoadingComponent
title="We can't find any Ethereum accounts!"
message="Please make sure that MetaMask is unlocked."
imageType="ethereum"
progress={20}
progressType="error"
/>
);
}
if (initializing
if (initializing
|| (!failed && !contractInitialized && contractDeployed)) {
return (
<LoadingComponent
title="Initializing contracts..."
message=""
imageType="ethereum"
progress={40}
progressType="indicating"
/>
);
}
if (!contractDeployed) {
return (
<LoadingComponent
title="No contracts found on the current network!"
message="Please make sure that you are connected to the correct network and the contracts are deployed."
imageType="ethereum"
progress={40}
progressType="error"
/>
);
}
return (
<LoadingComponent
title="Initializing contracts..."
message=""
imageType="ethereum"
progress={40}
progressType="indicating"
/>
);
}
if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) {
return (
<LoadingComponent
title="Initializing IPFS..."
message=""
imageType="ipfs"
progress={60}
progressType="indicating"
/>
);
}
if (!contractDeployed) {
return (
<LoadingComponent
title="No contracts found on the current network!"
message="Please make sure that you are connected to the correct network and the contracts are deployed."
imageType="ethereum"
progress={40}
progressType="error"
/>
);
}
if (ipfsStatus === breezeConstants.STATUS_FAILED) {
return (
<LoadingComponent
title="IPFS initialization failed!"
message=""
imageType="ipfs"
progress={60}
progressType="error"
/>
);
}
if (ipfsStatus === breezeConstants.STATUS_INITIALIZING) {
return (
<LoadingComponent
title="Initializing IPFS..."
message=""
imageType="ipfs"
progress={60}
progressType="indicating"
/>
);
}
if (orbitStatus === breezeConstants.STATUS_INITIALIZING) {
const message = process.env.NODE_ENV === 'development'
? 'If needed, please sign the transaction in MetaMask to create the databases.'
: 'Please sign the transaction in MetaMask to create the databases.';
return (
<LoadingComponent
title="Preparing OrbitDB..."
message={message}
imageType="orbit"
progress={80}
progressType="indicating"
/>
);
}
if (ipfsStatus === breezeConstants.STATUS_FAILED) {
return (
<LoadingComponent
title="IPFS initialization failed!"
message=""
imageType="ipfs"
progress={60}
progressType="error"
/>
);
}
if (orbitStatus === breezeConstants.STATUS_FAILED) {
return (
<LoadingComponent
title="OrbitDB initialization failed!"
message=""
imageType="orbit"
progress={80}
progressType="error"
/>
);
}
if (orbitStatus === breezeConstants.STATUS_INITIALIZING) {
const message = process.env.NODE_ENV === 'development'
? 'If needed, please sign the transaction in MetaMask to create the databases.'
: 'Please sign the transaction in MetaMask to create the databases.';
return (
<LoadingComponent
title="Preparing OrbitDB..."
message={message}
imageType="orbit"
progress={80}
progressType="indicating"
/>
);
}
if (!userFetched) {
return (
<LoadingComponent
title="Loading dapp..."
message=""
imageType="app"
progress={90}
progressType="indicating"
/>
);
}
if (orbitStatus === breezeConstants.STATUS_FAILED) {
return (
<LoadingComponent
title="OrbitDB initialization failed!"
message=""
imageType="orbit"
progress={80}
progressType="error"
/>
);
}
return Children.only(children);
if (!userFetched) {
return (
<LoadingComponent
title="Loading dapp..."
message=""
imageType="app"
progress={90}
progressType="indicating"
/>
);
}
}
const mapStateToProps = (state) => ({
drizzleStatus: state.drizzleStatus,
breezeStatus: state.breezeStatus,
ipfsStatus: state.ipfs.status,
orbitStatus: state.orbit.status,
web3: state.web3,
accounts: state.accounts,
contractInitialized: state.contracts.Forum.initialized,
contractDeployed: state.contracts.Forum.deployed,
userFetched: state.user.address,
});
return Children.only(children);
};
export default connect(mapStateToProps)(LoadingContainer);
export default LoadingContainer;

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

122
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 (
<>
<List.Header>
<List.Icon name="right triangle" />
{topicSubject && topicSubject.subject}
</List.Header>
<List.Content>
{topicData.username}
{topicData.numberOfReplies}
{' '}
replies
timestamp
</List.Content>
</>
);
return useMemo(() => {
const handleTopicClick = () => {
history.push(`/topics/${topicId}`);
};
return (
<Dimmer.Dimmable as={List.Item} onClick={handleTopicClick} blurring dimmed={loading} className="list-item">
<List.Icon name="user circle" size="big" inverted color="black" verticalAlign="middle" />
<List.Content>
<List.Header>
<Grid>
<Grid.Column floated="left" width={14}>
{topicSubject !== null
? topicSubject
: <Placeholder><Placeholder.Line length="very long" /></Placeholder>}
</Grid.Column>
<Grid.Column floated="right" width={2} textAlign="right">
<span className="topic-metadata">
{t('topic.list.row.topic.id', { id: topicId })}
</span>
</Grid.Column>
</Grid>
</List.Header>
<List.Description>
<Grid verticalAlign="middle">
<Grid.Column floated="left" width={14}>
{topicAuthor !== null && timeAgo !== null
? t('topic.list.row.author.date', { author: topicAuthor, timeAgo })
: <Placeholder><Placeholder.Line length="long" /></Placeholder>}
</Grid.Column>
<Grid.Column floated="right" width={2} textAlign="right">
{numberOfReplies !== null
? (
<span className="topic-metadata">
{t('topic.list.row.number.of.replies', { numberOfReplies })}
</span>
)
: <Placeholder fluid><Placeholder.Line /></Placeholder>}
</Grid.Column>
</Grid>
</List.Description>
</List.Content>
</Dimmer.Dimmable>
);
}, [history, loading, numberOfReplies, t, timeAgo, topicAuthor, topicId, topicSubject]);
};
const TopicData = PropTypes.PropTypes.shape({
userAddress: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
timestamp: PropTypes.number.isRequired,
numberOfReplies: PropTypes.number.isRequired,
});
TopicListRow.defaultProps = {
loading: false,
};
TopicListRow.propTypes = {
topicData: TopicData.isRequired,
topicId: PropTypes.number.isRequired,
id: PropTypes.number.isRequired,
topicCallHash: PropTypes.string,
loading: PropTypes.bool,
};
export default TopicListRow;
export default memo(TopicListRow);

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

86
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 (
<List.Item key={topicId} className="list-item" name={topicId} onClick={() => handleTopicClick(topicId)}>
<TopicListRow
topicData={topicData}
topicId={topicId}
/>
</List.Item>
);
}
const topicHash = getTopicCallHashes.find((getTopicCallHash) => getTopicCallHash.id === topicId);
return (
<List.Item key={topicId} className="list-item" name={topicId} onClick={() => handleTopicClick(topicId)}>
<Placeholder
placeholderType={PLACEHOLDER_TYPE_TOPIC}
extra={{ topicId }}
/>
</List.Item>
<TopicListRow
id={topicId}
key={topicId}
topicCallHash={topicHash && topicHash.hash}
loading={topicHash === undefined}
/>
);
}), [getTopicCallHashes, getTopicResults, handleTopicClick, topicIds]);
}), [getTopicCallHashes, topicIds]);
return (
<List selection divided id="topic-list" size="big">

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

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

12
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(
<Suspense fallback={<LoadingScreen />}>
<AppContext.Provider drizzle={drizzle} breeze={breeze}>
<App store={store} />
</AppContext.Provider>
<App store={store} />
</Suspense>,
document.getElementById('root'),
);

1
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) => {

4
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' }));

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

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, {
useContext, useEffect, useMemo, useState,
memo, useEffect, useMemo, useState,
} from 'react';
import { Container } from 'semantic-ui-react';
import { useSelector } from 'react-redux';
import AppContext from '../../components/AppContext';
import Board from './Board';
import './styles.css';
import { drizzle } from '../../redux/store';
const { contracts: { Forum: { methods: { getNumberOfTopics } } } } = drizzle;
const Home = () => {
const { drizzle: { contracts: { Forum: { methods: { getNumberOfTopics } } } } } = useContext(AppContext.Context);
const [numberOfTopicsCallHash, setNumberOfTopicsCallHash] = useState('');
const getNumberOfTopicsResults = useSelector((state) => state.contracts.Forum.getNumberOfTopics);
useEffect(() => {
setNumberOfTopicsCallHash(getNumberOfTopics.cacheCall());
}, [getNumberOfTopics]);
}, []);
const numberOfTopics = useMemo(() => (getNumberOfTopicsResults[numberOfTopicsCallHash] !== undefined
? parseInt(getNumberOfTopicsResults[numberOfTopicsCallHash].value, 10)
: null),
[getNumberOfTopicsResults, numberOfTopicsCallHash]);
return (
return useMemo(() => (
<Container id="home-container" textAlign="center">
{numberOfTopics !== null && <Board numberOfTopics={numberOfTopics} />}
</Container>
);
), [numberOfTopics]);
};
export default Home;
export default memo(Home);

30
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 (
<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 { useDispatch, useSelector } from 'react-redux';
import {
Container, Dimmer, Icon, Placeholder, Step,
} from 'semantic-ui-react';
import moment from 'moment';
import { breeze, drizzle } from '../../../redux/store';
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions';
import './styles.css';
import PostList from '../../../components/PostList';
const { contracts: { Forum: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle;
const { orbit } = breeze;
const TopicView = (props) => {
const { topicId } = props;
const {
topicId, topicAuthorAddress: initialTopicAuthorAddress, topicAuthor: initialTopicAuthor,
timestamp: initialTimestamp, postIds: initialPostIds,
} = props;
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
const userAddress = useSelector((state) => state.user.address);
const getTopicResults = useSelector((state) => state.contracts.Forum.getTopic);
const topics = useSelector((state) => state.orbitData.topics);
const [getTopicCallHash, setGetTopicCallHash] = useState([]);
const [topicAuthorAddress, setTopicAuthorAddress] = useState(initialTopicAuthorAddress || null);
const [topicAuthor, setTopicAuthor] = useState(initialTopicAuthor || null);
const [timestamp, setTimestamp] = useState(initialTimestamp || null);
const [postIds, setPostIds] = useState(initialPostIds || null);
const [topicSubject, setTopicSubject] = useState(null);
const dispatch = useDispatch();
useEffect(() => {
const shouldGetTopicDataFromChain = topicAuthorAddress === null
|| topicAuthor === null
|| timestamp === null
|| postIds === null;
if (drizzleInitialized && !drizzleInitializationFailed && shouldGetTopicDataFromChain) {
setGetTopicCallHash(getTopicChainData(topicId));
}
}, [
drizzleInitializationFailed, drizzleInitialized, postIds, timestamp, topicAuthor, topicAuthorAddress, topicId,
]);
useEffect(() => {
if (getTopicCallHash && getTopicResults && getTopicResults[getTopicCallHash]) {
setTopicAuthorAddress(getTopicResults[getTopicCallHash].value[0]);
setTopicAuthor(getTopicResults[getTopicCallHash].value[1]);
setTimestamp(getTopicResults[getTopicCallHash].value[2]);
setPostIds(getTopicResults[getTopicCallHash].value[3].map((postId) => parseInt(postId, 10)));
const topicFound = topics
.find((topic) => topic.id === topicId);
if (topicFound === undefined && userAddress !== getTopicResults[getTopicCallHash].value[0]) {
dispatch({
type: FETCH_USER_DATABASE,
orbit,
dbName: 'topics',
userAddress: getTopicResults[getTopicCallHash].value[0],
});
}
}
}, [dispatch, getTopicCallHash, getTopicResults, topicId, topics, userAddress]);
useEffect(() => {
const topicFound = topics
.find((topic) => topic.id === topicId);
if (topicFound) {
setTopicSubject(topicFound.subject);
}
}, [topicId, topics]);
return (
<div>
TODO
</div>
<Container id="topic-container" textAlign="center">
<Dimmer.Dimmable
blurring
dimmed={topicAuthorAddress === null && topicAuthor === null && timestamp === null}
>
<Step.Group fluid>
<Step key="topic-header-step-user">
<Icon name="user circle" size="big" inverted color="black" />
<Step.Content>
<Step.Title>
{topicAuthor || (
<Placeholder id="author-placeholder" inverted>
<Placeholder.Line length="full" />
</Placeholder>
)}
</Step.Title>
</Step.Content>
</Step>
<Step key="topic-header-step-title">
<Step.Content>
<Step.Title>
{topicSubject || (
<Placeholder id="subject-placeholder">
<Placeholder.Line length="full" />
</Placeholder>
)}
</Step.Title>
<Step.Description>
{timestamp
? moment(timestamp * 1000).fromNow()
: (
<Placeholder id="date-placeholder">
<Placeholder.Line length="full" />
</Placeholder>
)}
</Step.Description>
</Step.Content>
</Step>
</Step.Group>
</Dimmer.Dimmable>
<PostList postIds={postIds || []} loading={postIds === null} />
</Container>
);
};
TopicView.propTypes = {
topicId: PropTypes.number.isRequired,
topicAuthorAddress: PropTypes.string,
topicAuthor: PropTypes.string,
timestamp: PropTypes.number,
postIds: PropTypes.arrayOf(PropTypes.number),
};
export default TopicView;

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 />
)
: (
<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"
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"

Loading…
Cancel
Save