Browse Source

Merge branch 'develop' into feature/add-ci-support

develop
Apostolos Fanakis 4 years ago
parent
commit
4684805d95
  1. 8
      README.md
  2. 3
      packages/concordia-app/package.json
  3. 4
      packages/concordia-app/public/locales/en/translation.json
  4. 6
      packages/concordia-app/src/assets/About.md
  5. 48
      packages/concordia-app/src/assets/css/index.css
  6. 2
      packages/concordia-app/src/assets/particles.js
  7. 4
      packages/concordia-app/src/components/ClearDatabasesModal/index.jsx
  8. 2
      packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx
  9. 5
      packages/concordia-app/src/components/LoadingScreen.jsx
  10. 33
      packages/concordia-app/src/components/PostCreate/index.jsx
  11. 13
      packages/concordia-app/src/components/PostCreate/styles.css
  12. 50
      packages/concordia-app/src/components/PostList/PostListRow/index.jsx
  13. 31
      packages/concordia-app/src/components/PostList/PostListRow/styles.css
  14. 177
      packages/concordia-app/src/components/PostList/PostVoting/index.jsx
  15. 20
      packages/concordia-app/src/components/PostList/PostVoting/styles.css
  16. 4
      packages/concordia-app/src/components/PostList/index.jsx
  17. 52
      packages/concordia-app/src/components/ProfileImage.jsx
  18. 145
      packages/concordia-app/src/components/TopicList/TopicListRow/index.jsx
  19. 62
      packages/concordia-app/src/components/TopicList/TopicListRow/styles.css
  20. 3
      packages/concordia-app/src/components/TopicList/index.jsx
  21. 3
      packages/concordia-app/src/components/TopicList/styles.css
  22. 11
      packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx
  23. 4
      packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/styles.css
  24. 6
      packages/concordia-app/src/redux/actions/contractEventActions.js
  25. 4
      packages/concordia-app/src/views/About/index.jsx
  26. 3
      packages/concordia-app/src/views/About/styles.css
  27. 80
      packages/concordia-app/src/views/Home/Board/index.jsx
  28. 9
      packages/concordia-app/src/views/Home/Board/styles.css
  29. 3
      packages/concordia-app/src/views/Profile/GeneralTab/EditInformationModal/index.jsx
  30. 42
      packages/concordia-app/src/views/Profile/GeneralTab/index.jsx
  31. 11
      packages/concordia-app/src/views/Profile/GeneralTab/styles.css
  32. 3
      packages/concordia-app/src/views/Profile/index.jsx
  33. 3
      packages/concordia-app/src/views/Profile/styles.css
  34. 5
      packages/concordia-app/src/views/Register/PersonalInformationStep/index.jsx
  35. 5
      packages/concordia-app/src/views/Register/SignUpStep/index.jsx
  36. 4
      packages/concordia-app/src/views/Register/index.jsx
  37. 39
      packages/concordia-app/src/views/Topic/TopicCreate/index.jsx
  38. 10
      packages/concordia-app/src/views/Topic/TopicCreate/styles.css
  39. 111
      packages/concordia-app/src/views/Topic/TopicView/index.jsx
  40. 14
      packages/concordia-app/src/views/Topic/TopicView/styles.css
  41. 6
      packages/concordia-contracts/contracts/PostVoting.sol
  42. 10
      packages/concordia-shared/src/constants/contracts/events/PostVotingContractEvents.js
  43. 6
      packages/concordia-shared/src/constants/contracts/events/index.js
  44. 127
      yarn.lock

8
README.md

@ -4,28 +4,28 @@
## Setup
```shell script
cd apella
cd concordia
yarn
```
## Compile contracts
```shell script
cd packages/apella-contracts
cd packages/concordia-contracts
yarn compile
```
## Run app
```shell script
cd packages/apella-app
cd packages/concordia-app
yarn start
```
## Build app
```shell script
cd packages/apella-app
cd packages/concordia-app
yarn build
```

3
packages/concordia-app/package.json

@ -37,6 +37,7 @@
"lodash": "^4.17.20",
"prop-types": "~15.7.2",
"react": "~16.13.1",
"react-avatar": "~3.9.7",
"react-dom": "~16.13.1",
"react-i18next": "^11.7.3",
"react-markdown": "^5.0.3",
@ -48,7 +49,7 @@
"react-timeago": "~5.2.0",
"redux-saga": "~1.1.3",
"semantic-ui-css": "~2.4.1",
"semantic-ui-react": "~1.2.1",
"semantic-ui-react": "~2.0.3",
"web3": "~1.3.3"
},
"devDependencies": {

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

@ -69,11 +69,9 @@
"topbar.button.register": "Sign Up",
"topic.create.form.content.field.label": "First post content",
"topic.create.form.content.field.placeholder": "Message",
"topic.create.form.post.button": "Post",
"topic.create.form.post.button": "Create Topic",
"topic.create.form.subject.field.label": "Topic subject",
"topic.create.form.subject.field.placeholder": "Subject",
"topic.list.row.author": "by {{author}}",
"topic.list.row.number.of.replies": "{{numberOfReplies}} replies",
"topic.list.row.topic.id": "#{{id}}",
"username.selector.error.username.empty.message": "Username is required",
"username.selector.error.username.taken.message": "The username {{username}} is already taken.",

6
packages/concordia-app/src/assets/About.md

@ -27,9 +27,9 @@ You can read more about the technological stack in Concordia's [whitepaper][conc
Developed by [apostolof][devs-apostolof-profile], [ezerous][devs-ezerous-profile]
[concordia-repository]: https://gitlab.com/ecentrics/apella
[concordia-docker-hub]: https://hub.docker.com/repository/docker/ecentrics/apella-app
[concordia-license]: https://gitlab.com/ecentrics/apella/-/blob/master/LICENSE.md
[concordia-repository]: https://gitlab.com/ecentrics/concordia
[concordia-docker-hub]: https://hub.docker.com/repository/docker/ecentrics/concordia-app
[concordia-license]: https://gitlab.com/ecentrics/concordia/-/blob/master/LICENSE.md
[devs-apostolof-profile]: https://gitlab.com/Apostolof
[devs-ezerous-profile]: https://gitlab.com/Ezerous
[concordia-whitepaper]: https://whitepaper.concordia.ecentrics.net

48
packages/concordia-app/src/assets/css/index.css

@ -1,6 +1,16 @@
:root {
--primary-color: #EA6954;
--primary-color-highlighted: #DB5844;
--secondary-color: #0B2540;
--secondary-color-highlighted: #061A30;
}
body.app {
height: auto;
padding-bottom: 4rem;
overflow: auto;
margin: 0;
background: #E6E6E6;
}
div {
@ -19,3 +29,41 @@ div {
color: gray;
font-style: italic;
}
.primary-button{
color: white !important;
background-color: var(--primary-color) !important;
}
.primary-button:hover {
background-color: var(--primary-color-highlighted) !important;
}
.secondary-button{
color: white !important;
background-color: var(--secondary-color) !important;
}
.secondary-button:hover {
background-color: var(--secondary-color-highlighted) !important;
}
.skip-button {
color: var(--secondary-color) !important;
background-color: white !important;
box-shadow: 0 0 0 1px var(--secondary-color) inset !important;
}
.skip-button:hover {
color: var(--secondary-color-highlighted) !important;
box-shadow: 0 0 0 1px var(--secondary-color-highlighted) inset !important;
}
.unselectable {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}

2
packages/concordia-app/src/assets/particles.js

@ -16,7 +16,7 @@ const particlesOptions = {
speed: 0.12,
},
size: {
value: 1,
value: 1.5,
},
opacity: {
anim: {

4
packages/concordia-app/src/components/ClearDatabasesModal/index.jsx

@ -126,10 +126,10 @@ const ClearDatabasesModal = (props) => {
{!isClearing && (
<Modal.Actions>
<Button color="black" basic onClick={onCancelTry} disabled={isClearing}>
<Button className="secondary-button" onClick={onCancelTry} disabled={isClearing}>
{t('clear.databases.modal.cancel.button')}
</Button>
<Button onClick={handleSubmit} negative disabled={!userConfirmed}>
<Button onClick={handleSubmit} className="primary-button" disabled={!userConfirmed}>
{t('clear.databases.modal.clear.button')}
</Button>
</Modal.Actions>

2
packages/concordia-app/src/components/InitializationScreen/CustomLoader/index.jsx

@ -8,7 +8,7 @@ import metamaskLogo from '../../../assets/images/metamask_logo.svg';
import ethereumLogo from '../../../assets/images/ethereum_logo.svg';
import ipfsLogo from '../../../assets/images/ipfs_logo.svg';
import orbitdbLogo from '../../../assets/images/orbitdb_logo.svg';
import appLogo from '../../../assets/images/app_logo.svg';
import appLogo from '../../../assets/images/app_logo_circle.svg';
const LoadingComponent = (props) => {
useEffect(() => function cleanup() {

5
packages/concordia-app/src/components/LoadingScreen.jsx

@ -1,9 +1,8 @@
import React from 'react';
import { Loader } from 'semantic-ui-react';
const LoadingScreen = () => (
<div>
Loading
</div>
<Loader active />
);
export default LoadingScreen;

33
packages/concordia-app/src/components/PostCreate/index.jsx

@ -2,7 +2,7 @@ import React, {
memo, useCallback, useEffect, useState,
} from 'react';
import {
Button, Feed, Form, Icon, Image, TextArea,
Button, Feed, Form, Icon, TextArea,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
@ -12,7 +12,6 @@ import { POSTS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/or
import { POST_CREATED_EVENT } from 'concordia-shared/src/constants/contracts/events/ForumContractEvents';
import determineKVAddress from '../../utils/orbitUtils';
import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions';
import { USER_PROFILE_PICTURE } from '../../constants/orbit/UserDatabaseKeys';
import { breeze, drizzle } from '../../redux/store';
import './styles.css';
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus';
@ -28,7 +27,6 @@ const PostCreate = (props) => {
const transactionStack = useSelector((state) => state.transactionStack);
const transactions = useSelector((state) => state.transactions);
const [postContent, setPostContent] = useState('');
const [userProfilePictureUrl, setUserProfilePictureUrl] = useState();
const [createPostCacheSendStackId, setCreatePostCacheSendStackId] = useState('');
const [posting, setPosting] = useState(false);
const [storingPost, setStoringPost] = useState(false);
@ -44,9 +42,7 @@ const PostCreate = (props) => {
const userFound = users
.find((user) => user.id === userOrbitAddress);
if (userFound) {
setUserProfilePictureUrl(userFound[USER_PROFILE_PICTURE]);
} else {
if (!userFound) {
dispatch({
type: FETCH_USER_DATABASE,
orbit,
@ -122,23 +118,6 @@ const PostCreate = (props) => {
return (
<Feed>
<Feed.Event>
<Feed.Label className="post-profile-picture">
{userProfilePictureUrl
? (
<Image
avatar
src={userProfilePictureUrl}
/>
)
: (
<Icon
name="user circle"
size="big"
inverted
color="black"
/>
)}
</Feed.Label>
<Feed.Content>
<Feed.Summary>
<Form>
@ -152,12 +131,12 @@ const PostCreate = (props) => {
/>
</Form>
</Feed.Summary>
<Feed.Meta>
<Feed.Meta id="post-button-div">
<Feed.Like>
<Form.Button
<Button
animated
type="button"
color="green"
className="primary-button"
disabled={posting || postContent === ''}
onClick={savePost}
>
@ -167,7 +146,7 @@ const PostCreate = (props) => {
<Button.Content hidden>
<Icon name="send" />
</Button.Content>
</Form.Button>
</Button>
</Feed.Like>
</Feed.Meta>
</Feed.Content>

13
packages/concordia-app/src/components/PostCreate/styles.css

@ -1,7 +1,3 @@
.post-profile-picture {
margin: 5px 0 0 0;
}
.post-summary-meta-index {
float: right;
font-size: 12px;
@ -11,3 +7,12 @@
.like:hover .icon {
color: #fff !important;
}
#post-button-div {
float: right;
margin: 1rem 0 4rem 0;
}
#post-button-div button {
margin: 0;
}

50
packages/concordia-app/src/components/PostList/PostListRow/index.jsx

@ -2,7 +2,7 @@ import React, {
memo, useEffect, useMemo, useState, useCallback,
} from 'react';
import {
Dimmer, Icon, Image, Feed, Placeholder, Ref,
Dimmer, Feed, Placeholder, Ref,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
@ -15,8 +15,9 @@ import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationAct
import { breeze } from '../../../redux/store';
import './styles.css';
import determineKVAddress from '../../../utils/orbitUtils';
import { USER_PROFILE_PICTURE } from '../../../constants/orbit/UserDatabaseKeys';
import { POST_CONTENT } from '../../../constants/orbit/PostsDatabaseKeys';
import ProfileImage from '../../ProfileImage';
import PostVoting from '../PostVoting';
const { orbit } = breeze;
@ -90,34 +91,6 @@ const PostListRow = (props) => {
}
}, [postAuthorAddress, users]);
const authorAvatar = useMemo(() => (postAuthorMeta !== null && postAuthorMeta[USER_PROFILE_PICTURE]
? (
<Image
avatar
src={postAuthorMeta[USER_PROFILE_PICTURE]}
/>
)
: (
<Icon
name="user circle"
size="big"
inverted
color="black"
/>
)), [postAuthorMeta]);
const authorAvatarLink = useMemo(() => {
if (postAuthorAddress) {
return (
<Link to={`/users/${postAuthorAddress}`}>
{authorAvatar}
</Link>
);
}
return authorAvatar;
}, [authorAvatar, postAuthorAddress]);
const focusRef = useCallback((node) => {
if (focus && node !== null) {
node.scrollIntoView({ behavior: 'smooth' });
@ -130,13 +103,20 @@ const PostListRow = (props) => {
blurring
dimmed={loading}
id={`post-${postId}`}
className="post-list-row"
>
<Ref innerRef={focusRef}>
<Feed.Label className="post-profile-picture">
{authorAvatarLink}
<ProfileImage
topicAuthorAddress={postAuthorAddress}
topicAuthor={postAuthor}
topicAuthorMeta={postAuthorMeta}
size="42"
link
/>
</Feed.Label>
</Ref>
<Feed.Content>
<Feed.Content className="post-content">
<Feed.Summary>
<Link to={`/topics/${topicId}/#post-${postId}`}>
<span className="post-summary-meta-index">
@ -147,7 +127,7 @@ const PostListRow = (props) => {
? (
<>
<Feed.User as={Link} to={`/users/${postAuthorAddress}`}>{postAuthor}</Feed.User>
<Feed.Date className="post-summary-meta-date">
<Feed.Date>
<TimeAgo date={timeAgo} />
</Feed.Date>
</>
@ -159,10 +139,10 @@ const PostListRow = (props) => {
? postContent
: <Placeholder><Placeholder.Line length="long" /></Placeholder>}
</Feed.Extra>
<PostVoting postId={postId} postAuthorAddress={postAuthorAddress} />
</Feed.Content>
</Dimmer.Dimmable>
), [
authorAvatarLink, focusRef, loading, postAuthor, postAuthorAddress, postContent, postId, postIndex, t, timeAgo,
), [focusRef, loading, postAuthor, postAuthorAddress, postAuthorMeta, postContent, postId, postIndex, t, timeAgo,
topicId,
]);
};

31
packages/concordia-app/src/components/PostList/PostListRow/styles.css

@ -1,9 +1,36 @@
.post-list-row {
padding: 1.2rem 0.1rem 1.2rem 0.6rem !important;
}
.post-profile-picture {
margin: 5px 0 0 0;
margin-top: 0.9rem;
}
.post-content {
margin-left: 1.3rem !important;
}
.post-content > div.summary {
font-size: 1rem !important;
}
.post-content > div.extra {
padding-right: 1rem !important;
}
.post-content .placeholder {
margin: 0.5rem 0 0;
font-size: 2rem !important;
}
.post-content .placeholder > .line{
height: auto;
}
.post-summary-meta-index {
float: right;
font-size: 12px;
width: 3rem;
text-align: right;
font-size: 0.75rem !important;
opacity: 0.4;
}

177
packages/concordia-app/src/components/PostList/PostVoting/index.jsx

@ -0,0 +1,177 @@
import React, {
memo, useCallback, useEffect, useMemo, useState,
} from 'react';
import { Button, Popup } from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { POST_VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { drizzle } from '../../../redux/store';
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../../constants/TransactionStatus';
import './styles.css';
const CHOICE_DEFAULT = '0';
const CHOICE_UP = '1';
const CHOICE_DOWN = '2';
const {
contracts: {
[POST_VOTING_CONTRACT]: {
methods: {
getVote: { cacheCall: getVoteChainData },
getTotalVoteCount: { cacheCall: getTotalVoteCountChainData },
getUpvoteCount: { cacheCall: getUpvoteCountChainData },
getDownvoteCount: { cacheCall: getDownvoteCountChainData },
upvote, downvote, unvote,
},
},
},
} = drizzle;
const PostVoting = (props) => {
const { postId, postAuthorAddress } = props;
const drizzleInitialized = useSelector((state) => state.drizzleStatus.initialized);
const drizzleInitializationFailed = useSelector((state) => state.drizzleStatus.failed);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const userAccount = useSelector((state) => state.accounts[0]);
// Current votes
const [getVoteCallHash, setGetVoteCallHash] = useState(null);
const [getTotalVoteCountCallHash, setGetTotalVoteCountCallHash] = useState(null);
const [getUpvoteCountCallHash, setGetUpvoteCountCallHash] = useState(null);
const [getDownvoteCountCallHash, setGetDownvoteCountCallHash] = useState(null);
const getVoteResult = useSelector((state) => state.contracts[POST_VOTING_CONTRACT].getVote[getVoteCallHash]);
const getTotalVoteCountResult = useSelector((state) => state.contracts[POST_VOTING_CONTRACT].getTotalVoteCount[getTotalVoteCountCallHash]);
const getUpvoteCountResult = useSelector((state) => state.contracts[POST_VOTING_CONTRACT].getUpvoteCount[getUpvoteCountCallHash]);
const getDownvoteCountResult = useSelector((state) => state.contracts[POST_VOTING_CONTRACT].getDownvoteCount[getDownvoteCountCallHash]);
const [ownVote, setOwnVote] = useState(null);
const [totalVoteCount, setTotalVoteCount] = useState(null);
const [upvoteCount, setUpvoteCount] = useState(null);
const [downvoteCount, setDownvoteCount] = useState(null);
// Voting
const transactionStack = useSelector((state) => state.transactionStack);
const transactions = useSelector((state) => state.transactions);
const [voting, setVoting] = useState(false);
const [createVoteCacheSendStackId, setVoteCacheSendStackId] = useState('');
// Current votes
useEffect(() => {
if (drizzleInitialized && !drizzleInitializationFailed && postId !== null) {
if (getTotalVoteCountCallHash === null) setGetTotalVoteCountCallHash(getTotalVoteCountChainData(postId));
if (getUpvoteCountCallHash === null) setGetUpvoteCountCallHash(getUpvoteCountChainData(postId));
if (getDownvoteCountCallHash === null) setGetDownvoteCountCallHash(getDownvoteCountChainData(postId));
}
}, [drizzleInitializationFailed, drizzleInitialized, getDownvoteCountCallHash,
getTotalVoteCountCallHash, getUpvoteCountCallHash, postId]);
useEffect(() => {
const shouldGetOwnVoteFromChain = ownVote === null;
if (drizzleInitialized && !drizzleInitializationFailed && shouldGetOwnVoteFromChain
&& postId !== null && userAccount !== null && getVoteCallHash === null) {
setGetVoteCallHash(getVoteChainData(postId, userAccount));
}
}, [drizzleInitializationFailed, drizzleInitialized, getVoteCallHash, ownVote, postId, userAccount]);
useEffect(() => {
if (getVoteResult) {
setOwnVote(getVoteResult.value);
}
}, [getVoteResult]);
useEffect(() => {
if (getTotalVoteCountResult) {
setTotalVoteCount(getTotalVoteCountResult.value);
}
}, [getTotalVoteCountResult]);
useEffect(() => {
if (getUpvoteCountResult) {
setUpvoteCount(getUpvoteCountResult.value);
}
}, [getUpvoteCountResult]);
useEffect(() => {
if (getDownvoteCountResult) {
setDownvoteCount(getDownvoteCountResult.value);
}
}, [getDownvoteCountResult]);
// Voting
useEffect(() => {
if (voting && transactionStack && transactionStack[createVoteCacheSendStackId]
&& transactions[transactionStack[createVoteCacheSendStackId]]) {
if (transactions[transactionStack[createVoteCacheSendStackId]].status === TRANSACTION_SUCCESS
|| transactions[transactionStack[createVoteCacheSendStackId]].status === TRANSACTION_ERROR) {
setVoting(false);
}
}
}, [createVoteCacheSendStackId, transactionStack, transactions, voting]);
const vote = useCallback((choice) => {
if (voting) return;
setVoting(true);
if ((ownVote === CHOICE_DEFAULT || ownVote === CHOICE_DOWN) && choice === CHOICE_UP) setVoteCacheSendStackId(upvote.cacheSend(...[postId], { from: userAccount }));
else if ((ownVote === CHOICE_DEFAULT || ownVote === CHOICE_UP) && choice === CHOICE_DOWN) setVoteCacheSendStackId(downvote.cacheSend(...[postId], { from: userAccount }));
else if ((ownVote === CHOICE_UP && choice === CHOICE_UP) || (ownVote === CHOICE_DOWN && choice === CHOICE_DOWN)) setVoteCacheSendStackId(unvote.cacheSend(...[postId], { from: userAccount }));
}, [ownVote, postId, userAccount, voting]);
const disableVoting = userAccount === null || !hasSignedUp || postAuthorAddress === null || userAccount === postAuthorAddress;
return useMemo(() => (
<div className="post-voting">
<Button
compact
size="mini"
icon="arrow down"
negative={ownVote === CHOICE_DOWN}
disabled={disableVoting}
onClick={() => vote(CHOICE_DOWN)}
/>
<Popup
trigger={(
<span className="unselectable">
&nbsp;&nbsp;
{totalVoteCount || 0}
&nbsp;&nbsp;
</span>
)}
disabled={(upvoteCount === null && downvoteCount === null) || (upvoteCount === '0' && downvoteCount === '0')}
position="bottom center"
>
{upvoteCount !== '0' ? (
<span className="upvote-count">
+
{upvoteCount}
&nbsp;&nbsp;
</span>
) : null}
{downvoteCount !== '0' ? (
<span className="downvote-count">
-
{downvoteCount}
</span>
) : null}
</Popup>
<Button
compact
size="mini"
icon="arrow up"
positive={ownVote === CHOICE_UP}
disabled={disableVoting}
onClick={() => vote(CHOICE_UP)}
/>
</div>
), [disableVoting, downvoteCount, ownVote, totalVoteCount, upvoteCount, vote]);
};
PostVoting.propTypes = {
postId: PropTypes.number.isRequired,
postAuthorAddress: PropTypes.string,
totalVoteCount: PropTypes.number,
};
export default memo(PostVoting);

20
packages/concordia-app/src/components/PostList/PostVoting/styles.css

@ -0,0 +1,20 @@
.post-voting {
float: left;
margin-top: 1.2rem;
}
.post-voting > button {
margin: 0 !important;
}
.post-voting > span{
vertical-align: middle;
}
.upvote-count {
color: #21ba45;
}
.downvote-count {
color: #db2828;
}

4
packages/concordia-app/src/components/PostList/index.jsx

@ -3,7 +3,9 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Dimmer, Feed, Loader } from 'semantic-ui-react';
import {
Dimmer, Feed, Loader,
} from 'semantic-ui-react';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import PostListRow from './PostListRow';
import { drizzle } from '../../redux/store';

52
packages/concordia-app/src/components/ProfileImage.jsx

@ -0,0 +1,52 @@
import React, { useMemo } from 'react';
import Avatar from 'react-avatar';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { USER_PROFILE_PICTURE } from '../constants/orbit/UserDatabaseKeys';
const ProfileImage = (props) => {
const {
topicAuthorAddress, topicAuthor, topicAuthorMeta, avatarUrl, size, link,
} = props;
const stopClickPropagation = (event) => {
event.stopPropagation();
};
const authorAvatar = useMemo(() => {
let profileImageUrl = '';
if (avatarUrl) profileImageUrl = avatarUrl;
else if (topicAuthorMeta && topicAuthorMeta[USER_PROFILE_PICTURE]) profileImageUrl = topicAuthorMeta[USER_PROFILE_PICTURE];
return (
<Avatar
name={topicAuthor}
size={size}
round
src={profileImageUrl}
/>
);
}, [avatarUrl, size, topicAuthor, topicAuthorMeta]);
return useMemo(() => {
if (link && topicAuthorAddress) {
return (
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}>
{authorAvatar}
</Link>
);
}
return authorAvatar;
}, [authorAvatar, link, topicAuthorAddress]);
};
ProfileImage.propTypes = {
topicAuthorAddress: PropTypes.string,
topicAuthor: PropTypes.string,
topicAuthorMeta: PropTypes.shape({ id: PropTypes.string, profile_picture: PropTypes.string }),
avatarUrl: PropTypes.string,
size: PropTypes.string,
link: PropTypes.bool,
};
export default ProfileImage;

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

@ -2,7 +2,7 @@ import React, {
memo, useEffect, useMemo, useState,
} from 'react';
import {
Dimmer, Grid, Image, List, Placeholder,
Dimmer, Grid, Icon, Item, List, Placeholder, Segment,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
@ -12,11 +12,11 @@ import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { TOPICS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases';
import ProfileImage from '../../ProfileImage';
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions';
import { breeze } from '../../../redux/store';
import './styles.css';
import determineKVAddress from '../../../utils/orbitUtils';
import { USER_PROFILE_PICTURE } from '../../../constants/orbit/UserDatabaseKeys';
import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys';
const { orbit } = breeze;
@ -42,7 +42,7 @@ const TopicListRow = (props) => {
setTopicAuthorAddress(getTopicResults[topicCallHash].value[0]);
setTopicAuthor(getTopicResults[topicCallHash].value[1]);
setTimeAgo(getTopicResults[topicCallHash].value[2] * 1000);
setNumberOfReplies(getTopicResults[topicCallHash].value[3].length);
setNumberOfReplies(getTopicResults[topicCallHash].value[3].length - 1);
}
}, [getTopicResults, loading, topicCallHash]);
@ -94,87 +94,78 @@ const TopicListRow = (props) => {
event.stopPropagation();
};
const authorAvatar = useMemo(() => (topicAuthorMeta !== null && topicAuthorMeta[USER_PROFILE_PICTURE]
? (
<Image
className="profile-picture"
avatar
src={topicAuthorMeta[USER_PROFILE_PICTURE]}
/>
)
: (
<List.Icon
name="user circle"
size="big"
inverted
color="black"
verticalAlign="middle"
/>
)), [topicAuthorMeta]);
const authorAvatarLink = useMemo(() => {
if (topicAuthorAddress) {
return (
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}>
{authorAvatar}
</Link>
);
}
return authorAvatar;
}, [authorAvatar, topicAuthorAddress]);
return useMemo(() => {
const handleTopicClick = () => {
history.push(`/topics/${topicId}`);
};
return (
<Dimmer.Dimmable as={List.Item} onClick={handleTopicClick} blurring dimmed={loading} className="list-item">
{authorAvatarLink}
<List.Content className="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
? (
<div>
{t('topic.list.row.author', { author: topicAuthor })}
,&nbsp;
<TimeAgo date={timeAgo} />
</div>
)
: <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 as={List.Item} blurring dimmed={loading} className="topic-row" onClick={handleTopicClick}>
<Segment className="topic-row-segment">
<Grid columns={2}>
<Grid.Column width={1} className="topic-row-avatar">
<Item>
<ProfileImage
topicAuthorAddress={topicAuthorAddress}
topicAuthor={topicAuthor}
topicAuthorMeta={topicAuthorMeta}
size="65"
link
/>
</Item>
</Grid.Column>
<Grid.Column width={15} className="topic-row-content">
<Grid verticalAlign="middle" columns={2}>
<Grid.Row>
<Grid.Column floated="left" width={14} className="topic-row-subject">
{topicSubject !== null
? topicSubject
: <Placeholder><Placeholder.Line length="very long" /></Placeholder>}
</Grid.Column>
<Grid.Column floated="right" width={2} textAlign="right">
<span className="topic-row-metadata">
{t('topic.list.row.topic.id', { id: topicId })}
</span>
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column floated="left" width={14}>
{topicAuthor !== null && timeAgo !== null
? (
<div>
<TimeAgo date={timeAgo} />
&nbsp;&nbsp;
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}>
{topicAuthor}
</Link>
</div>
)
: <Placeholder><Placeholder.Line length="long" /></Placeholder>}
</Grid.Column>
<Grid.Column floated="right" width={2} textAlign="right">
{numberOfReplies !== null
? (
<span className="topic-row-metadata">
<Icon name="reply" fitted />
&nbsp;
{ numberOfReplies }
</span>
)
: (
<Placeholder fluid className="replies-placeholder">
<Placeholder.Line />
</Placeholder>
)}
</Grid.Column>
</Grid.Row>
</Grid>
</Grid.Column>
</Grid>
</Segment>
</Dimmer.Dimmable>
);
}, [authorAvatarLink, history, loading, numberOfReplies, t, timeAgo, topicAuthor, topicId, topicSubject]);
}, [history, loading, numberOfReplies, t, timeAgo, topicAuthor, topicAuthorAddress, topicAuthorMeta, topicId, topicSubject]);
};
TopicListRow.defaultProps = {

62
packages/concordia-app/src/components/TopicList/TopicListRow/styles.css

@ -1,21 +1,55 @@
.topic-metadata {
font-size: 12px !important;
font-weight: initial;
}
.list-item {
.topic-row {
display: flex !important;
text-align: start;
}
.profile-picture {
cursor: pointer;
max-width: 36px;
max-height: 36px;
margin: 0;
vertical-align: middle;
}
.list-content {
.topic-row-segment {
flex-grow: 1;
}
.topic-row-segment:hover {
background-color: #F7F7F7;
}
.topic-row-segment div {
color: black;
}
.topic-row-segment .placeholder{
font-size: 0.9rem;
}
.topic-row-segment .placeholder>.line {
background-color:transparent;
}
.topic-row-avatar {
margin: auto;
padding-left: 1rem !important;
padding-right: 0 !important;
font-size: 2rem;
color: red;
}
.topic-row-avatar span{
color: white;
}
.topic-row-content {
padding-left: 2rem !important;
}
.topic-row-subject {
font-size: 1.4rem;
font-weight: bold;
}
.topic-row-metadata {
font-size: 0.9em !important;
}
.replies-placeholder{
float:right;
width: 6rem;
}

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

@ -7,6 +7,7 @@ import { List } from 'semantic-ui-react';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import TopicListRow from './TopicListRow';
import { drizzle } from '../../redux/store';
import './styles.css';
const { contracts: { [FORUM_CONTRACT]: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle;
@ -51,7 +52,7 @@ const TopicList = (props) => {
}), [getTopicCallHashes, topicIds]);
return (
<List selection divided id="topic-list" size="big">
<List id="topic-list" size="big">
{topics}
</List>
);

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

@ -1,3 +1,6 @@
#topic-list{
height: 100%;
margin-bottom: 4em;
clear: both;
}

11
packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx

@ -42,17 +42,6 @@ const MainLayoutMenu = () => {
<img src={appLogo} alt="app_logo" />
</Menu.Item>
<Menu.Menu position="right">
{hasSignedUp && history.location.pathname === '/home' && (
<Menu.Item
link
name="create-topic"
key="create-topic"
onClick={() => history.push('/topics/new')}
position="right"
>
{t('topbar.button.create.topic')}
</Menu.Item>
)}
{hasSignedUp
? (
<Menu.Item

4
packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/styles.css

@ -3,7 +3,7 @@
}
.ui.inverted.menu {
background: #0B2540 !important;
background: var(--secondary-color) !important;
border-radius: 0;
margin-bottom: 5em;
margin-bottom: 4rem;
}

6
packages/concordia-app/src/redux/actions/contractEventActions.js

@ -4,17 +4,23 @@ import {
USER_SIGNED_UP_EVENT,
USERNAME_UPDATED_EVENT,
} from 'concordia-shared/src/constants/contracts/events/ForumContractEvents';
import {
USER_VOTED_POST_EVENT,
} from 'concordia-shared/src/constants/contracts/events/PostVotingContractEvents';
export const FORUM_EVENT_USER_SIGNED_UP = 'FORUM_EVENT_USER_SIGNED_UP';
export const FORUM_EVENT_USERNAME_UPDATED = 'FORUM_EVENT_USERNAME_UPDATED';
export const FORUM_EVENT_TOPIC_CREATED = 'FORUM_EVENT_TOPIC_CREATED';
export const FORUM_EVENT_POST_CREATED = 'FORUM_EVENT_POST_CREATED';
export const POST_VOTING_USER_VOTED_POST = 'POST_VOTING_USER_VOTED_POST';
const eventActionMap = {
[USER_SIGNED_UP_EVENT]: FORUM_EVENT_USER_SIGNED_UP,
[USERNAME_UPDATED_EVENT]: FORUM_EVENT_USERNAME_UPDATED,
[TOPIC_CREATED_EVENT]: FORUM_EVENT_TOPIC_CREATED,
[POST_CREATED_EVENT]: FORUM_EVENT_POST_CREATED,
[USER_VOTED_POST_EVENT]: POST_VOTING_USER_VOTED_POST,
};
export default eventActionMap;

4
packages/concordia-app/src/views/About/index.jsx

@ -6,6 +6,8 @@ import { Container, Image } from 'semantic-ui-react';
import AboutMd from '../../assets/About.md';
import appLogo from '../../assets/images/app_logo_circle.svg';
import './styles.css';
const targetBlank = () => ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
@ -26,7 +28,7 @@ const About = () => {
return (
<Container id="about-container">
<div style={{ textAlign: 'center' }}>
<Image src={appLogo} size="small" centered />
<Image id="app-logo" src={appLogo} size="small" centered />
{`v${process.env.REACT_APP_VERSION}`}
</div>
<ReactMarkdown

3
packages/concordia-app/src/views/About/styles.css

@ -0,0 +1,3 @@
#app-logo{
margin-bottom: 1rem;
}

80
packages/concordia-app/src/views/Home/Board/index.jsx

@ -1,43 +1,63 @@
import React, { useMemo } from 'react';
import { Header } from 'semantic-ui-react';
import { Button, Header } from 'semantic-ui-react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import TopicList from '../../../components/TopicList';
import './styles.css';
const Board = (props) => {
const { numberOfTopics } = props;
const userHasSignedUp = useSelector((state) => state.user.hasSignedUp);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const history = useHistory();
const { t } = useTranslation();
const boardContents = useMemo(() => {
if (numberOfTopics > 0) {
return (<TopicList topicIds={_.rangeRight(0, numberOfTopics)} />);
} if (!userHasSignedUp) {
return (
<div className="vertical-center-in-parent">
<Header textAlign="center" as="h2">
{t('board.header.no.topics.message')}
</Header>
<Header textAlign="center" as="h3">
{t('board.sub.header.no.topics.guest')}
</Header>
</div>
);
}
return (
<div className="vertical-center-in-parent">
<Header textAlign="center" as="h2">
{t('board.header.no.topics.message')}
</Header>
<Header textAlign="center" as="h3">
{t('board.sub.header.no.topics.user')}
</Header>
</div>
);
}, [numberOfTopics, userHasSignedUp, t]);
const boardContents = useMemo(() => (
<>
{hasSignedUp
? (
<Button
id="new-topic-button"
className="primary-button"
content="New Topic"
icon="plus"
labelPosition="left"
positive
onClick={() => history.push('/topics/new')}
/>
)
: null}
{/* eslint-disable-next-line no-nested-ternary */}
{numberOfTopics > 0
? (<TopicList topicIds={_.rangeRight(0, numberOfTopics)} />)
: (!hasSignedUp
? (
<div id="no-topic-message" className="vertical-center-in-parent">
<Header textAlign="center" as="h2">
{t('board.header.no.topics.message')}
</Header>
<Header textAlign="center" as="h3">
{t('board.sub.header.no.topics.guest')}
</Header>
</div>
)
: (
<div id="no-topic-message" className="vertical-center-in-parent">
<Header textAlign="center" as="h2">
{t('board.header.no.topics.message')}
</Header>
<Header textAlign="center" as="h3">
{t('board.sub.header.no.topics.user')}
</Header>
</div>
))}
</>
), [numberOfTopics, hasSignedUp, t, history]);
return (boardContents);
};

9
packages/concordia-app/src/views/Home/Board/styles.css

@ -0,0 +1,9 @@
#no-topic-message {
padding-top: 7rem;
clear: both;
}
#new-topic-button{
float:right;
margin-bottom: 2em;
}

3
packages/concordia-app/src/views/Profile/GeneralTab/EditInformationModal/index.jsx

@ -197,10 +197,11 @@ const EditInformationModal = (props) => {
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button color="black" onClick={onCancel}>
<Button className="secondary-button" onClick={onCancel}>
{t('edit.information.modal.form.cancel.button')}
</Button>
<Button
className="primary-button"
content={t('edit.information.modal.form.submit.button')}
labelPosition="right"
icon="checkmark"

42
packages/concordia-app/src/views/Profile/GeneralTab/index.jsx

@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
Button, Icon, Image, Placeholder, Table,
Button, Icon, Placeholder, Table,
} from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
@ -10,8 +10,9 @@ import determineKVAddress from '../../../utils/orbitUtils';
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions';
import { breeze } from '../../../redux/store';
import { USER_LOCATION, USER_PROFILE_PICTURE } from '../../../constants/orbit/UserDatabaseKeys';
import './styles.css';
import EditInformationModal from './EditInformationModal';
import ProfileImage from '../../../components/ProfileImage';
import './styles.css';
const { orbit } = breeze;
@ -66,24 +67,6 @@ const GeneralTab = (props) => {
}
}, [dispatch, profileAddress, users]);
const authorAvatar = useMemo(() => (profileMetadataFetched && userAvatarUrl
? (
<Image
className="general-tab-profile-picture"
centered
size="tiny"
src={userAvatarUrl}
/>
)
: (
<Icon
name="user circle"
size="massive"
inverted
color="black"
/>
)), [profileMetadataFetched, userAvatarUrl]);
const userLocationCell = useMemo(() => {
if (!profileMetadataFetched) {
return (
@ -123,7 +106,14 @@ const GeneralTab = (props) => {
<Table basic="very" singleLine>
<Table.Body>
<Table.Row textAlign="center">
<Table.Cell colSpan="3">{authorAvatar}</Table.Cell>
<Table.Cell colSpan="3" className="profile-image">
<ProfileImage
topicAuthorAddress={profileAddress}
topicAuthor={username}
avatarUrl={userAvatarUrl}
size="160"
/>
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell><strong>{t('profile.general.tab.username.row.title')}</strong></Table.Cell>
@ -172,7 +162,7 @@ const GeneralTab = (props) => {
<Table.Row>
<Table.Cell><strong>{t('profile.general.tab.registration.date.row.title')}</strong></Table.Cell>
<Table.Cell>
{new Date(userRegistrationTimestamp * 1000).toLocaleString()}
{new Date(userRegistrationTimestamp * 1000).toLocaleString('el-gr', { hour12: false })}
</Table.Cell>
</Table.Row>
</Table.Body>
@ -182,6 +172,8 @@ const GeneralTab = (props) => {
<Table.Row>
<Table.HeaderCell colSpan="2">
<Button
id="edit-info-button"
className="primary-button"
floated="right"
icon
labelPosition="left"
@ -200,11 +192,7 @@ const GeneralTab = (props) => {
</Table>
{isSelf && editInformationModal}
</>
), [
authorAvatar, editInformationModal, isSelf, numberOfPosts, numberOfTopics, profileAddress, profileMetadataFetched,
t, userInfoOrbitAddress, userLocationCell, userPostsOrbitAddress, userRegistrationTimestamp, userTopicsOrbitAddress,
username,
]);
), [editInformationModal, isSelf, numberOfPosts, numberOfTopics, profileAddress, profileMetadataFetched, t, userAvatarUrl, userInfoOrbitAddress, userLocationCell, userPostsOrbitAddress, userRegistrationTimestamp, userTopicsOrbitAddress, username]);
};
GeneralTab.defaultProps = {

11
packages/concordia-app/src/views/Profile/GeneralTab/styles.css

@ -1,6 +1,7 @@
.general-tab-profile-picture {
max-width: 112px;
max-height: 112px;
margin: 0;
vertical-align: middle;
.profile-image{
padding: 3rem !important;
}
#edit-info-button {
margin: 1rem;
}

3
packages/concordia-app/src/views/Profile/index.jsx

@ -12,6 +12,7 @@ import TopicList from '../../components/TopicList';
import PostList from '../../components/PostList';
import GeneralTab from './GeneralTab';
import { GENERAL_TAB, POSTS_TAB, TOPICS_TAB } from '../../constants/ProfileTabs';
import './styles.css';
const { contracts: { [FORUM_CONTRACT]: { methods: { getUser } } } } = drizzle;
@ -105,7 +106,7 @@ const Profile = () => {
}, [generalTab, loading, postsTab, t, topicsTab]);
return useMemo(() => (
<Container id="home-container" textAlign="center">
<Container id="profile-container" textAlign="center">
<Tab panes={panes} />
</Container>
), [panes]);

3
packages/concordia-app/src/views/Profile/styles.css

@ -0,0 +1,3 @@
#profile-container {
height: auto !important;
}

5
packages/concordia-app/src/views/Register/PersonalInformationStep/index.jsx

@ -150,16 +150,15 @@ const PersonalInformationStep = (props) => {
)}
<Card.Content extra>
<Button
color="green"
className="primary-button"
floated="right"
content={t('register.form.personal.information.step.button.submit')}
onClick={handleSubmit}
disabled={!profilePictureUrlValid}
/>
<Button
color="violet"
className="skip-button"
floated="right"
basic
content={t('register.form.personal.information.step.button.skip')}
onClick={goToHomePage}
/>

5
packages/concordia-app/src/views/Register/SignUpStep/index.jsx

@ -94,7 +94,7 @@ const SignUpStep = (props) => {
)}
<Card.Content extra>
<Button
color="green"
className="primary-button"
floated="right"
content={t('register.form.sign.up.step.button.submit')}
onClick={handleSubmit}
@ -102,9 +102,8 @@ const SignUpStep = (props) => {
loading={!usernameIsChecked}
/>
<Button
color="violet"
className="skip-button"
floated="right"
basic
content={t('register.form.sign.up.step.button.guest')}
onClick={goToHomePage}
disabled={signingUp}

4
packages/concordia-app/src/views/Register/index.jsx

@ -5,10 +5,10 @@ import {
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router';
import './styles.css';
import SignUpStep from './SignUpStep';
import PersonalInformationStep from './PersonalInformationStep';
import { REGISTER_STEP_PROFILE_INFORMATION, REGISTER_STEP_SIGNUP } from '../../constants/RegisterSteps';
import './styles.css';
const Register = () => {
const [currentStep, setCurrentStep] = useState('signup');
@ -107,7 +107,7 @@ const Register = () => {
</Card.Content>
<Card.Content extra>
<Button
color="black"
className="secondary-button"
floated="right"
content={t('register.form.button.back')}
onClick={goToHomePage}

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

@ -2,12 +2,11 @@ import React, {
useCallback, useEffect, useState,
} from 'react';
import {
Button, Container, Form, Icon, Input, TextArea,
Button, Container, Form, Header, Icon, Input, TextArea,
} from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router';
import { useSelector } from 'react-redux';
import './styles.css';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { TOPIC_CREATED_EVENT } from 'concordia-shared/src/constants/contracts/events/ForumContractEvents';
import { POSTS_DATABASE, TOPICS_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases';
@ -15,6 +14,7 @@ import { drizzle, breeze } from '../../../redux/store';
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../../constants/TransactionStatus';
import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys';
import { POST_CONTENT } from '../../../constants/orbit/PostsDatabaseKeys';
import './styles.css';
const { contracts: { [FORUM_CONTRACT]: { methods: { createTopic } } } } = drizzle;
const { orbit: { stores } } = breeze;
@ -97,6 +97,7 @@ const TopicCreate = (props) => {
return (
<Container>
<Header id="new-topic-header" as="h2">New Topic</Header>
<Form loading={posting}>
<Form.Field required>
<label htmlFor="form-topic-create-field-subject">
@ -125,23 +126,23 @@ const TopicCreate = (props) => {
onChange={handleSubjectInputChange}
/>
</Form.Field>
<Form.Group>
<Form.Button
animated
key="form-topic-create-button-submit"
type="button"
color="green"
disabled={posting || subjectInput === '' || contentInput === ''}
onClick={validateAndPost}
>
<Button.Content visible>
{t('topic.create.form.post.button')}
</Button.Content>
<Button.Content hidden>
<Icon name="send" />
</Button.Content>
</Form.Button>
</Form.Group>
<Button
id="create-topic-button"
animated
key="form-topic-create-button-submit"
type="button"
className="primary-button"
disabled={posting || subjectInput === '' || contentInput === ''}
onClick={validateAndPost}
>
<Button.Content visible>
{t('topic.create.form.post.button')}
</Button.Content>
<Button.Content hidden>
<Icon name="send" />
</Button.Content>
</Button>
</Form>
</Container>
);

10
packages/concordia-app/src/views/Topic/TopicCreate/styles.css

@ -1,6 +1,16 @@
#new-topic-header {
padding: 2rem;
text-align: center;
}
.form-textarea-required {
color: rgb(159, 58, 56) !important;
outline-color: rgb(159, 58, 56) !important;
border-color: rgb(224, 180, 180) !important;
background-color: rgb(255, 246, 246) !important;
}
#create-topic-button {
float: right;
margin: 1rem 0 4rem 0;
}

111
packages/concordia-app/src/views/Topic/TopicView/index.jsx

@ -2,11 +2,10 @@ import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import {
Container, Dimmer, Icon, Image, Placeholder, Step,
Container, Dimmer, Divider, Header, Icon, Placeholder, Segment,
} from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useHistory } from 'react-router';
import TimeAgo from 'react-timeago';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { TOPICS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases';
import { breeze, drizzle } from '../../../redux/store';
@ -14,7 +13,6 @@ import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationAct
import './styles.css';
import PostList from '../../../components/PostList';
import determineKVAddress from '../../../utils/orbitUtils';
import { USER_PROFILE_PICTURE } from '../../../constants/orbit/UserDatabaseKeys';
import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys';
import PostCreate from '../../../components/PostCreate';
@ -36,9 +34,9 @@ const TopicView = (props) => {
const [getTopicCallHash, setGetTopicCallHash] = useState([]);
const [topicAuthorAddress, setTopicAuthorAddress] = useState(initialTopicAuthorAddress || null);
const [topicAuthor, setTopicAuthor] = useState(initialTopicAuthor || null);
const [topicAuthorMeta, setTopicAuthorMeta] = useState(null);
const [timestamp, setTimestamp] = useState(initialTimestamp || null);
const [postIds, setPostIds] = useState(initialPostIds || null);
const [numberOfReplies, setReplyCount] = useState(0);
const [topicSubject, setTopicSubject] = useState(null);
const history = useHistory();
const dispatch = useDispatch();
@ -66,7 +64,9 @@ const TopicView = (props) => {
setTopicAuthorAddress(getTopicResults[getTopicCallHash].value[0]);
setTopicAuthor(getTopicResults[getTopicCallHash].value[1]);
setTimestamp(getTopicResults[getTopicCallHash].value[2] * 1000);
setPostIds(getTopicResults[getTopicCallHash].value[3].map((postId) => parseInt(postId, 10)));
const postIds = getTopicResults[getTopicCallHash].value[3].map((postId) => parseInt(postId, 10));
setPostIds(postIds);
setReplyCount(postIds.length - 1);
const topicFound = topics
.find((topic) => topic.id === topicId);
@ -89,9 +89,7 @@ const TopicView = (props) => {
const userFound = users
.find((user) => user.id === userOrbitAddress);
if (userFound) {
setTopicAuthorMeta(userFound);
} else {
if (!userFound) {
dispatch({
type: FETCH_USER_DATABASE,
orbit,
@ -115,66 +113,45 @@ const TopicView = (props) => {
}
}, [topicId, topics]);
const stopClickPropagation = (event) => {
event.stopPropagation();
};
return (
<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">
<Link to={`/users/${topicAuthorAddress}`}>
{topicAuthorMeta !== null && topicAuthorMeta[USER_PROFILE_PICTURE]
? (
<Image
avatar
src={topicAuthorMeta[USER_PROFILE_PICTURE]}
/>
)
: (
<Icon
name="user circle"
size="big"
inverted
color="black"
/>
)}
</Link>
<Step.Content>
<Step.Title>
<Link to={`/users/${topicAuthorAddress}`}>
{topicAuthor || (
<Placeholder id="author-placeholder" inverted>
<Placeholder.Line length="full" />
</Placeholder>
)}
</Link>
</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
? <TimeAgo date={timestamp} />
: (
<Placeholder id="date-placeholder">
<Placeholder.Line length="full" />
</Placeholder>
)}
</Step.Description>
</Step.Content>
</Step>
</Step.Group>
</Dimmer.Dimmable>
<PostList postIds={postIds || []} loading={postIds === null} focusOnPost={focusOnPost} />
<Container id="topic-container">
<Segment>
<Dimmer.Dimmable
blurring
dimmed={topicAuthorAddress === null && topicAuthor === null && timestamp === null}
>
<div id="topic-header">
<Header as="h2">
{topicSubject || (
<Placeholder id="subject-placeholder">
<Placeholder.Line />
</Placeholder>
)}
</Header>
<div id="topic-metadata">
<Icon name="calendar alternate" fitted />
&nbsp;
{new Date(timestamp).toLocaleString('el-gr', { hour12: false })}
&nbsp;&nbsp;&nbsp;
<Icon name="user" fitted />
&nbsp;
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}>{ topicAuthor }</Link>
&nbsp;&nbsp;&nbsp;
<Icon name="reply" fitted />
&nbsp;
{ numberOfReplies }
</div>
</div>
<Divider />
</Dimmer.Dimmable>
<PostList postIds={postIds || []} loading={postIds === null} focusOnPost={focusOnPost} />
</Segment>
{topicSubject !== null && postIds !== null && hasSignedUp && (
<PostCreate
topicId={topicId}

14
packages/concordia-app/src/views/Topic/TopicView/styles.css

@ -1,17 +1,29 @@
#topic-container {
height: 100%;
height: auto !important;
}
#topic-header {
padding: 1.2rem 0;
text-align: center;
}
#topic-grid {
height: 100%;
}
#topic-metadata {
font-size: 1rem;
margin-top: 2rem;
color: rgba(0,0,0,.6);
}
#author-placeholder {
width: 150px !important;
}
#subject-placeholder {
width: 250px !important;
margin:auto;
}
#date-placeholder {

6
packages/concordia-contracts/contracts/PostVoting.sol

@ -40,6 +40,12 @@ contract PostVoting {
return (getVoteCount(postID, Option.DOWN));
}
function getTotalVoteCount(uint postID) public view returns (int) {
int upvoteCount = int(getUpvoteCount(postID));
int downvoteCount = int(getDownvoteCount(postID));
return upvoteCount - downvoteCount;
}
// Gets voters for a specific option (Option.UP/ Option.DOWN)
function getVoters(uint postID, Option option) private view returns (address[] memory) {
require(forum.postExists(postID), forum.POST_DOES_NOT_EXIST());

10
packages/concordia-shared/src/constants/contracts/events/PostVotingContractEvents.js

@ -0,0 +1,10 @@
const USER_VOTED_POST_EVENT = 'UserVotedPost';
const postVotingContractEvents = Object.freeze([
USER_VOTED_POST_EVENT,
]);
module.exports = {
USER_VOTED_POST_EVENT,
postVotingContractEvents,
};

6
packages/concordia-shared/src/constants/contracts/events/index.js

@ -1,8 +1,10 @@
const forumContractEvents = require('./ForumContractEvents');
const { FORUM_CONTRACT } = require('../ContractNames');
const { forumContractEvents } = require('./ForumContractEvents');
const { postVotingContractEvents } = require('./PostVotingContractEvents');
const { FORUM_CONTRACT, POST_VOTING_CONTRACT } = require('../ContractNames');
const appEvents = Object.freeze({
[FORUM_CONTRACT]: forumContractEvents,
[POST_VOTING_CONTRACT]: postVotingContractEvents,
});
module.exports = appEvents;

127
yarn.lock

@ -1530,14 +1530,14 @@
level "~6.0.1"
orbit-db-identity-provider "~0.3.1"
"@fluentui/react-component-event-listener@~0.51.0":
"@fluentui/react-component-event-listener@~0.51.6":
version "0.51.7"
resolved "https://registry.yarnpkg.com/@fluentui/react-component-event-listener/-/react-component-event-listener-0.51.7.tgz#158adb970d8bc982c91c57fd1322a0036042d86e"
integrity sha512-NjVm+crN0T9A7vITL8alZeHnuV8zi2gos0nezU/2YOxaUAB9E4zKiPxt/6k5U50rJs/gj8Nu45iXxnjO41HbZg==
dependencies:
"@babel/runtime" "^7.10.4"
"@fluentui/react-component-ref@~0.51.0":
"@fluentui/react-component-ref@~0.51.6":
version "0.51.7"
resolved "https://registry.yarnpkg.com/@fluentui/react-component-ref/-/react-component-ref-0.51.7.tgz#bfb0312e926c213bed35e53ee5105a68732eea99"
integrity sha512-CX27jVJYaFoBCWpuWAizQZ2se137ku1dmDyn8sw+ySNJa+kkQf7LnMydiPW5K7cRdUSqUJW3eS4EjKRvVAx8xA==
@ -2022,6 +2022,11 @@
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.3.0.tgz#ffdb693c5c349fc33bba420248dd3ac0a2d7c408"
integrity sha512-AemZEsQYtUp1WRkcmZm1div5ORfTpLquLaziCIrSagjxyKdmObxuaY1yjQ5SHFMctR8rLwp706NXTbiIRJg7pw==
"@popperjs/core@^2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f"
integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@ -2129,7 +2134,7 @@
redux-thunk "^2.3.0"
reselect "^4.0.0"
"@semantic-ui-react/event-stack@^3.1.0":
"@semantic-ui-react/event-stack@^3.1.2":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@semantic-ui-react/event-stack/-/event-stack-3.1.2.tgz#14fac9796695aa3967962d94ea9733a85325f9c4"
integrity sha512-Yd0Qf7lPCIjzJ9bZYfurlNu2RDXT6KKSyubHfYK3WjRauhxCsq6Fk2LMRI9DEvShoEU+AsLSv3NGkqXAcVp0zg==
@ -4277,6 +4282,11 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
charenc@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=
check-error@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
@ -4808,7 +4818,7 @@ core-js@^2.4.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.5.0:
core-js@^3.5.0, core-js@^3.6.1:
version "3.8.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0"
integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==
@ -4883,14 +4893,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-react-context@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.3.0.tgz#546dede9dc422def0d3fc2fe03afe0bc0f4f7d8c"
integrity sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==
dependencies:
gud "^1.0.0"
warning "^4.0.3"
cross-env@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
@ -4927,6 +4929,11 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
crypt@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b"
integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=
crypto-browserify@3.12.0, crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@ -5315,7 +5322,7 @@ deep-eql@^3.0.1:
dependencies:
type-detect "^4.0.0"
deep-equal@^1.0.1, deep-equal@^1.1.1:
deep-equal@^1.0.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a"
integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==
@ -7639,11 +7646,6 @@ growly@^1.3.0:
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
gzip-size@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
@ -9203,7 +9205,7 @@ is-binary-path@~2.1.0:
dependencies:
binary-extensions "^2.0.0"
is-buffer@^1.0.2, is-buffer@^1.1.5:
is-buffer@^1.0.2, is-buffer@^1.1.5, is-buffer@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
@ -9557,6 +9559,11 @@ is-resolvable@^1.0.0:
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
is-retina@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/is-retina/-/is-retina-1.0.3.tgz#d7401b286bea2ae37f62477588de504d0b8647e3"
integrity sha1-10AbKGvqKuN/Ykd1iN5QTQuGR+M=
is-retry-allowed@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
@ -11618,6 +11625,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.15:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7"
integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@ -11825,6 +11837,15 @@ md5.js@^1.3.4:
inherits "^2.0.1"
safe-buffer "^5.1.2"
md5@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==
dependencies:
charenc "0.0.2"
crypt "0.0.2"
is-buffer "~1.1.6"
mdast-add-list-metadata@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz#95e73640ce2fc1fa2dcb7ec443d09e2bfe7db4cf"
@ -13892,11 +13913,6 @@ pnp-webpack-plugin@1.6.4:
dependencies:
ts-pnp "^1.1.6"
popper.js@^1.14.4:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
portfinder@^1.0.26:
version "1.0.28"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
@ -14740,7 +14756,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2:
prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -15027,6 +15043,15 @@ react-app-polyfill@^1.0.6:
regenerator-runtime "^0.13.3"
whatwg-fetch "^3.0.0"
react-avatar@~3.9.7:
version "3.9.7"
resolved "https://registry.yarnpkg.com/react-avatar/-/react-avatar-3.9.7.tgz#c7eb50d7f827350475ec6041f38fc2dbd249b740"
integrity sha512-UX1prYgo4gS1g2u16tZbx/Vy45M/BxyHHexIoRj6m9hI3ZR0FdHTDt66X5GpTtf6PRYE8KlvwHte1x5n8B0/XQ==
dependencies:
core-js "^3.6.1"
is-retina "^1.0.3"
md5 "^2.0.0"
react-dev-utils@^10.2.1:
version "10.2.1"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.2.1.tgz#f6de325ae25fa4d546d09df4bb1befdc6dd19c19"
@ -15072,6 +15097,11 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
react-fast-compare@^3.0.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
react-i18next@^11.7.3:
version "11.8.5"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.8.5.tgz#a093335822e36252cda6efc0f55facef6253643f"
@ -15085,6 +15115,11 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.6.3, react-is@^16.7.0, react-i
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
"react-is@^16.8.6 || ^17.0.0":
version "17.0.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
react-markdown@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-5.0.3.tgz#41040ea7a9324b564b328fb81dd6c04f2a5373ac"
@ -15109,17 +15144,12 @@ react-particles-js@^3.4.0:
lodash "^4.17.11"
tsparticles "^1.18.10"
react-popper@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.7.tgz#f6a3471362ef1f0d10a4963673789de1baca2324"
integrity sha512-nmqYTx7QVjCm3WUZLeuOomna138R1luC4EqkW3hxJUrAe+3eNz3oFCLYdnPwILfn0mX1Ew2c3wctrjlUMYYUww==
react-popper@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.4.tgz#d2ad3d2474ac9f1abf93df3099d408e5aa6a2e22"
integrity sha512-NacOu4zWupdQjVXq02XpTD3yFPSfg5a7fex0wa3uGKVkFK7UN6LvVxgcb+xYr56UCuWiNPMH20tntdVdJRwYew==
dependencies:
"@babel/runtime" "^7.1.2"
create-react-context "^0.3.0"
deep-equal "^1.1.1"
popper.js "^1.14.4"
prop-types "^15.6.1"
typed-styles "^0.0.7"
react-fast-compare "^3.0.1"
warning "^4.0.2"
react-redux@~7.2.1:
@ -15955,21 +15985,23 @@ semantic-ui-css@~2.4.1:
dependencies:
jquery x.*
semantic-ui-react@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/semantic-ui-react/-/semantic-ui-react-1.2.1.tgz#c58603db072d3628fc2d9b626f6c3b39535deca3"
integrity sha512-p0HKdHg8ZGhnanyNuGOyTp5M6mVGWzTULPY02uBYHxXMzsyRPoRDlUa/tbaU0UdFpg96CmToG4WrvkINqhcZ+Q==
semantic-ui-react@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/semantic-ui-react/-/semantic-ui-react-2.0.3.tgz#39091e24078e28129ff9b1beb7dbfc84ca85544b"
integrity sha512-a0hGN6XXw64sRSKwWqMCKSI/AGLohxNeWuErS39eswvBbUnLjBij8ZoEdiqDiz/PuWpwYIRjgmQVrut+7h3b2g==
dependencies:
"@babel/runtime" "^7.10.5"
"@fluentui/react-component-event-listener" "~0.51.0"
"@fluentui/react-component-ref" "~0.51.0"
"@semantic-ui-react/event-stack" "^3.1.0"
"@fluentui/react-component-event-listener" "~0.51.6"
"@fluentui/react-component-ref" "~0.51.6"
"@popperjs/core" "^2.6.0"
"@semantic-ui-react/event-stack" "^3.1.2"
clsx "^1.1.1"
keyboard-key "^1.1.0"
lodash "^4.17.19"
lodash-es "^4.17.15"
prop-types "^15.7.2"
react-is "^16.8.6"
react-popper "^1.3.7"
react-is "^16.8.6 || ^17.0.0"
react-popper "^2.2.4"
shallowequal "^1.1.0"
semver-diff@^3.1.1:
@ -17372,11 +17404,6 @@ type@^2.0.0:
resolved "https://registry.yarnpkg.com/type/-/type-2.1.0.tgz#9bdc22c648cf8cf86dd23d32336a41cfb6475e3f"
integrity sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==
typed-styles@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
typedarray-to-buffer@^3.1.5, typedarray-to-buffer@~3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
@ -17907,7 +17934,7 @@ walker@^1.0.7, walker@~1.0.5:
dependencies:
makeerror "1.0.x"
warning@^4.0.2, warning@^4.0.3:
warning@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==

Loading…
Cancel
Save