From 8bc5855a3b4cabaa8375bb6e85c254de0aea5be9 Mon Sep 17 00:00:00 2001 From: apostolof Date: Sat, 27 Mar 2021 21:19:50 +0200 Subject: [PATCH 01/13] feat: init poll view --- packages/concordia-app/package.json | 2 + .../public/locales/en/translation.json | 5 + .../components/PollView/PollGraph/index.jsx | 82 +++++++++++++++ .../components/PollView/PollVote/index.jsx | 62 ++++++++++++ .../src/components/PollView/index.jsx | 99 +++++++++++++++++++ .../src/constants/polls/PollGraph.js | 2 + .../src/constants/polls/PollTabs.js | 16 +++ .../src/views/Topic/TopicView/index.jsx | 49 +++++++-- yarn.lock | 79 ++++++++++++++- 9 files changed, 387 insertions(+), 9 deletions(-) create mode 100644 packages/concordia-app/src/components/PollView/PollGraph/index.jsx create mode 100644 packages/concordia-app/src/components/PollView/PollVote/index.jsx create mode 100644 packages/concordia-app/src/components/PollView/index.jsx create mode 100644 packages/concordia-app/src/constants/polls/PollGraph.js create mode 100644 packages/concordia-app/src/constants/polls/PollTabs.js diff --git a/packages/concordia-app/package.json b/packages/concordia-app/package.json index 54356c7..f911dc0 100644 --- a/packages/concordia-app/package.json +++ b/packages/concordia-app/package.json @@ -29,6 +29,7 @@ "@ezerous/eth-identity-provider": "~0.1.2", "@reduxjs/toolkit": "~1.4.0", "@welldone-software/why-did-you-render": "~6.0.5", + "apexcharts": "^3.26.0", "concordia-contracts": "~0.1.0", "concordia-shared": "~0.1.0", "i18next": "^19.8.3", @@ -37,6 +38,7 @@ "lodash": "^4.17.20", "prop-types": "~15.7.2", "react": "~16.13.1", + "react-apexcharts": "^1.3.7", "react-avatar": "~3.9.7", "react-copy-to-clipboard": "^5.0.3", "react-dom": "~16.13.1", diff --git a/packages/concordia-app/public/locales/en/translation.json b/packages/concordia-app/public/locales/en/translation.json index d4661ce..f9799bd 100644 --- a/packages/concordia-app/public/locales/en/translation.json +++ b/packages/concordia-app/public/locales/en/translation.json @@ -74,6 +74,11 @@ "topic.create.form.subject.field.label": "Topic subject", "topic.create.form.subject.field.placeholder": "Subject", "topic.list.row.topic.id": "#{{id}}", + "topic.poll.tab.graph.title": "Results", + "topic.poll.tab.results.votes.count": "{{totalVotes}} votes casted", + "topic.poll.tab.vote.form.button.submit": "Submit", + "topic.poll.tab.vote.form.radio.label": "Select one of the options:", + "topic.poll.tab.vote.title": "Vote", "username.selector.error.username.empty.message": "Username is required", "username.selector.error.username.taken.message": "The username {{username}} is already taken.", "username.selector.username.field.label": "Username", diff --git a/packages/concordia-app/src/components/PollView/PollGraph/index.jsx b/packages/concordia-app/src/components/PollView/PollGraph/index.jsx new file mode 100644 index 0000000..be698a9 --- /dev/null +++ b/packages/concordia-app/src/components/PollView/PollGraph/index.jsx @@ -0,0 +1,82 @@ +import React, { useMemo } from 'react'; +import Chart from 'react-apexcharts'; +import { Grid, Header } from 'semantic-ui-react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import { CASTED_OPTION_COLOR, DEFAULT_OPTION_COLOR } from '../../../constants/polls/PollGraph'; + +const PollGraph = (props) => { + const { + pollOptions, voteCounts, hasUserVoted, userVoteHash, + } = props; + const { t } = useTranslation(); + + const chartOptions = useMemo(() => ({ + chart: { + id: 'topic-poll', + }, + plotOptions: { + bar: { + horizontal: true, + }, + }, + colors: [ + (value) => { + if (hasUserVoted && pollOptions[value.dataPointIndex].hash === userVoteHash) { + return CASTED_OPTION_COLOR; + } + return DEFAULT_OPTION_COLOR; + }, + ], + xaxis: { + categories: pollOptions.map((pollOption) => pollOption.label), + }, + }), [hasUserVoted, pollOptions, userVoteHash]); + + const chartSeries = useMemo(() => [{ + name: 'votes', + data: voteCounts, + }], [voteCounts]); + + return ( + + + + + + + + + + +
+ {t('topic.poll.tab.results.votes.count', { + totalVotes: voteCounts.reduce((accumulator, voteCount) => accumulator + voteCount, 0), + })} +
+
+
+
+ ); +}; + +PollGraph.defaultProps = { + hasUserVoted: false, + userVoteHash: '', +}; + +PollGraph.propTypes = { + pollOptions: PropTypes.arrayOf(PropTypes.exact({ + label: PropTypes.string, + hash: PropTypes.string, + })).isRequired, + voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired, + hasUserVoted: PropTypes.bool, + userVoteHash: PropTypes.string, +}; + +export default PollGraph; diff --git a/packages/concordia-app/src/components/PollView/PollVote/index.jsx b/packages/concordia-app/src/components/PollView/PollVote/index.jsx new file mode 100644 index 0000000..3b4641e --- /dev/null +++ b/packages/concordia-app/src/components/PollView/PollVote/index.jsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { Form } from 'semantic-ui-react'; +import { useTranslation } from 'react-i18next'; + +const PollVote = (props) => { + const { + pollOptions, enableVoteChanges, hasUserVoted, userVoteHash, + } = props; + const [selectedOptionHash, setSelectedOptionHash] = useState(userVoteHash); + const { t } = useTranslation(); + + const onOptionSelected = (e, { value }) => { + setSelectedOptionHash(value); + }; + + const onCastVote = () => { + console.log('vote'); + // TODO + // TODO: callback for immediate poll data refresh on vote? + }; + + return ( +
+ + + {pollOptions.map((pollOption) => ( + + ))} + + + {t('topic.poll.tab.vote.form.button.submit')} + +
+ ); +}; + +PollVote.defaultProps = { + userVoteHash: '', +}; + +PollVote.propTypes = { + pollOptions: PropTypes.arrayOf(PropTypes.exact({ + label: PropTypes.string, + hash: PropTypes.string, + })).isRequired, + enableVoteChanges: PropTypes.bool.isRequired, + hasUserVoted: PropTypes.bool.isRequired, + userVoteHash: PropTypes.string, +}; + +export default PollVote; diff --git a/packages/concordia-app/src/components/PollView/index.jsx b/packages/concordia-app/src/components/PollView/index.jsx new file mode 100644 index 0000000..6c65d05 --- /dev/null +++ b/packages/concordia-app/src/components/PollView/index.jsx @@ -0,0 +1,99 @@ +import React, { useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; +import { + Container, Header, Icon, Tab, +} from 'semantic-ui-react'; +import { useTranslation } from 'react-i18next'; +import { drizzle } from '../../redux/store'; +import PollGraph from './PollGraph'; +import CustomLoadingTabPane from '../CustomLoadingTabPane'; +import { GRAPH_TAB, VOTE_TAB } from '../../constants/polls/PollTabs'; +import PollVote from './PollVote'; + +const { contracts: { [VOTING_CONTRACT]: { methods: { pollExists: { cacheCall: pollExistsChainData } } } } } = drizzle; + +const hashOption = (val) => { + let hash = 0; + let i; + let chr; + + for (i = 0; i < val.length; i++) { + chr = val.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; + } + + return `${hash}`; +}; + +const PollView = (props) => { + const { topicId } = props; + const userAddress = useSelector((state) => state.user.address); + const hasSignedUp = useSelector((state) => state.user.hasSignedUp); + const getPollInfoResults = useSelector((state) => state.contracts[VOTING_CONTRACT].getPollInfo); + const [getPollInfoCallHash, setGetPollInfoCallHash] = useState(null); + const [pollOptions, setPollOptions] = useState([ + { + label: 'option 1', + hash: hashOption('option 1'), + }, + { + label: 'option 2', + hash: hashOption('option 2'), + }, + { + label: 'option 3', + hash: hashOption('option 3'), + }, + { + label: 'option 4', + hash: hashOption('option 4'), + }, + { + label: 'option 5', + hash: hashOption('option 5'), + }, + + ]); + const [voteCounts, setVoteCounts] = useState([2, 8, 4, 12, 7]); + const [voteLoading, setVoteLoading] = useState(false); + const [resultsLoading, setResultsLoading] = useState(false); + const { t } = useTranslation(); + + // TODO: get vote options + + // TODO: get current results + + // TODO: check poll hash validity, add invalid view + + const pollVoteTab = useMemo(() => ( + + ), [pollOptions]); + + const pollGraphTab = useMemo(() => ( + + ), [pollOptions, voteCounts]); + + const panes = useMemo(() => { + const pollVotePane = ({pollVoteTab}); + const pollGraphPane = ({pollGraphTab}); + + return ([ + { menuItem: t(VOTE_TAB.intl_display_name_id), render: () => pollVotePane }, + { menuItem: t(GRAPH_TAB.intl_display_name_id), render: () => pollGraphPane }, + ]); + }, [pollGraphTab, pollVoteTab, resultsLoading, t, voteLoading]); + + return ( + +
+ + Do you thing asdf or fdsa? +
+ +
+ ); +}; + +export default PollView; diff --git a/packages/concordia-app/src/constants/polls/PollGraph.js b/packages/concordia-app/src/constants/polls/PollGraph.js new file mode 100644 index 0000000..a19a2ee --- /dev/null +++ b/packages/concordia-app/src/constants/polls/PollGraph.js @@ -0,0 +1,2 @@ +export const DEFAULT_OPTION_COLOR = '#3B5066'; +export const CASTED_OPTION_COLOR = '#0b2540'; diff --git a/packages/concordia-app/src/constants/polls/PollTabs.js b/packages/concordia-app/src/constants/polls/PollTabs.js new file mode 100644 index 0000000..b48a05e --- /dev/null +++ b/packages/concordia-app/src/constants/polls/PollTabs.js @@ -0,0 +1,16 @@ +export const VOTE_TAB = { + id: 'vote-tab', + intl_display_name_id: 'topic.poll.vote.tab.title', +}; + +export const GRAPH_TAB = { + id: 'graph-tab', + intl_display_name_id: 'topic.poll.graph.tab.title', +}; + +const pollTabs = [ + VOTE_TAB, + GRAPH_TAB, +]; + +export default pollTabs; diff --git a/packages/concordia-app/src/views/Topic/TopicView/index.jsx b/packages/concordia-app/src/views/Topic/TopicView/index.jsx index 602fa54..8e1f6d3 100644 --- a/packages/concordia-app/src/views/Topic/TopicView/index.jsx +++ b/packages/concordia-app/src/views/Topic/TopicView/index.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { @@ -6,19 +6,25 @@ import { } from 'semantic-ui-react'; import { Link } from 'react-router-dom'; import { useHistory } from 'react-router'; -import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; +import { FORUM_CONTRACT, VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { TOPICS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases'; import ReactMarkdown from 'react-markdown'; import { breeze, drizzle } from '../../../redux/store'; import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions'; import './styles.css'; import TopicPostList from './TopicPostList'; +import PollView from '../../../components/PollView'; import determineKVAddress from '../../../utils/orbitUtils'; import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys'; import PostCreate from '../../../components/PostCreate'; import targetBlank from '../../../utils/markdownUtils'; -const { contracts: { [FORUM_CONTRACT]: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle; +const { + contracts: { + [FORUM_CONTRACT]: { methods: { getTopic: { cacheCall: getTopicChainData } } }, + [VOTING_CONTRACT]: { methods: { pollExists: { cacheCall: pollExistsChainData } } }, + }, +} = drizzle; const { orbit } = breeze; const TopicView = (props) => { @@ -29,15 +35,18 @@ const TopicView = (props) => { const userAddress = useSelector((state) => state.user.address); const hasSignedUp = useSelector((state) => state.user.hasSignedUp); const getTopicResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getTopic); + const pollExistsResults = useSelector((state) => state.contracts[VOTING_CONTRACT].pollExists); const topics = useSelector((state) => state.orbitData.topics); const users = useSelector((state) => state.orbitData.users); - const [getTopicCallHash, setGetTopicCallHash] = useState([]); + const [getTopicCallHash, setGetTopicCallHash] = useState(null); + const [pollExistsCallHash, setPollExistsCallHash] = useState(null); 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 [numberOfReplies, setReplyCount] = useState(0); const [topicSubject, setTopicSubject] = useState(null); + const [hasPoll, setHasPoll] = useState(false); const history = useHistory(); const dispatch = useDispatch(); @@ -52,6 +61,10 @@ const TopicView = (props) => { } }, [postIds, timestamp, topicAuthor, topicAuthorAddress, topicId]); + useEffect(() => { + setPollExistsCallHash(pollExistsChainData(topicId)); + }, [topicId]); + useEffect(() => { if (getTopicCallHash && getTopicResults && getTopicResults[getTopicCallHash]) { if (getTopicResults[getTopicCallHash].value == null) { @@ -62,9 +75,9 @@ const TopicView = (props) => { setTopicAuthorAddress(getTopicResults[getTopicCallHash].value[0]); setTopicAuthor(getTopicResults[getTopicCallHash].value[1]); setTimestamp(getTopicResults[getTopicCallHash].value[2] * 1000); - const postIds = getTopicResults[getTopicCallHash].value[3].map((postId) => parseInt(postId, 10)); - setPostIds(postIds); - setReplyCount(postIds.length - 1); + const fetchedPostIds = getTopicResults[getTopicCallHash].value[3].map((postId) => parseInt(postId, 10)); + setPostIds(fetchedPostIds); + setReplyCount(fetchedPostIds.length - 1); const topicFound = topics .find((topic) => topic.id === topicId); @@ -80,6 +93,12 @@ const TopicView = (props) => { } }, [dispatch, getTopicCallHash, getTopicResults, history, topicId, topics, userAddress]); + useEffect(() => { + if (pollExistsCallHash && pollExistsResults && pollExistsResults[pollExistsCallHash]) { + setHasPoll(pollExistsResults[pollExistsCallHash].value); + } + }, [pollExistsCallHash, pollExistsResults, topicId]); + useEffect(() => { if (topicAuthorAddress !== null) { determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress: topicAuthorAddress }) @@ -111,6 +130,8 @@ const TopicView = (props) => { } }, [topicId, topics]); + const poll = useMemo(() => hasPoll && , [hasPoll]); + const stopClickPropagation = (event) => { event.stopPropagation(); }; @@ -147,7 +168,9 @@ const TopicView = (props) => {       - { topicAuthor } + + {topicAuthor} +       @@ -155,6 +178,16 @@ const TopicView = (props) => { + { + hasPoll && ( + <> +
+ {poll} +
+ + + ) + } diff --git a/yarn.lock b/yarn.lock index 4dc7c8e..03d6ff4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3084,6 +3084,18 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +apexcharts@^3.26.0: + version "3.26.0" + resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.26.0.tgz#a78abc108b2e1b3086a738f0ec7c98e292f4a14b" + integrity sha512-zdYHs3k3tgmCn1BpYLj7rhGEndBYF33Pq1+g0ora37xAr+3act5CJrpdXM2jx2boVUyXgavoSp6sa8WpK7RkSA== + dependencies: + svg.draggable.js "^2.2.2" + svg.easing.js "^2.0.0" + svg.filter.js "^2.0.2" + svg.pathmorphing.js "^0.1.3" + svg.resize.js "^1.4.3" + svg.select.js "^3.0.1" + app-module-path@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5" @@ -14910,7 +14922,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2: +prop-types@^15.5.7, prop-types@^15.5.8, 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== @@ -15185,6 +15197,13 @@ rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-apexcharts@^1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/react-apexcharts/-/react-apexcharts-1.3.7.tgz#42c8785e260535a4f8db1aadbe7b66552770944d" + integrity sha512-2OFhEHd70/WHN0kmrJtVx37UfaL71ZogVkwezmDqwQWgwhK6upuhlnEEX7tEq4xvjA+RFDn6hiUTNIuC/Q7Zqw== + dependencies: + prop-types "^15.5.7" + react-app-polyfill@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0" @@ -16469,6 +16488,9 @@ snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" snapdragon-node@^2.0.1: version "2.1.1" @@ -17163,6 +17185,61 @@ svg-parser@^2.0.0: resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== +svg.draggable.js@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba" + integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw== + dependencies: + svg.js "^2.0.1" + +svg.easing.js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12" + integrity sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI= + dependencies: + svg.js ">=2.3.x" + +svg.filter.js@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203" + integrity sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM= + dependencies: + svg.js "^2.2.5" + +svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d" + integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA== + +svg.pathmorphing.js@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65" + integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww== + dependencies: + svg.js "^2.4.0" + +svg.resize.js@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332" + integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw== + dependencies: + svg.js "^2.6.5" + svg.select.js "^2.1.2" + +svg.select.js@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73" + integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ== + dependencies: + svg.js "^2.2.5" + +svg.select.js@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917" + integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw== + dependencies: + svg.js "^2.6.5" + svgo@^1.0.0, svgo@^1.2.2: version "1.3.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" From 1e1c1cd3afec31a964bb5417220e7d49055de184 Mon Sep 17 00:00:00 2001 From: apostolof Date: Sat, 27 Mar 2021 21:29:06 +0200 Subject: [PATCH 02/13] fix: fix tab titles intl --- packages/concordia-app/src/constants/polls/PollTabs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/concordia-app/src/constants/polls/PollTabs.js b/packages/concordia-app/src/constants/polls/PollTabs.js index b48a05e..b37feba 100644 --- a/packages/concordia-app/src/constants/polls/PollTabs.js +++ b/packages/concordia-app/src/constants/polls/PollTabs.js @@ -1,11 +1,11 @@ export const VOTE_TAB = { id: 'vote-tab', - intl_display_name_id: 'topic.poll.vote.tab.title', + intl_display_name_id: 'topic.poll.tab.vote.title', }; export const GRAPH_TAB = { id: 'graph-tab', - intl_display_name_id: 'topic.poll.graph.tab.title', + intl_display_name_id: 'topic.poll.tab.graph.title', }; const pollTabs = [ From f848f987a530f6153195372a731e3485c84b3467 Mon Sep 17 00:00:00 2001 From: apostolof Date: Sun, 28 Mar 2021 16:51:52 +0300 Subject: [PATCH 03/13] feat: add contract method for getting the full poll struct --- .../concordia-contracts/contracts/Voting.sol | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/concordia-contracts/contracts/Voting.sol b/packages/concordia-contracts/contracts/Voting.sol index b4ce20d..129e4f6 100644 --- a/packages/concordia-contracts/contracts/Voting.sol +++ b/packages/concordia-contracts/contracts/Voting.sol @@ -68,7 +68,41 @@ contract Voting { ); } - function isOptionValid(uint topicID, uint option) public view returns (bool) { + function getPoll(uint topicID) public view returns (uint, string memory, bool, uint, uint[] memory, address[] memory, uint) { + require(pollExists(topicID), POLL_DOES_NOT_EXIST); + + uint totalVotes = getTotalVotes(topicID); + uint[] memory voteCounts = getVoteCounts(topicID); + address[] memory voters = getSerializedVoters(topicID, voteCounts, totalVotes); + + return ( + polls[topicID].numOptions, + polls[topicID].dataHash, + polls[topicID].enableVoteChanges, + polls[topicID].timestamp, + voteCounts, + voters, + totalVotes + ); + } + + function getSerializedVoters(uint topicID, uint[] memory voteCounts, uint totalVotes) private view returns (address[] memory) { + + address[] memory voters = new address[](totalVotes); + uint serializationIndex = 0; + + for (uint pollOption = 1; pollOption < polls[topicID].numOptions; pollOption++) { + address[] memory optionVoters = getVoters(topicID, pollOption); + + for (uint voteIndex = 0; voteIndex < voteCounts[pollOption - 1]; voteIndex++) { + voters[serializationIndex++] = optionVoters[voteIndex]; + } + } + + return (voters); + } + + function isOptionValid(uint topicID, uint option) private view returns (bool) { require(pollExists(topicID), POLL_DOES_NOT_EXIST); if (option <= polls[topicID].numOptions) // Option 0 is valid as well (no option chosen) return true; @@ -93,6 +127,18 @@ contract Voting { return (polls[topicID].voters[option].length); } + function getVoteCounts(uint topicID) public view returns (uint[] memory) { + require(pollExists(topicID), POLL_DOES_NOT_EXIST); + + uint[] memory voteCounts = new uint[](polls[topicID].numOptions); + + for (uint pollOption = 1; pollOption < polls[topicID].numOptions; pollOption++) { + voteCounts[pollOption - 1] = getVoteCount(topicID, pollOption); + } + + return voteCounts; + } + function getTotalVotes(uint topicID) public view returns (uint) { require(pollExists(topicID), POLL_DOES_NOT_EXIST); @@ -111,7 +157,7 @@ contract Voting { return (polls[topicID].voters[option]); } - function getVoterIndex(uint topicID, address voter) public view returns (uint) { + function getVoterIndex(uint topicID, address voter) private view returns (uint) { require(pollExists(topicID), POLL_DOES_NOT_EXIST); require(hasVoted(topicID, voter), USER_HAS_NOT_VOTED); Poll storage poll = polls[topicID]; From 76c78d754c18216b6d29586f979960eca135a88e Mon Sep 17 00:00:00 2001 From: apostolof Date: Sun, 28 Mar 2021 16:52:12 +0300 Subject: [PATCH 04/13] feat: add poll DBs replication in saga --- .../reducers/peerDbReplicationReducer.js | 8 +++- .../src/redux/sagas/peerDbReplicationSaga.js | 41 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/concordia-app/src/redux/reducers/peerDbReplicationReducer.js b/packages/concordia-app/src/redux/reducers/peerDbReplicationReducer.js index 117d442..9795819 100644 --- a/packages/concordia-app/src/redux/reducers/peerDbReplicationReducer.js +++ b/packages/concordia-app/src/redux/reducers/peerDbReplicationReducer.js @@ -4,13 +4,16 @@ const initialState = { users: [], topics: [], posts: [], + polls: [], }; const peerDbReplicationReducer = (state = initialState, action) => { const { type } = action; if (type === UPDATE_ORBIT_DATA) { - const { users, topics, posts } = action; + const { + users, topics, posts, polls, + } = action; return { ...state, @@ -23,6 +26,9 @@ const peerDbReplicationReducer = (state = initialState, action) => { posts: [ ...posts, ], + polls: [ + ...polls, + ], }; } diff --git a/packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js b/packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js index 251a283..b64d91d 100644 --- a/packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js +++ b/packages/concordia-app/src/redux/sagas/peerDbReplicationSaga.js @@ -7,12 +7,18 @@ import { ORBIT_DB_REPLICATED, ORBIT_DB_WRITE, } from '@ezerous/breeze/src/orbit/orbitActions'; -import { POSTS_DATABASE, TOPICS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases'; +import { + POLLS_DATABASE, + POSTS_DATABASE, + TOPICS_DATABASE, + USER_DATABASE, +} from 'concordia-shared/src/constants/orbit/OrbitDatabases'; import determineKVAddress from '../../utils/orbitUtils'; import { FETCH_USER_DATABASE, UPDATE_ORBIT_DATA } from '../actions/peerDbReplicationActions'; import userDatabaseKeys from '../../constants/orbit/UserDatabaseKeys'; import { TOPIC_SUBJECT } from '../../constants/orbit/TopicsDatabaseKeys'; import { POST_CONTENT } from '../../constants/orbit/PostsDatabaseKeys'; +import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; function* fetchUserDb({ orbit, userAddress, dbName }) { const peerDbAddress = yield call(determineKVAddress, { @@ -23,10 +29,13 @@ function* fetchUserDb({ orbit, userAddress, dbName }) { } function* updateReduxState({ database }) { - const { users, topics, posts } = yield select((state) => ({ + const { + users, topics, posts, polls, + } = yield select((state) => ({ users: state.orbitData.users, topics: state.orbitData.topics, posts: state.orbitData.posts, + polls: state.orbitData.polls, })); if (database.dbname === USER_DATABASE) { @@ -53,6 +62,7 @@ function* updateReduxState({ database }) { ], topics: [...topics], posts: [...posts], + polls: [...polls], }); } @@ -76,6 +86,7 @@ function* updateReduxState({ database }) { })), ], posts: [...posts], + polls: [...polls], }); } @@ -97,6 +108,32 @@ function* updateReduxState({ database }) { [POST_CONTENT]: value[POST_CONTENT], })), ], + polls: [...polls], + }); + } + + if (database.dbname === POLLS_DATABASE) { + const oldPollsUnchanged = polls + .filter((poll) => !Object + .keys(database.all) + .map((key) => parseInt(key, 10)) + .includes(poll.id)); + + yield put({ + type: UPDATE_ORBIT_DATA, + users: [...users], + topics: [...topics], + posts: [...posts], + polls: [ + ...oldPollsUnchanged, + ...Object.entries(database.all).map(([key, value]) => ({ + id: parseInt(key, 10), + [POLL_QUESTION]: value[POLL_QUESTION], + [POLL_OPTIONS]: [ + ...value[POLL_OPTIONS], + ], + })), + ], }); } } From cf55e75501030d252693f28bc24e12f8264c0e59 Mon Sep 17 00:00:00 2001 From: apostolof Date: Sun, 28 Mar 2021 16:59:03 +0300 Subject: [PATCH 05/13] refactor: export function for poll hash generation --- .../concordia-app/src/components/PollCreate/index.jsx | 4 ++-- packages/concordia-app/src/utils/hashUtils.js | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/concordia-app/src/components/PollCreate/index.jsx b/packages/concordia-app/src/components/PollCreate/index.jsx index f631cdc..1b0219e 100644 --- a/packages/concordia-app/src/components/PollCreate/index.jsx +++ b/packages/concordia-app/src/components/PollCreate/index.jsx @@ -15,7 +15,7 @@ import { breeze, drizzle } from '../../redux/store'; import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus'; import './styles.css'; import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; -import generateHash from '../../utils/hashUtils'; +import generatePollHash from '../../utils/hashUtils'; const { contracts: { [VOTING_CONTRACT]: { methods: { createPoll } } } } = drizzle; const { orbit: { stores } } = breeze; @@ -96,7 +96,7 @@ const PollCreate = forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ createPoll(topicId) { setCreating(true); - const dataHash = generateHash(JSON.stringify({ question, optionValues })); + const dataHash = generatePollHash(question, optionValues); setCreatePollCacheSendStackId(createPoll.cacheSend( ...[topicId, options.length, dataHash, allowVoteChanges], { from: account }, )); diff --git a/packages/concordia-app/src/utils/hashUtils.js b/packages/concordia-app/src/utils/hashUtils.js index 3a6d269..a4d31d1 100644 --- a/packages/concordia-app/src/utils/hashUtils.js +++ b/packages/concordia-app/src/utils/hashUtils.js @@ -1,7 +1,8 @@ import sha256 from 'crypto-js/sha256'; -function generateHash(message) { - return sha256(message).toString().substring(0, 16); -} +const generateHash = (message) => sha256(message).toString().substring(0, 16); -export default generateHash; +const generatePollHash = (pollQuestion, pollOptions) => generateHash(JSON + .stringify({ question: pollQuestion, optionValues: pollOptions })); + +export default generatePollHash; From e4967cc9d06ef708b0ee68769eca4d4c32130f8b Mon Sep 17 00:00:00 2001 From: apostolof Date: Sun, 28 Mar 2021 17:44:35 +0300 Subject: [PATCH 06/13] feat: implement poll data fetching --- .../src/components/PollCreate/index.jsx | 2 +- .../src/components/PollView/index.jsx | 171 ++++++++++++------ packages/concordia-app/src/utils/hashUtils.js | 6 +- .../src/views/Topic/TopicView/index.jsx | 8 +- 4 files changed, 123 insertions(+), 64 deletions(-) diff --git a/packages/concordia-app/src/components/PollCreate/index.jsx b/packages/concordia-app/src/components/PollCreate/index.jsx index 1b0219e..453c405 100644 --- a/packages/concordia-app/src/components/PollCreate/index.jsx +++ b/packages/concordia-app/src/components/PollCreate/index.jsx @@ -15,7 +15,7 @@ import { breeze, drizzle } from '../../redux/store'; import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus'; import './styles.css'; import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; -import generatePollHash from '../../utils/hashUtils'; +import { generatePollHash } from '../../utils/hashUtils'; const { contracts: { [VOTING_CONTRACT]: { methods: { createPoll } } } } = drizzle; const { orbit: { stores } } = breeze; diff --git a/packages/concordia-app/src/components/PollView/index.jsx b/packages/concordia-app/src/components/PollView/index.jsx index 6c65d05..5698f64 100644 --- a/packages/concordia-app/src/components/PollView/index.jsx +++ b/packages/concordia-app/src/components/PollView/index.jsx @@ -1,89 +1,144 @@ -import React, { useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; +import React, { useEffect, useMemo, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { Container, Header, Icon, Tab, } from 'semantic-ui-react'; import { useTranslation } from 'react-i18next'; -import { drizzle } from '../../redux/store'; +import PropTypes from 'prop-types'; +import { POLLS_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases'; +import { breeze, drizzle } from '../../redux/store'; import PollGraph from './PollGraph'; import CustomLoadingTabPane from '../CustomLoadingTabPane'; import { GRAPH_TAB, VOTE_TAB } from '../../constants/polls/PollTabs'; import PollVote from './PollVote'; +import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions'; +import { generatePollHash, generateHash } from '../../utils/hashUtils'; +import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; -const { contracts: { [VOTING_CONTRACT]: { methods: { pollExists: { cacheCall: pollExistsChainData } } } } } = drizzle; - -const hashOption = (val) => { - let hash = 0; - let i; - let chr; - - for (i = 0; i < val.length; i++) { - chr = val.charCodeAt(i); - hash = ((hash << 5) - hash) + chr; - hash |= 0; - } - - return `${hash}`; -}; +const { contracts: { [VOTING_CONTRACT]: { methods: { getPoll: { cacheCall: getPollChainData } } } } } = drizzle; +const { orbit } = breeze; const PollView = (props) => { const { topicId } = props; const userAddress = useSelector((state) => state.user.address); const hasSignedUp = useSelector((state) => state.user.hasSignedUp); - const getPollInfoResults = useSelector((state) => state.contracts[VOTING_CONTRACT].getPollInfo); - const [getPollInfoCallHash, setGetPollInfoCallHash] = useState(null); - const [pollOptions, setPollOptions] = useState([ - { - label: 'option 1', - hash: hashOption('option 1'), - }, - { - label: 'option 2', - hash: hashOption('option 2'), - }, - { - label: 'option 3', - hash: hashOption('option 3'), - }, - { - label: 'option 4', - hash: hashOption('option 4'), - }, - { - label: 'option 5', - hash: hashOption('option 5'), - }, - - ]); - const [voteCounts, setVoteCounts] = useState([2, 8, 4, 12, 7]); - const [voteLoading, setVoteLoading] = useState(false); - const [resultsLoading, setResultsLoading] = useState(false); + const getPollResults = useSelector((state) => state.contracts[VOTING_CONTRACT].getPoll); + const polls = useSelector((state) => state.orbitData.polls); + const [getPollCallHash, setGetPollCallHash] = useState(null); + const [pollHash, setPollHash] = useState(''); + const [pollChangeVoteEnabled, setPollChangeVoteEnabled] = useState(false); + const [pollOptions, setPollOptions] = useState([]); + const [voteCounts, setVoteCounts] = useState([]); + const [voters, setVoters] = useState([]); + const [pollHashValid, setPollHashValid] = useState(false); + const [loading, setLoading] = useState(true); + const dispatch = useDispatch(); const { t } = useTranslation(); - // TODO: get vote options + useEffect(() => { + if (!getPollCallHash) { + setGetPollCallHash(getPollChainData(topicId)); + } + }, [getPollCallHash, topicId]); + + useEffect(() => { + dispatch({ + type: FETCH_USER_DATABASE, + orbit, + dbName: POLLS_DATABASE, + userAddress, + }); + }, [dispatch, userAddress]); + + useEffect(() => { + if (getPollCallHash && getPollResults && getPollResults[getPollCallHash]) { + setPollHash(getPollResults[getPollCallHash].value[1]); + setPollChangeVoteEnabled(getPollResults[getPollCallHash].value[2]); + setVoteCounts(getPollResults[getPollCallHash].value[4].map((voteCount) => parseInt(voteCount, 10))); + + const cumulativeSum = getPollResults[getPollCallHash].value[4] + .map((voteCount) => parseInt(voteCount, 10)) + .reduce((accumulator, voteCount) => (accumulator.length === 0 + ? [voteCount] + : [...accumulator, accumulator[accumulator.length - 1] + voteCount]), []); + + setVoters(cumulativeSum + .map((subArrayEnd, index) => getPollResults[getPollCallHash].value[5] + .slice(index > 0 ? cumulativeSum[index - 1] : 0, + subArrayEnd))); + } + }, [getPollCallHash, getPollResults]); - // TODO: get current results + useEffect(() => { + const pollFound = polls + .find((poll) => poll.id === topicId); - // TODO: check poll hash validity, add invalid view + if (pollHash && pollFound) { + if (generatePollHash(pollFound[POLL_QUESTION], pollFound[POLL_OPTIONS]) === pollHash) { + setPollHashValid(true); + setPollOptions(pollFound[POLL_OPTIONS].map((pollOption) => ({ + label: pollOption, + hash: generateHash(pollOption), + }))); + } else { + setPollHashValid(false); + } + + setLoading(false); + } + }, [pollHash, polls, topicId]); + + // TODO: add a "Signup to enable voting" view + + const userHasVoted = useMemo(() => hasSignedUp && voters + .some((optionVoters) => optionVoters.includes(userAddress)), + [hasSignedUp, userAddress, voters]); + + const userVoteHash = useMemo(() => { + if (userHasVoted) { + return pollOptions[voters + .findIndex((optionVoters) => optionVoters.includes(userAddress))].hash; + } + + return ''; + }, [pollOptions, userAddress, userHasVoted, voters]); const pollVoteTab = useMemo(() => ( - - ), [pollOptions]); + !loading + ? ( + + ) + :
+ ), [loading, pollChangeVoteEnabled, pollOptions, userHasVoted, userVoteHash]); const pollGraphTab = useMemo(() => ( - - ), [pollOptions, voteCounts]); + !loading + ? ( + + ) + :
+ ), [loading, pollOptions, userHasVoted, userVoteHash, voteCounts]); const panes = useMemo(() => { - const pollVotePane = ({pollVoteTab}); - const pollGraphPane = ({pollGraphTab}); + const pollVotePane = ({pollVoteTab}); + const pollGraphPane = ({pollGraphTab}); return ([ { menuItem: t(VOTE_TAB.intl_display_name_id), render: () => pollVotePane }, { menuItem: t(GRAPH_TAB.intl_display_name_id), render: () => pollGraphPane }, ]); - }, [pollGraphTab, pollVoteTab, resultsLoading, t, voteLoading]); + }, [loading, pollGraphTab, pollVoteTab, t]); return ( @@ -96,4 +151,8 @@ const PollView = (props) => { ); }; +PollView.propTypes = { + topicId: PropTypes.number.isRequired, +}; + export default PollView; diff --git a/packages/concordia-app/src/utils/hashUtils.js b/packages/concordia-app/src/utils/hashUtils.js index a4d31d1..f544be3 100644 --- a/packages/concordia-app/src/utils/hashUtils.js +++ b/packages/concordia-app/src/utils/hashUtils.js @@ -1,8 +1,6 @@ import sha256 from 'crypto-js/sha256'; -const generateHash = (message) => sha256(message).toString().substring(0, 16); +export const generateHash = (message) => sha256(message).toString().substring(0, 16); -const generatePollHash = (pollQuestion, pollOptions) => generateHash(JSON +export const generatePollHash = (pollQuestion, pollOptions) => generateHash(JSON .stringify({ question: pollQuestion, optionValues: pollOptions })); - -export default generatePollHash; diff --git a/packages/concordia-app/src/views/Topic/TopicView/index.jsx b/packages/concordia-app/src/views/Topic/TopicView/index.jsx index 8e1f6d3..7546616 100644 --- a/packages/concordia-app/src/views/Topic/TopicView/index.jsx +++ b/packages/concordia-app/src/views/Topic/TopicView/index.jsx @@ -62,8 +62,10 @@ const TopicView = (props) => { }, [postIds, timestamp, topicAuthor, topicAuthorAddress, topicId]); useEffect(() => { - setPollExistsCallHash(pollExistsChainData(topicId)); - }, [topicId]); + if (!pollExistsCallHash) { + setPollExistsCallHash(pollExistsChainData(topicId)); + } + }, [pollExistsCallHash, topicId]); useEffect(() => { if (getTopicCallHash && getTopicResults && getTopicResults[getTopicCallHash]) { @@ -130,7 +132,7 @@ const TopicView = (props) => { } }, [topicId, topics]); - const poll = useMemo(() => hasPoll && , [hasPoll]); + const poll = useMemo(() => hasPoll && , [hasPoll, topicId]); const stopClickPropagation = (event) => { event.stopPropagation(); From 7552a67001013b52056dd4d9c81419832f7456cf Mon Sep 17 00:00:00 2001 From: apostolof Date: Sun, 28 Mar 2021 17:59:33 +0300 Subject: [PATCH 07/13] refactor: add placeholder for poll question --- .../src/components/PollView/index.jsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/concordia-app/src/components/PollView/index.jsx b/packages/concordia-app/src/components/PollView/index.jsx index 5698f64..2014cf7 100644 --- a/packages/concordia-app/src/components/PollView/index.jsx +++ b/packages/concordia-app/src/components/PollView/index.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { - Container, Header, Icon, Tab, + Container, Grid, Header, Icon, Placeholder, Tab, } from 'semantic-ui-react'; import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; @@ -32,6 +32,7 @@ const PollView = (props) => { const [voteCounts, setVoteCounts] = useState([]); const [voters, setVoters] = useState([]); const [pollHashValid, setPollHashValid] = useState(false); + const [pollQuestion, setPollQuestion] = useState(''); const [loading, setLoading] = useState(true); const dispatch = useDispatch(); const { t } = useTranslation(); @@ -77,6 +78,7 @@ const PollView = (props) => { if (pollHash && pollFound) { if (generatePollHash(pollFound[POLL_QUESTION], pollFound[POLL_OPTIONS]) === pollHash) { setPollHashValid(true); + setPollQuestion(pollFound[POLL_QUESTION]); setPollOptions(pollFound[POLL_OPTIONS].map((pollOption) => ({ label: pollOption, hash: generateHash(pollOption), @@ -143,8 +145,14 @@ const PollView = (props) => { return (
- - Do you thing asdf or fdsa? + + + + {loading + ? + : pollQuestion} + +
From 5d3cceb478bca9eda259ac2d4f51a1b7371360fa Mon Sep 17 00:00:00 2001 From: apostolof Date: Sun, 28 Mar 2021 18:10:01 +0300 Subject: [PATCH 08/13] feat: add invalid poll data view --- .../PollView/PollDataInvalid/index.jsx | 16 ++++++++++ .../src/components/PollView/index.jsx | 29 ++++++++++++------- 2 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx diff --git a/packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx b/packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx new file mode 100644 index 0000000..c699e45 --- /dev/null +++ b/packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { Container, Header, Icon } from 'semantic-ui-react'; + +const PollDataInvalid = () => ( + +
+ + + This topic has a poll but the data are untrusted! + The poll data downloaded from the poster have been tampered with. + +
+
+); + +export default PollDataInvalid; diff --git a/packages/concordia-app/src/components/PollView/index.jsx b/packages/concordia-app/src/components/PollView/index.jsx index 2014cf7..bb2ab0d 100644 --- a/packages/concordia-app/src/components/PollView/index.jsx +++ b/packages/concordia-app/src/components/PollView/index.jsx @@ -15,6 +15,7 @@ import PollVote from './PollVote'; import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions'; import { generatePollHash, generateHash } from '../../utils/hashUtils'; import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; +import PollDataInvalid from './PollDataInvalid'; const { contracts: { [VOTING_CONTRACT]: { methods: { getPoll: { cacheCall: getPollChainData } } } } } = drizzle; const { orbit } = breeze; @@ -144,17 +145,23 @@ const PollView = (props) => { return ( -
- - - - {loading - ? - : pollQuestion} - - -
- + {!loading && pollHashValid + ? ( + <> +
+ + + + {loading + ? + : pollQuestion} + + +
+ + + ) + : }
); }; From 150806c39da43ed1c584512656e4664fa6d5408a Mon Sep 17 00:00:00 2001 From: apostolof Date: Mon, 29 Mar 2021 20:03:28 +0300 Subject: [PATCH 09/13] feat: add signup-to-enable-voting view --- .../PollView/PollGuestView/index.jsx | 21 +++++++++++ .../src/components/PollView/index.jsx | 35 +++++++++++-------- 2 files changed, 41 insertions(+), 15 deletions(-) create mode 100644 packages/concordia-app/src/components/PollView/PollGuestView/index.jsx diff --git a/packages/concordia-app/src/components/PollView/PollGuestView/index.jsx b/packages/concordia-app/src/components/PollView/PollGuestView/index.jsx new file mode 100644 index 0000000..0db3864 --- /dev/null +++ b/packages/concordia-app/src/components/PollView/PollGuestView/index.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Container, Header, Icon } from 'semantic-ui-react'; +import { Link } from 'react-router-dom'; + +const PollGuestView = () => ( + +
+ + + Only registered users are able to vote in polls. + + You can register in the  + signup +  page. + + +
+
+); + +export default PollGuestView; diff --git a/packages/concordia-app/src/components/PollView/index.jsx b/packages/concordia-app/src/components/PollView/index.jsx index bb2ab0d..18ff75c 100644 --- a/packages/concordia-app/src/components/PollView/index.jsx +++ b/packages/concordia-app/src/components/PollView/index.jsx @@ -16,6 +16,7 @@ import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationAction import { generatePollHash, generateHash } from '../../utils/hashUtils'; import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; import PollDataInvalid from './PollDataInvalid'; +import PollGuestView from './PollGuestView'; const { contracts: { [VOTING_CONTRACT]: { methods: { getPoll: { cacheCall: getPollChainData } } } } } = drizzle; const { orbit } = breeze; @@ -92,8 +93,6 @@ const PollView = (props) => { } }, [pollHash, polls, topicId]); - // TODO: add a "Signup to enable voting" view - const userHasVoted = useMemo(() => hasSignedUp && voters .some((optionVoters) => optionVoters.includes(userAddress)), [hasSignedUp, userAddress, voters]); @@ -107,18 +106,24 @@ const PollView = (props) => { return ''; }, [pollOptions, userAddress, userHasVoted, voters]); - const pollVoteTab = useMemo(() => ( - !loading - ? ( - - ) - :
- ), [loading, pollChangeVoteEnabled, pollOptions, userHasVoted, userVoteHash]); + const pollVoteTab = useMemo(() => { + if (!hasSignedUp) { + return ; + } + + if (loading) { + return null; + } + + return ( + + ); + }, [hasSignedUp, loading, pollChangeVoteEnabled, pollOptions, userHasVoted, userVoteHash]); const pollGraphTab = useMemo(() => ( !loading @@ -130,7 +135,7 @@ const PollView = (props) => { userVoteHash={userVoteHash} /> ) - :
+ : null ), [loading, pollOptions, userHasVoted, userVoteHash, voteCounts]); const panes = useMemo(() => { From 8c2d26bfe919b16fc44b442ea3a36e6d0f610639 Mon Sep 17 00:00:00 2001 From: apostolof Date: Mon, 29 Mar 2021 20:55:38 +0300 Subject: [PATCH 10/13] feat: implement vote casting --- .../components/PollView/PollGraph/index.jsx | 17 ++--- .../components/PollView/PollVote/index.jsx | 40 ++++++----- .../src/components/PollView/index.jsx | 70 ++++++++++--------- 3 files changed, 66 insertions(+), 61 deletions(-) diff --git a/packages/concordia-app/src/components/PollView/PollGraph/index.jsx b/packages/concordia-app/src/components/PollView/PollGraph/index.jsx index be698a9..12d77d7 100644 --- a/packages/concordia-app/src/components/PollView/PollGraph/index.jsx +++ b/packages/concordia-app/src/components/PollView/PollGraph/index.jsx @@ -7,7 +7,7 @@ import { CASTED_OPTION_COLOR, DEFAULT_OPTION_COLOR } from '../../../constants/po const PollGraph = (props) => { const { - pollOptions, voteCounts, hasUserVoted, userVoteHash, + pollOptions, voteCounts, hasUserVoted, selectedOptionIndex, } = props; const { t } = useTranslation(); @@ -22,16 +22,16 @@ const PollGraph = (props) => { }, colors: [ (value) => { - if (hasUserVoted && pollOptions[value.dataPointIndex].hash === userVoteHash) { + if (hasUserVoted && value.dataPointIndex === selectedOptionIndex) { return CASTED_OPTION_COLOR; } return DEFAULT_OPTION_COLOR; }, ], xaxis: { - categories: pollOptions.map((pollOption) => pollOption.label), + categories: pollOptions, }, - }), [hasUserVoted, pollOptions, userVoteHash]); + }), [hasUserVoted, pollOptions, selectedOptionIndex]); const chartSeries = useMemo(() => [{ name: 'votes', @@ -66,17 +66,14 @@ const PollGraph = (props) => { PollGraph.defaultProps = { hasUserVoted: false, - userVoteHash: '', + selectedOptionIndex: '', }; PollGraph.propTypes = { - pollOptions: PropTypes.arrayOf(PropTypes.exact({ - label: PropTypes.string, - hash: PropTypes.string, - })).isRequired, + pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired, voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired, hasUserVoted: PropTypes.bool, - userVoteHash: PropTypes.string, + selectedOptionIndex: PropTypes.string, }; export default PollGraph; diff --git a/packages/concordia-app/src/components/PollView/PollVote/index.jsx b/packages/concordia-app/src/components/PollView/PollVote/index.jsx index 3b4641e..836e45e 100644 --- a/packages/concordia-app/src/components/PollView/PollVote/index.jsx +++ b/packages/concordia-app/src/components/PollView/PollVote/index.jsx @@ -2,42 +2,46 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Form } from 'semantic-ui-react'; import { useTranslation } from 'react-i18next'; +import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; +import { drizzle } from '../../../redux/store'; + +const { contracts: { [VOTING_CONTRACT]: { methods: { vote } } } } = drizzle; const PollVote = (props) => { const { - pollOptions, enableVoteChanges, hasUserVoted, userVoteHash, + topicId, account, pollOptions, enableVoteChanges, hasUserVoted, userVoteIndex, } = props; - const [selectedOptionHash, setSelectedOptionHash] = useState(userVoteHash); + const [selectedOptionIndex, setSelectedOptionIndex] = useState(userVoteIndex); + const [voting, setVoting] = useState(''); const { t } = useTranslation(); const onOptionSelected = (e, { value }) => { - setSelectedOptionHash(value); + setSelectedOptionIndex(value); }; const onCastVote = () => { - console.log('vote'); - // TODO - // TODO: callback for immediate poll data refresh on vote? + setVoting(true); + vote.cacheSend(...[topicId, selectedOptionIndex + 1], { from: account }); }; return (
- {pollOptions.map((pollOption) => ( + {pollOptions.map((pollOption, index) => ( ))} {t('topic.poll.tab.vote.form.button.submit')} @@ -46,17 +50,15 @@ const PollVote = (props) => { }; PollVote.defaultProps = { - userVoteHash: '', + userVoteIndex: -1, }; PollVote.propTypes = { - pollOptions: PropTypes.arrayOf(PropTypes.exact({ - label: PropTypes.string, - hash: PropTypes.string, - })).isRequired, + topicId: PropTypes.number.isRequired, + pollOptions: PropTypes.arrayOf(PropTypes.string).isRequired, enableVoteChanges: PropTypes.bool.isRequired, hasUserVoted: PropTypes.bool.isRequired, - userVoteHash: PropTypes.string, + userVoteIndex: PropTypes.number, }; export default PollVote; diff --git a/packages/concordia-app/src/components/PollView/index.jsx b/packages/concordia-app/src/components/PollView/index.jsx index 18ff75c..c591826 100644 --- a/packages/concordia-app/src/components/PollView/index.jsx +++ b/packages/concordia-app/src/components/PollView/index.jsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; import { - Container, Grid, Header, Icon, Placeholder, Tab, + Container, Header, Icon, Tab, } from 'semantic-ui-react'; import { useTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; @@ -13,7 +13,7 @@ import CustomLoadingTabPane from '../CustomLoadingTabPane'; import { GRAPH_TAB, VOTE_TAB } from '../../constants/polls/PollTabs'; import PollVote from './PollVote'; import { FETCH_USER_DATABASE } from '../../redux/actions/peerDbReplicationActions'; -import { generatePollHash, generateHash } from '../../utils/hashUtils'; +import { generatePollHash } from '../../utils/hashUtils'; import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; import PollDataInvalid from './PollDataInvalid'; import PollGuestView from './PollGuestView'; @@ -35,7 +35,8 @@ const PollView = (props) => { const [voters, setVoters] = useState([]); const [pollHashValid, setPollHashValid] = useState(false); const [pollQuestion, setPollQuestion] = useState(''); - const [loading, setLoading] = useState(true); + const [chainDataLoading, setChainDataLoading] = useState(true); + const [orbitDataLoading, setOrbitDataLoading] = useState(true); const dispatch = useDispatch(); const { t } = useTranslation(); @@ -70,6 +71,8 @@ const PollView = (props) => { .map((subArrayEnd, index) => getPollResults[getPollCallHash].value[5] .slice(index > 0 ? cumulativeSum[index - 1] : 0, subArrayEnd))); + + setChainDataLoading(false); } }, [getPollCallHash, getPollResults]); @@ -81,15 +84,12 @@ const PollView = (props) => { if (generatePollHash(pollFound[POLL_QUESTION], pollFound[POLL_OPTIONS]) === pollHash) { setPollHashValid(true); setPollQuestion(pollFound[POLL_QUESTION]); - setPollOptions(pollFound[POLL_OPTIONS].map((pollOption) => ({ - label: pollOption, - hash: generateHash(pollOption), - }))); + setPollOptions([...pollFound[POLL_OPTIONS]]); } else { setPollHashValid(false); } - setLoading(false); + setOrbitDataLoading(false); } }, [pollHash, polls, topicId]); @@ -97,71 +97,77 @@ const PollView = (props) => { .some((optionVoters) => optionVoters.includes(userAddress)), [hasSignedUp, userAddress, voters]); - const userVoteHash = useMemo(() => { - if (userHasVoted) { - return pollOptions[voters - .findIndex((optionVoters) => optionVoters.includes(userAddress))].hash; + const userVoteIndex = useMemo(() => { + if (!chainDataLoading && !orbitDataLoading && userHasVoted) { + return voters + .findIndex((optionVoters) => optionVoters.includes(userAddress)); } - return ''; - }, [pollOptions, userAddress, userHasVoted, voters]); + return -1; + }, [chainDataLoading, orbitDataLoading, userAddress, userHasVoted, voters]); const pollVoteTab = useMemo(() => { if (!hasSignedUp) { return ; } - if (loading) { + if (chainDataLoading || orbitDataLoading) { return null; } return ( ); - }, [hasSignedUp, loading, pollChangeVoteEnabled, pollOptions, userHasVoted, userVoteHash]); + }, [ + chainDataLoading, hasSignedUp, orbitDataLoading, pollChangeVoteEnabled, pollOptions, topicId, userHasVoted, + userVoteIndex, + ]); const pollGraphTab = useMemo(() => ( - !loading + !chainDataLoading || orbitDataLoading ? ( ) : null - ), [loading, pollOptions, userHasVoted, userVoteHash, voteCounts]); + ), [chainDataLoading, orbitDataLoading, pollOptions, userHasVoted, userVoteIndex, voteCounts]); const panes = useMemo(() => { - const pollVotePane = ({pollVoteTab}); - const pollGraphPane = ({pollGraphTab}); + const pollVotePane = ( + + {pollVoteTab} + + ); + const pollGraphPane = ( + + {pollGraphTab} + + ); return ([ { menuItem: t(VOTE_TAB.intl_display_name_id), render: () => pollVotePane }, { menuItem: t(GRAPH_TAB.intl_display_name_id), render: () => pollGraphPane }, ]); - }, [loading, pollGraphTab, pollVoteTab, t]); + }, [chainDataLoading, orbitDataLoading, pollGraphTab, pollVoteTab, t]); return ( - {!loading && pollHashValid + {!chainDataLoading && !orbitDataLoading && pollHashValid ? ( <>
- - - - {loading - ? - : pollQuestion} - - + + {pollQuestion}
From 91a6aa89405399738ad7a65a1e74732c0045cdc9 Mon Sep 17 00:00:00 2001 From: Ezerous Date: Tue, 30 Mar 2021 15:27:58 +0300 Subject: [PATCH 11/13] fix: fix last option voting --- packages/concordia-contracts/contracts/Voting.sol | 4 ++-- yarn.lock | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/concordia-contracts/contracts/Voting.sol b/packages/concordia-contracts/contracts/Voting.sol index 129e4f6..b11d9b1 100644 --- a/packages/concordia-contracts/contracts/Voting.sol +++ b/packages/concordia-contracts/contracts/Voting.sol @@ -91,7 +91,7 @@ contract Voting { address[] memory voters = new address[](totalVotes); uint serializationIndex = 0; - for (uint pollOption = 1; pollOption < polls[topicID].numOptions; pollOption++) { + for (uint pollOption = 1; pollOption <= polls[topicID].numOptions; pollOption++) { address[] memory optionVoters = getVoters(topicID, pollOption); for (uint voteIndex = 0; voteIndex < voteCounts[pollOption - 1]; voteIndex++) { @@ -132,7 +132,7 @@ contract Voting { uint[] memory voteCounts = new uint[](polls[topicID].numOptions); - for (uint pollOption = 1; pollOption < polls[topicID].numOptions; pollOption++) { + for (uint pollOption = 1; pollOption <= polls[topicID].numOptions; pollOption++) { voteCounts[pollOption - 1] = getVoteCount(topicID, pollOption); } diff --git a/yarn.lock b/yarn.lock index 61b8ce3..957d618 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16493,9 +16493,6 @@ snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" snapdragon-node@^2.0.1: version "2.1.1" From d97242ce3e3fe94320811c59d9ac5d9195619c75 Mon Sep 17 00:00:00 2001 From: Ezerous Date: Tue, 30 Mar 2021 15:47:18 +0300 Subject: [PATCH 12/13] fix: voting options styling --- packages/concordia-app/src/components/PollCreate/styles.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/concordia-app/src/components/PollCreate/styles.css b/packages/concordia-app/src/components/PollCreate/styles.css index 9fc624c..8a6fb8d 100644 --- a/packages/concordia-app/src/components/PollCreate/styles.css +++ b/packages/concordia-app/src/components/PollCreate/styles.css @@ -3,7 +3,9 @@ padding-bottom: 1em; } -.poll-create .checkbox > label, label:focus, label:hover{ +.poll-create .checkbox > label, +.poll-create .checkbox > label:focus, +.poll-create .checkbox > label:hover{ color: white !important; font-weight: 700; } From 5c850834d7e1812f3b4398b151f66b26eb931681 Mon Sep 17 00:00:00 2001 From: apostolof Date: Thu, 1 Apr 2021 18:32:33 +0300 Subject: [PATCH 13/13] refactor: replace hardcoded strings with intl equivalents --- .../public/locales/en/translation.json | 18 ++++++---- .../PollView/PollDataInvalid/index.jsx | 29 +++++++++------ .../PollView/PollGuestView/index.jsx | 35 +++++++++++-------- 3 files changed, 50 insertions(+), 32 deletions(-) diff --git a/packages/concordia-app/public/locales/en/translation.json b/packages/concordia-app/public/locales/en/translation.json index 151d1eb..5c2c92d 100644 --- a/packages/concordia-app/public/locales/en/translation.json +++ b/packages/concordia-app/public/locales/en/translation.json @@ -22,12 +22,12 @@ "edit.information.modal.form.profile.picture.field.placeholder": "URL", "edit.information.modal.form.submit.button": "Submit", "edit.information.modal.title": "Edit profile information", - "poll.create.question.field.label": "Poll Question", - "poll.create.question.field.placeholder": "Question", + "poll.create.add.option.button": "Add Option", "poll.create.allow.vote.changes.field.label": "Allow vote changes", "poll.create.option.field.label": "Option #{{id}}", "poll.create.option.field.placeholder": "Option #{{id}}", - "poll.create.add.option.button": "Add Option", + "poll.create.question.field.label": "Poll Question", + "poll.create.question.field.placeholder": "Question", "post.create.form.send.button": "Post", "post.form.content.field.placeholder": "Message", "post.form.subject.field.placeholder": "Subject", @@ -39,8 +39,8 @@ "profile.general.tab.location.row.title": "Location:", "profile.general.tab.number.of.posts.row.title": "Number of posts:", "profile.general.tab.number.of.topics.row.title": "Number of topics created:", - "profile.general.tab.posts.db.address.row.title": "PostsDB:", "profile.general.tab.polls.db.address.row.title": "PollsDB:", + "profile.general.tab.posts.db.address.row.title": "PostsDB:", "profile.general.tab.registration.date.row.title": "Member since:", "profile.general.tab.save.info.button.title": "Save information", "profile.general.tab.title": "General", @@ -75,14 +75,20 @@ "topbar.button.create.topic": "Create topic", "topbar.button.profile": "Profile", "topbar.button.register": "Sign Up", + "topic.create.form.add.poll.button": "Add Poll", "topic.create.form.content.field.label": "First post content", "topic.create.form.content.field.placeholder": "Message", "topic.create.form.post.button": "Create Topic", + "topic.create.form.remove.poll.button": "Remove Poll", "topic.create.form.subject.field.label": "Topic subject", "topic.create.form.subject.field.placeholder": "Subject", - "topic.create.form.add.poll.button": "Add Poll", - "topic.create.form.remove.poll.button": "Remove Poll", "topic.list.row.topic.id": "#{{id}}", + "topic.poll.guest.header": "Only registered users are able to vote in polls.", + "topic.poll.guest.sub.header.link": "signup", + "topic.poll.guest.sub.header.post": " page.", + "topic.poll.guest.sub.header.pre": "You can register in the ", + "topic.poll.invalid.data.header": "This topic has a poll but the data are untrusted!", + "topic.poll.invalid.data.sub.header": "The poll data downloaded from the poster have been tampered with.", "topic.poll.tab.graph.title": "Results", "topic.poll.tab.results.votes.count": "{{totalVotes}} votes casted", "topic.poll.tab.vote.form.button.submit": "Submit", diff --git a/packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx b/packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx index c699e45..518dbc0 100644 --- a/packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx +++ b/packages/concordia-app/src/components/PollView/PollDataInvalid/index.jsx @@ -1,16 +1,23 @@ import React from 'react'; import { Container, Header, Icon } from 'semantic-ui-react'; +import { useTranslation } from 'react-i18next'; -const PollDataInvalid = () => ( - -
- - - This topic has a poll but the data are untrusted! - The poll data downloaded from the poster have been tampered with. - -
-
-); +const PollDataInvalid = () => { + const { t } = useTranslation(); + + return ( + +
+ + + {t('topic.poll.invalid.data.header')} + + {t('topic.poll.invalid.data.sub.header')} + + +
+
+ ); +}; export default PollDataInvalid; diff --git a/packages/concordia-app/src/components/PollView/PollGuestView/index.jsx b/packages/concordia-app/src/components/PollView/PollGuestView/index.jsx index 0db3864..af23be9 100644 --- a/packages/concordia-app/src/components/PollView/PollGuestView/index.jsx +++ b/packages/concordia-app/src/components/PollView/PollGuestView/index.jsx @@ -1,21 +1,26 @@ import React from 'react'; import { Container, Header, Icon } from 'semantic-ui-react'; import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; -const PollGuestView = () => ( - -
- - - Only registered users are able to vote in polls. - - You can register in the  - signup -  page. - - -
-
-); +const PollGuestView = () => { + const { t } = useTranslation(); + + return ( + +
+ + + {t('topic.poll.guest.header')} + + {t('topic.poll.guest.sub.header.pre')} + {t('topic.poll.guest.sub.header.link')} + {t('topic.poll.guest.sub.header.post')} + + +
+
+ ); +}; export default PollGuestView;