[FE] Batching functionality (#88)

This commit is contained in:
Jakub Kotula 2023-11-01 12:52:59 +01:00 committed by GitHub
parent afdfe715f5
commit 2ea23bd37d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 595 additions and 205 deletions

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import { GlobalStyle } from './providers/GlobalStyle'
import { NotificationsList } from './components/NotificationsList'
import { MobileRouter } from './pagesMobile/MobileRouter'
import { DesktopRouter } from './pages/DesktopRouter'
@ -25,7 +24,6 @@ export function App() {
<Page>
<GlobalStyle />
{mobileVersion ? <MobileRouter /> : <DesktopRouter />}
{/* <NotificationsList /> */}
</Page>
)
}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React from 'react'
import { ProposeButton } from './Button'
import { useEthers } from '@usedapp/core'

View File

@ -37,6 +37,28 @@ export function NotificationItem({ publicKey, text, transaction }: NotificationI
</NotificationBlock>
)
}
return null
}
interface NotificationItemPlainProps {
text: string
}
export function NotificationItemPlain({ text }: NotificationItemPlainProps) {
const [show, setShow] = useState(true)
if (show) {
return (
<NotificationBlock>
<NotificationContent>
<NotificationText>{text}</NotificationText>
</NotificationContent>
<NotificationCloseButton onClick={() => setShow(false)} />
</NotificationBlock>
)
}
return null
}

View File

@ -3,44 +3,83 @@ import React from 'react'
import styled from 'styled-components'
import { AnimationNotification, AnimationNotificationMobile } from '../constants/animation'
import { useContracts } from '../hooks/useContracts'
import { NotificationItem } from './NotificationItem'
import { NotificationItem, NotificationItemPlain } from './NotificationItem'
export function NotificationsList() {
interface Props {
type: 'votes' | 'featured'
}
export function NotificationsList({ type }: Props) {
const { notifications } = useNotifications()
const { votingContract } = useContracts()
const { votingContract, featuredVotingContract } = useContracts()
const getParsedLog = (log: any, type: 'votes' | 'featured') => {
switch (type) {
case 'votes': {
return votingContract.interface.parseLog(log)
}
case 'featured': {
return featuredVotingContract.interface.parseLog(log)
}
}
}
const parseVoting = (parsedLog: any) => {
let text = ''
if (parsedLog.name === 'VotingRoomStarted') {
text = ' voting room started.'
}
if (parsedLog.name === 'VotingRoomFinalized') {
if (parsedLog.args.passed == true) {
if (parsedLog.args.voteType === 1) {
text = ' is now in the communities directory!'
}
if (parsedLog.args.voteType === 0) {
text = ' is now removed from communities directory!'
}
}
}
return text
}
const parseFeatured = (parsedLog: any) => {
let text = ''
if (parsedLog.name === 'VotingStarted') {
text = 'Featured voting started.'
}
if (parsedLog.name === 'VotingFinalized') {
text = 'Featured voting was finalized.'
}
return text
}
return (
<NotificationsWrapper>
{notifications.map((notification) => {
if ('receipt' in notification) {
return notification.receipt.logs.map((log) => {
// this needs to be updated so it takes into account also interface of featuredVotingContract
const parsedLog = votingContract.interface.parseLog(log)
const parsedLog = getParsedLog(log, type)
let text
if (parsedLog.name === 'VotingRoomStarted') {
text = ' voting room started.'
let res = ''
if (type === 'votes') {
res = parseVoting(parsedLog)
} else if (type === 'featured') {
res = parseFeatured(parsedLog)
}
if (parsedLog.name === 'VotingRoomFinalized') {
if (parsedLog.args.passed == true) {
if (parsedLog.args.voteType === 1) {
text = ' is now in the communities directory!'
}
if (parsedLog.args.voteType === 0) {
text = ' is now removed from communities directory!'
}
}
}
if (text) {
if (res && type === 'votes') {
return (
<NotificationItem
key={log.transactionHash}
publicKey={parsedLog.args.publicKey}
text={text}
text={res}
transaction={notification.transaction}
/>
)
} else if (res && type === 'featured') {
return <NotificationItemPlain key={log.transactionHash} text={res} />
}
})
}

View File

@ -27,13 +27,6 @@ export function Rules() {
directory. Otherwise, a new vote for removal can be submitted after about 30 days.
</RuleText>
</Rule>
{/* <Rule>
<RuleIcon></RuleIcon>
<RuleText>
The minimum amount of SNT needed to start a new vote doubles with every failed vote attempt (for both addition
and removal votes).
</RuleText>
</Rule> */}
<Rule>
<RuleIcon></RuleIcon>
<RuleText>
@ -48,14 +41,6 @@ export function Rules() {
a finalization transaction.
</RuleText>
</Rule>
{/* <Rule>
<RuleIcon></RuleIcon>
<RuleText>
If a single vote of more than 2,000,000 SNT votes for is made in favor of the removal of a community, the
remaining vote duration shortens to 24 hours. This shortening of the vote duration can be reversed if a single
vote of more than 2,000,000 SNT is made against the removal.
</RuleText>
</Rule> */}
<Rule>
<RuleIcon></RuleIcon>
<RuleText>

View File

@ -2,18 +2,18 @@ import React from 'react'
import styled from 'styled-components'
import backgroundImage from '../assets/images/curve-shape.svg'
import { Colors } from '../constants/styles'
import { getFeaturedVotingState } from '../helpers/featuredVoting'
import { useFeaturedVotingState } from '../hooks/useFeaturedVotingState'
import { useFeaturedVotes } from '../hooks/useFeaturedVotes'
import { formatTimeLeft } from '../helpers/fomatTimeLeft'
export const WeeklyFeature = () => {
const { activeVoting } = useFeaturedVotes()
if (!activeVoting) {
const featuredVotingState = useFeaturedVotingState(activeVoting)
if (!activeVoting || !featuredVotingState) {
return null
}
const featuredVotingState = getFeaturedVotingState(activeVoting)
if (featuredVotingState === 'ended') {
return (
<div>

View File

@ -138,7 +138,7 @@ export const CardCommunityBlock = styled.div`
&.notModal {
@media (max-width: 768px) {
align-items: flex-end;
align-items: stretch;
}
}
`

View File

@ -10,7 +10,7 @@ import { VoteConfirmModal } from './VoteConfirmModal'
import { useContractCall, useEthers } from '@usedapp/core'
import { VoteBtn } from '../Button'
import { useFeaturedVotes } from '../../hooks/useFeaturedVotes'
import { getFeaturedVotingState } from '../../helpers/featuredVoting'
import { useFeaturedVotingState } from '../../hooks/useFeaturedVotingState'
import { useContracts } from '../../hooks/useContracts'
import { BigNumber } from 'ethers'
@ -38,7 +38,7 @@ export const CardFeature = ({ community, featured }: CardFeatureProps) => {
const [heading, setHeading] = useState('Weekly Feature vote')
const [icon, setIcon] = useState('⭐')
const { activeVoting, alreadyVoted } = useFeaturedVotes()
const featuredVotingState = getFeaturedVotingState(activeVoting)
const featuredVotingState = useFeaturedVotingState(activeVoting)
const [savedVotes] =
useContractCall({

View File

@ -11,13 +11,12 @@ import { getVotingWinner } from '../../../helpers/voting'
import { VoteAnimatedModal } from './../VoteAnimatedModal'
import voting from '../../../helpers/voting'
import { DetailedVotingRoom } from '../../../models/smartContract'
import { useRoomAggregateVotes } from '../../../hooks/useRoomAggregateVotes'
import styled from 'styled-components'
import { Modal } from './../../Modal'
import { VoteBtn, VotesBtns } from '../../Button'
import { CardHeading, CardVoteBlock } from '../../Card'
import { useVotesAggregate } from '../../../hooks/useVotesAggregate'
import { useUnverifiedVotes } from '../../../hooks/useUnverifiedVotes'
import { useVotingBatches } from '../../../hooks/useVotingBatches'
interface CardVoteProps {
room: DetailedVotingRoom
@ -33,6 +32,11 @@ export const CardVote = ({ room, hideModalFunction }: CardVoteProps) => {
const [sentVotesFor, setSentVotesFor] = useState(0)
const [sentVotesAgainst, setSentVotesAgainst] = useState(0)
const [voted, setVoted] = useState<null | boolean>(null)
const [verificationPeriod, setVerificationPeriod] = useState(false)
const [finalizationPeriod, setFinalizationPeriod] = useState(false)
const [loading, setLoading] = useState(false)
const { finalizeVotingLimit, batchCount, batchDoneCount, beingEvaluated, beingFinalized, batchedVotes } =
useVotingBatches({ room })
useEffect(() => {
setVoted(null)
@ -41,11 +45,7 @@ export const CardVote = ({ room, hideModalFunction }: CardVoteProps) => {
const { votingContract } = useContracts()
const vote = voting.fromRoom(room)
const voteConstants = voteTypes[vote.type]
const { votes } = useVotesAggregate(vote.ID, room.verificationStartAt, room.startAt)
const castVotes = useContractFunction(votingContract, 'castVotes')
room = useRoomAggregateVotes(room, showConfirmModal)
const finalizeVoting = useContractFunction(votingContract, 'finalizeVotingRoom')
const setNext = (val: boolean) => {
@ -60,10 +60,28 @@ export const CardVote = ({ room, hideModalFunction }: CardVoteProps) => {
setShowConfirmModal(val)
}
const now = Date.now() / 1000
const verificationStarted = room.verificationStartAt.toNumber() - now < 0
const verificationEnded = room.endAt.toNumber() - now < 0
const verificationPeriod = verificationStarted && !verificationEnded
useEffect(() => {
const checkPeriod = () => {
const now = Date.now() / 1000
const verificationStarted = room.verificationStartAt.toNumber() - now < 0
const verificationEnded = room.endAt.toNumber() - now < 0
const verificationPeriod = verificationStarted && !verificationEnded
const finalizationPeriod = verificationStarted && verificationEnded
setVerificationPeriod(verificationPeriod)
setFinalizationPeriod(finalizationPeriod)
}
checkPeriod()
const timer = setInterval(checkPeriod, 1000)
return () => clearInterval(timer)
}, [])
useEffect(() => {
if (finalizeVoting.state.status === 'Success' || castVotes.state.status === 'Success') {
history.go(0)
}
}, [finalizeVoting.state.status, castVotes.state.status])
const winner = verificationPeriod ? 0 : getVotingWinner(vote)
@ -132,10 +150,17 @@ export const CardVote = ({ room, hideModalFunction }: CardVoteProps) => {
)}
{verificationPeriod && (
<CardHeadingEndedVote>Verification period in progress, please verify your vote.</CardHeadingEndedVote>
<CardHeadingEndedVote>
Verification period in progress, please verify your vote.{' '}
{batchCount > 1 && (
<>
<br />({beingEvaluated ? batchDoneCount : 0}/{batchCount} verified)
</>
)}
</CardHeadingEndedVote>
)}
{winner ? (
{finalizationPeriod ? (
<CardHeadingEndedVote>
SNT holders have decided <b>{winner == 1 ? voteConstants.against.verb : voteConstants.for.verb}</b> this
community to the directory!
@ -157,24 +182,36 @@ export const CardVote = ({ room, hideModalFunction }: CardVoteProps) => {
{verificationPeriod && (
<VoteBtnFinal
onClick={async () => {
await castVotes.send(votes)
setLoading(true)
await castVotes.send(batchedVotes)
setSentVotesFor(0)
setSentVotesAgainst(0)
setLoading(false)
}}
disabled={!account}
disabled={!account || loading}
>
Verify votes
{loading ? 'Waiting...' : 'Verify votes'}
</VoteBtnFinal>
)}
{Boolean(winner) && (
// note: @jkbktl PR
<VoteBtnFinal onClick={() => finalizeVoting.send(room.roomNumber, 1)} disabled={!account}>
Finalize the vote <span></span>
{finalizationPeriod && (
<VoteBtnFinal
onClick={() => finalizeVoting.send(room.roomNumber, finalizeVotingLimit < 1 ? 1 : finalizeVotingLimit)}
disabled={!account}
>
<>
Finalize the vote <span></span>
<br />
{batchCount > 1 && (
<>
({beingFinalized ? batchDoneCount : 0}/{batchCount} finalized)
</>
)}
</>
</VoteBtnFinal>
)}
{!verificationPeriod && !winner && (
{!verificationPeriod && !finalizationPeriod && (
<VotesBtns>
<VoteBtn
disabled={!canVote}

View File

@ -1,27 +0,0 @@
import React from 'react'
import { votingFromRoom } from '../../helpers/voting'
import { CommunityDetail } from '../../models/community'
import { VotingRoom } from '../../models/smartContract'
import { CardVoteBlock } from '../Card'
import { CardVote } from './CardVote/CardVote'
import { Modal } from '../Modal'
export interface OngoingVoteProps {
community: CommunityDetail
setShowOngoingVote: (show: boolean) => void
room: VotingRoom
}
export function OngoingVote({ community, setShowOngoingVote, room }: OngoingVoteProps) {
const vote = votingFromRoom(room)
const detailedVoting = { ...room, details: community }
if (!vote) {
return <CardVoteBlock />
}
return (
<Modal heading={`${vote?.type} ${community.name}?`} setShowModal={setShowOngoingVote}>
<CardVote hideModalFunction={setShowOngoingVote} room={detailedVoting} />
</Modal>
)
}

View File

@ -32,7 +32,7 @@ export function RemoveAmountPicker({ community, setShowConfirmModal }: RemoveAmo
if (community.votingHistory && community.votingHistory.length > 0) {
const lastVote = community.votingHistory[community.votingHistory.length - 1]
const lastVoteDate = lastVote.date
if (timespan(lastVoteDate) < 30 && lastVote.type === 'Remove') {
if (timespan(lastVoteDate) < 3 && lastVote.type === 'Remove') {
return (
<WarningWrapRemoval>
<Warning

View File

@ -1,27 +0,0 @@
import { useContractFunction } from '@usedapp/core'
import React from 'react'
import { useContracts } from '../../hooks/useContracts'
import { useVotesAggregate } from '../../hooks/useVotesAggregate'
import { CurrentVoting } from '../../models/community'
import { VoteSendingBtn } from '../Button'
import { BigNumber } from 'ethers'
import { addCommas } from '../../helpers/addCommas'
import { VotingRoom } from '../../models/smartContract'
interface VoteSubmitButtonProps {
vote: CurrentVoting
room: VotingRoom
}
export function VoteSubmitButton({ vote, room }: VoteSubmitButtonProps) {
const { votes } = useVotesAggregate(vote.ID, room.verificationStartAt, room.startAt)
const { votingContract } = useContracts()
const { send } = useContractFunction(votingContract, 'castVotes')
const voteAmount = votes.reduce((prev, curr) => prev.add(curr[2]), BigNumber.from(0))
if (votes.length > 0) {
return (
<VoteSendingBtn onClick={() => send(votes)}> {addCommas(voteAmount.toString())} votes need saving</VoteSendingBtn>
)
}
return null
}

View File

@ -1,26 +1,46 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import { InfoWrap, PageInfo } from '../PageInfo'
import { useContractFunction, useEthers } from '@usedapp/core'
import { ConnectButton } from '../ConnectButton'
import { ProposeButton } from '../Button'
import { useFeaturedVotes } from '../../hooks/useFeaturedVotes'
import { getFeaturedVotingState } from '../../helpers/featuredVoting'
import { useFeaturedVotingState } from '../../hooks/useFeaturedVotingState'
import { useContracts } from '../../hooks/useContracts'
import { useWaku } from '../../providers/waku/provider'
import { mapFeaturesVotes, receiveWakuFeature } from '../../helpers/receiveWakuFeature'
import { config } from '../../config'
import { useTypedFeatureVote } from '../../hooks/useTypedFeatureVote'
import { useFeaturedBatches } from '../../hooks/useFeaturedBatches'
export function DirectoryInfo() {
const { account } = useEthers()
const { featuredVotingContract } = useContracts()
const { getTypedFeatureVote } = useTypedFeatureVote()
const { activeVoting } = useFeaturedVotes()
const { waku } = useWaku()
const featuredVotingState = getFeaturedVotingState(activeVoting)
const { activeVoting } = useFeaturedVotes()
const featuredVotingState = useFeaturedVotingState(activeVoting)
const castVotes = useContractFunction(featuredVotingContract, 'castVotes')
const finalizeVoting = useContractFunction(featuredVotingContract, 'finalizeVoting')
const [loading, setLoading] = useState(false)
const { finalizeVotingLimit, batchCount, batchDoneCount, beingEvaluated, beingFinalized } = useFeaturedBatches()
useEffect(() => {
if (finalizeVoting.state.status === 'Success' || castVotes.state.status === 'Success') {
history.go(0)
}
}, [finalizeVoting.state.status, castVotes.state.status])
if (!activeVoting) {
return (
<InfoWrap>
<PageInfo
heading="Current directory"
text="Vote on your favourite communities being included in
Weekly Featured Communities"
/>
</InfoWrap>
)
}
return (
<InfoWrap>
@ -29,27 +49,50 @@ export function DirectoryInfo() {
text="Vote on your favourite communities being included in
Weekly Featured Communities"
/>
{!account && <ConnectButton />}
{account && featuredVotingState === 'verification' && (
<ProposeButton
onClick={async () => {
setLoading(true)
const { votesToSend } = await receiveWakuFeature(waku, config.wakuConfig.wakuFeatureTopic, activeVoting!)
const votes = mapFeaturesVotes(votesToSend, getTypedFeatureVote)
await castVotes.send(votes)
const batchedVotes = votes.slice(
batchDoneCount * config.votesLimit,
batchDoneCount * config.votesLimit + finalizeVotingLimit
)
await castVotes.send(batchedVotes)
setLoading(false)
}}
>
Verify Weekly featured
{loading ? (
'Waiting...'
) : (
<>
Verify Weekly featured{' '}
{batchCount > 1 && (
<>
({beingEvaluated ? batchDoneCount : 0}/{batchCount} verified)
</>
)}
</>
)}
</ProposeButton>
)}
{account && featuredVotingState === 'ended' && (
<ProposeButton
onClick={async () => {
// note: @jkbktl PR
await finalizeVoting.send(activeVoting?.evaluatingPos)
onClick={() => {
finalizeVoting.send(finalizeVotingLimit < 1 ? 1 : finalizeVotingLimit)
}}
>
Finalize Weekly featured
Finalize Weekly featured{' '}
{batchCount > 1 && (
<>
({beingFinalized ? batchDoneCount : 0}/{batchCount} finalized)
</>
)}
</ProposeButton>
)}
</InfoWrap>

View File

@ -1,6 +1,6 @@
import React from 'react'
import styled from 'styled-components'
import { SearchEmpty } from '../SearchEmpty'
import { Colors } from '../../constants/styles'
import { useFeaturedCommunities } from '../../hooks/useFeaturedCommunities'
import { DirectoryCard } from '../directory/DirectoryCard'
import { DirectoryCardSkeleton } from '../directory/DirectoryCardSkeleton'
@ -14,7 +14,11 @@ export function FeaturedCards() {
}
if (publicKeys.length === 0) {
return <SearchEmpty />
return (
<EmptyWrap>
<span>No communities were featured last week.</span>
</EmptyWrap>
)
}
if (communities.length === 0) {
@ -37,3 +41,24 @@ const Voting = styled.div`
display: flex;
flex-direction: column;
`
export const EmptyWrap = styled.div`
padding: 0 32px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
margin-top: 90px;
background: ${Colors.White};
font-size: 22px;
line-height: 38px;
z-index: 99;
& > p {
font-weight: bold;
font-size: 64px;
line-height: 64%;
margin-bottom: 24px;
}
`

View File

@ -7,6 +7,7 @@ import { VoteType, voteTypes } from './../../constants/voteTypes'
import { CurrentVoting } from '../../models/community'
import { VoteGraphBar } from './VoteGraphBar'
import { formatTimeLeft, formatTimeLeftVerification } from '../../helpers/fomatTimeLeft'
import { useTimeLeft } from '../../hooks/useTimeLeft'
export interface VoteChartProps {
vote: CurrentVoting
votesFor: number
@ -43,6 +44,9 @@ export function VoteChart({
return () => window.removeEventListener('resize', handleResize)
}, [])
const timeLeft = useTimeLeft(vote.votingEndAt)
const timeLeftVerification = useTimeLeft(vote.verificationEndAt)
const voteConstants = voteTypes[vote.type]
const voteSum = votesFor + votesAgainst
@ -74,9 +78,8 @@ export function VoteChart({
<span style={{ fontWeight: 'normal' }}>SNT</span>
</span>
</VoteBox>
{/* todo: wrapper component with timer and setInterval */}
<TimeLeft className={selectedVote ? '' : 'notModal'}>
{vote.timeLeft > 0 ? formatTimeLeft(vote.timeLeft) : formatTimeLeftVerification(vote.timeLeftVerification)}
{timeLeft > 0 ? formatTimeLeft(timeLeft) : formatTimeLeftVerification(timeLeftVerification)}
</TimeLeft>
<VoteBox
style={{
@ -107,7 +110,7 @@ export function VoteChart({
voteWinner={voteWinner}
isAnimation={isAnimation}
/>
<TimeLeftMobile className={selectedVote ? '' : 'notModal'}>{formatTimeLeft(vote.timeLeft)}</TimeLeftMobile>
<TimeLeftMobile className={selectedVote ? '' : 'notModal'}>{formatTimeLeft(timeLeft)}</TimeLeftMobile>
</VoteGraphBarWrap>
</Votes>
)

View File

@ -20,9 +20,8 @@ import { DetailedVotingRoom } from '../models/smartContract'
import arrowDown from '../assets/images/arrowDown.svg'
import { useSendWakuVote } from '../hooks/useSendWakuVote'
import { WrapperBottom, WrapperTop } from '../constants/styles'
import { useRoomAggregateVotes } from '../hooks/useRoomAggregateVotes'
import { useVotesAggregate } from '../hooks/useVotesAggregate'
import { useUnverifiedVotes } from '../hooks/useUnverifiedVotes'
import { useVotingBatches } from '../hooks/useVotingBatches'
interface CardVoteMobileProps {
room: DetailedVotingRoom
@ -33,6 +32,8 @@ export const CardVoteMobile = ({ room }: CardVoteMobileProps) => {
const selectedVoted = voteTypes['Add'].for
const [sentVotesFor, setSentVotesFor] = useState(0)
const [sentVotesAgainst, setSentVotesAgainst] = useState(0)
const [verificationPeriod, setVerificationPeriod] = useState(false)
const [finalizationPeriod, setFinalizationPeriod] = useState(false)
const [voted, setVoted] = useState<null | boolean>(null)
useEffect(() => {
@ -42,16 +43,33 @@ export const CardVoteMobile = ({ room }: CardVoteMobileProps) => {
const { votingContract } = useContracts()
const vote = voting.fromRoom(room)
const voteConstants = voteTypes[vote.type]
const { votes } = useVotesAggregate(vote.ID, room.verificationStartAt, room.startAt)
const castVotes = useContractFunction(votingContract, 'castVotes')
const { finalizeVotingLimit, batchedVotes } = useVotingBatches({ room })
const finalizeVoting = useContractFunction(votingContract, 'finalizeVotingRoom')
room = useRoomAggregateVotes(room, false)
const now = Date.now() / 1000
const verificationStarted = room.verificationStartAt.toNumber() - now < 0
const verificationEnded = room.endAt.toNumber() - now < 0
const verificationPeriod = verificationStarted && !verificationEnded
useEffect(() => {
const checkPeriod = () => {
const now = Date.now() / 1000
const verificationStarted = room.verificationStartAt.toNumber() - now < 0
const verificationEnded = room.endAt.toNumber() - now < 0
const verificationPeriod = verificationStarted && !verificationEnded
const finalizationPeriod = verificationStarted && verificationEnded
setVerificationPeriod(verificationPeriod)
setFinalizationPeriod(finalizationPeriod)
}
checkPeriod()
const timer = setInterval(checkPeriod, 1000)
return () => clearInterval(timer)
}, [])
useEffect(() => {
if (finalizeVoting.state.status === 'Success' || castVotes.state.status === 'Success') {
history.go(0)
}
}, [finalizeVoting.state.status, castVotes.state.status])
const winner = verificationPeriod ? 0 : getVotingWinner(vote)
@ -117,7 +135,7 @@ export const CardVoteMobile = ({ room }: CardVoteMobileProps) => {
{verificationPeriod && (
<VoteBtnFinal
onClick={async () => {
await castVotes.send(votes)
await castVotes.send(batchedVotes)
setSentVotesFor(0)
setSentVotesAgainst(0)
@ -127,13 +145,16 @@ export const CardVoteMobile = ({ room }: CardVoteMobileProps) => {
Verify votes
</VoteBtnFinal>
)}
{Boolean(winner) && (
<VoteBtnFinal onClick={() => finalizeVoting.send(room.roomNumber)} disabled={!account}>
{finalizationPeriod && (
<VoteBtnFinal
onClick={() => finalizeVoting.send(room.roomNumber, finalizeVotingLimit < 1 ? 1 : finalizeVotingLimit)}
disabled={!account}
>
Finalize the vote <span></span>
</VoteBtnFinal>
)}
{!verificationPeriod && !winner && (
{!verificationPeriod && !finalizationPeriod && (
<VotesBtns>
<VoteBtn
disabled={!canVote}

View File

@ -15,16 +15,32 @@ import { CommunitySkeleton } from '../components/skeleton/CommunitySkeleton'
import { HeaderVotingMobile } from './VotingMobile'
import { ConnectMobile } from './ConnectMobile'
import { HistoryLink } from './CardVoteMobile'
import { useEthers } from '@usedapp/core'
import { useContractCall, useContractFunction, useEthers } from '@usedapp/core'
import { useGetCurrentVoting } from '../hooks/useGetCurrentVoting'
import { MobileHeading, MobileBlock, MobileTop, MobileWrap, ColumnFlexDiv } from '../constants/styles'
import { useFeaturedVotes } from '../hooks/useFeaturedVotes'
import { useContracts } from '../hooks/useContracts'
import { useSendWakuFeature } from '../hooks/useSendWakuFeature'
import { useFeaturedVotingState } from '../hooks/useFeaturedVotingState'
export function FeatureMobile() {
const { publicKey } = useParams<{ publicKey: string }>()
const [community] = useCommunities([publicKey])
const [proposingAmount, setProposingAmount] = useState(0)
const { account } = useEthers()
const disabled = proposingAmount === 0 || !account
const sendWaku = useSendWakuFeature()
const { activeVoting } = useFeaturedVotes()
const { featuredVotingContract } = useContracts()
const { send } = useContractFunction(featuredVotingContract, 'initializeVoting')
const featuredVotingState = useFeaturedVotingState(activeVoting)
const [isInCooldownPeriod] =
useContractCall({
abi: featuredVotingContract.interface,
address: featuredVotingContract.address,
method: 'isInCooldownPeriod',
args: [community.publicKey],
}) ?? []
const inFeatured = isInCooldownPeriod
const [showHistory, setShowHistory] = useState(false)
const isDisabled = community ? community.votingHistory.length === 0 : false
@ -47,9 +63,19 @@ export function FeatureMobile() {
<MobileBlock>
<FeatureHeading>{`Feature ${community.name}?`}</FeatureHeading>
<VotePropose setProposingAmount={setProposingAmount} proposingAmount={proposingAmount} />
<FeatureBtn disabled>Coming soon!</FeatureBtn>
<FeatureBtn disabled={disabled}>
<FeatureBtn
disabled={
!account || inFeatured || featuredVotingState === 'verification' || featuredVotingState === 'ended'
}
onClick={async () => {
if (!activeVoting) {
await send(community.publicKey, proposingAmount)
} else {
await sendWaku(proposingAmount, community.publicKey)
}
history.go(-1)
}}
>
Feature this community! <span style={{ fontSize: '20px' }}></span>
</FeatureBtn>
{currentVoting && (

View File

@ -10,6 +10,7 @@ export interface Config {
wakuFeatureTopic: string
}
daapConfig: DAppConfig
votesLimit: number
}
/**
@ -41,6 +42,7 @@ const configs: Record<typeof process.env.ENV, Config> = {
expirationPeriod: 50000,
},
},
votesLimit: 2,
},
/**
* Preview/Stage.
@ -64,6 +66,7 @@ const configs: Record<typeof process.env.ENV, Config> = {
expirationPeriod: 50000,
},
},
votesLimit: 2,
},
/**
* Production.
@ -82,6 +85,7 @@ const configs: Record<typeof process.env.ENV, Config> = {
expirationPeriod: 50000,
},
},
votesLimit: 400,
},
}

View File

@ -44,6 +44,8 @@ export const communities: Array<CommunityDetail> = [
type: 'Remove',
voteFor: BigNumber.from(16740235),
voteAgainst: BigNumber.from(6740235),
votingEndAt: 10000,
verificationEndAt: 10000,
},
featureVotes: BigNumber.from(62142321),
directoryInfo: {
@ -145,6 +147,8 @@ export const communities: Array<CommunityDetail> = [
type: 'Add',
voteFor: BigNumber.from(16740235),
voteAgainst: BigNumber.from(126740235),
votingEndAt: 10000,
verificationEndAt: 10000,
},
},
{
@ -165,6 +169,8 @@ export const communities: Array<CommunityDetail> = [
type: 'Add',
voteFor: BigNumber.from(16740235),
voteAgainst: BigNumber.from(126740235),
votingEndAt: 10000,
verificationEndAt: 10000,
},
},
]

View File

@ -1,34 +0,0 @@
import { FeaturedVoting } from '../models/smartContract'
type Phase = 'not started' | 'voting' | 'verification' | 'ended' | null
export function getFeaturedVotingState(featuredVoting: FeaturedVoting | null): Phase {
const currentTimestamp = Math.floor(Date.now() / 1000)
if (!featuredVoting) {
return null
}
if (featuredVoting.startAt.toNumber() > currentTimestamp) {
return 'not started'
}
if (featuredVoting.verificationStartAt.toNumber() > currentTimestamp) {
return 'voting'
}
if (
featuredVoting.verificationStartAt.toNumber() < currentTimestamp &&
featuredVoting.endAt.toNumber() > currentTimestamp
) {
return 'verification'
}
if (featuredVoting.endAt.toNumber() < currentTimestamp) {
return 'ended'
}
return 'ended'
}
export default { getFeaturedVotingState }

View File

@ -14,6 +14,8 @@ export function votingFromRoom(votingRoom: VotingRoom) {
const currentVoting: CurrentVoting = {
timeLeft: votingRoom.verificationStartAt.toNumber() - currentTimestamp,
timeLeftVerification: votingRoom.endAt.toNumber() - currentTimestamp,
votingEndAt: votingRoom.verificationStartAt.toNumber(),
verificationEndAt: votingRoom.endAt.toNumber(),
type: votingRoom.voteType === 1 ? 'Add' : 'Remove',
voteFor: votingRoom.totalVotesFor,
voteAgainst: votingRoom.totalVotesAgainst,

View File

@ -0,0 +1,46 @@
import { config } from '../config'
import { useFeaturedVotes } from './useFeaturedVotes'
export const useFeaturedBatches = () => {
const { activeVoting, votes } = useFeaturedVotes()
if (!activeVoting) {
return {
finalizeVotingLimit: 1,
batchCount: 1,
batchDoneCount: 0,
beingEvaluated: false,
beingFinalized: false,
}
}
const evaluated = activeVoting.evaluated
const finalized = activeVoting.finalized
const allVotes = votes ?? {}
const votesCount: number = Object.values(allVotes).reduce(
(acc: number, curr: any) => acc + Object.keys(curr?.votes).length,
0
)
const beingFinalized = !evaluated && finalized
const beingEvaluated = evaluated && !finalized
const currentPosition = activeVoting.evaluatingPos
const firstFinalization = beingEvaluated && currentPosition === votesCount + 1
const votesLeftCount = votesCount - currentPosition + 1
const finalizeVotingLimit = firstFinalization
? Math.min(votesCount, config.votesLimit)
: Math.min(votesLeftCount, config.votesLimit)
const batchCount = Math.ceil((beingFinalized ? votesCount + 1 : votesCount) / config.votesLimit)
const batchLeftCount = Math.ceil(votesLeftCount / config.votesLimit)
const batchDoneCount = batchCount - batchLeftCount
return {
finalizeVotingLimit,
batchCount,
batchDoneCount,
beingFinalized,
beingEvaluated,
}
}

View File

@ -35,7 +35,7 @@ export function useFeaturedVotes() {
if (featuredVotings) {
const lastVoting: FeaturedVoting = featuredVotings[featuredVotings.length - 1]
if (lastVoting && !lastVoting.finalized) {
if (lastVoting && (!lastVoting.evaluated || !lastVoting.finalized)) {
setActiveVoting(lastVoting)
}
}

View File

@ -0,0 +1,40 @@
import { useEffect, useState } from 'react'
import { FeaturedVoting } from '../models/smartContract'
type Phase = 'not started' | 'voting' | 'verification' | 'ended' | null
export const useFeaturedVotingState = (featuredVoting: FeaturedVoting | null): Phase => {
const [votingState, setVotingState] = useState<Phase>(null)
useEffect(() => {
const getState = () => {
const currentTimestamp = Math.floor(Date.now() / 1000)
const startAt = featuredVoting?.startAt.toNumber() ?? 0
const verificationStartAt = featuredVoting?.verificationStartAt.toNumber() ?? 0
const endAt = featuredVoting?.endAt.toNumber() ?? 0
if (!featuredVoting || featuredVoting === null) {
setVotingState(null)
} else if (endAt < currentTimestamp) {
setVotingState('ended')
} else if (verificationStartAt < currentTimestamp && endAt > currentTimestamp) {
setVotingState('verification')
} else if (verificationStartAt > currentTimestamp) {
setVotingState('voting')
} else if (startAt > currentTimestamp) {
setVotingState('not started')
}
}
const timer = setInterval(() => {
getState()
}, 1000)
getState()
return () => clearInterval(timer)
})
return votingState
}

View File

@ -3,13 +3,13 @@ import { DetailedVotingRoom } from '../models/smartContract'
import { useVotesAggregate } from './useVotesAggregate'
export function useRoomAggregateVotes(room: DetailedVotingRoom, showConfirmModal: boolean) {
const { votes } = useVotesAggregate(room.roomNumber, room.verificationStartAt, room.startAt)
const { votesToSend } = useVotesAggregate(room.roomNumber, room.verificationStartAt, room.startAt)
const [returnRoom, setReturnRoom] = useState(room)
useEffect(() => {
if (room.endAt.toNumber() > Date.now() / 1000 && showConfirmModal === false) {
const reducedVotes = votes.reduce(
const reducedVotes = votesToSend.reduce(
(accumulator, vote) => {
if (vote[1].mod(2).toNumber()) {
return { for: accumulator.for.add(vote[2]), against: accumulator.against }
@ -20,7 +20,7 @@ export function useRoomAggregateVotes(room: DetailedVotingRoom, showConfirmModal
)
setReturnRoom({ ...room, totalVotesAgainst: reducedVotes.against, totalVotesFor: reducedVotes.for })
}
}, [JSON.stringify(votes), JSON.stringify(room), showConfirmModal])
}, [JSON.stringify(votesToSend), JSON.stringify(room), showConfirmModal])
return returnRoom
}

View File

@ -0,0 +1,13 @@
import { useEffect, useState } from 'react'
export function useTimeLeft(timeEndAt: number): number {
const [timeLeft, setTimeLeft] = useState(timeEndAt - Math.floor(Date.now() / 1000))
useEffect(() => {
const timer = setInterval(() => setTimeLeft(timeEndAt - Math.floor(Date.now() / 1000)), 1000)
return () => clearInterval(timer)
}, [])
return timeLeft
}

View File

@ -20,19 +20,22 @@ export function useVotesAggregate(room: number | undefined, verificationStartAt:
}) ?? []
const { waku } = useWaku()
const [votesToSend, setVotesToSend] = useState<any[]>([])
const [allVotes, setAllVotes] = useState<any[]>([])
const { getTypedVote } = useTypedVote()
useEffect(() => {
const accumulateVotes = async () => {
if (waku && alreadyVotedList && room) {
const messages = await wakuMessage.receive(waku, config.wakuConfig.wakuTopic, room)
const validMessages = messages?.filter((message) => validateVote(message, verificationStartAt, startAt))
const validMessages = messages?.filter((message) => validateVote(message, verificationStartAt, startAt)) ?? []
const verifiedMessages = wakuMessage.filterVerified(validMessages, alreadyVotedList, getTypedVote)
setAllVotes(validMessages)
setVotesToSend(verifiedMessages)
}
}
accumulateVotes()
}, [waku, room, alreadyVotedList])
return { votes: votesToSend }
return { votesToSend, allVotes }
}

View File

@ -0,0 +1,38 @@
import { config } from '../config'
import voting from '../helpers/voting'
import { DetailedVotingRoom } from '../models/smartContract'
import { useRoomAggregateVotes } from './useRoomAggregateVotes'
import { useVotesAggregate } from './useVotesAggregate'
interface Props {
room: DetailedVotingRoom
}
export const useVotingBatches = ({ room }: Props) => {
const vote = voting.fromRoom(room)
const { votesToSend, allVotes } = useVotesAggregate(vote.ID, room.verificationStartAt, room.startAt)
const { evaluated, finalized, evaluatingPos } = useRoomAggregateVotes(room, false)
const beingFinalized = !evaluated && finalized
const beingEvaluated = evaluated && !finalized
const firstFinalization = beingEvaluated && evaluatingPos === allVotes.length + 1
const votesLeftCount = allVotes.length - evaluatingPos + 1
const finalizeVotingLimit = firstFinalization
? Math.min(allVotes.length, config.votesLimit)
: Math.min(votesLeftCount, config.votesLimit)
const batchCount = Math.ceil((beingFinalized ? allVotes.length + 1 : allVotes.length) / config.votesLimit)
const batchLeftCount = Math.ceil(votesLeftCount / config.votesLimit)
const batchDoneCount = batchCount - batchLeftCount
const batchedVotes = votesToSend.slice(0, finalizeVotingLimit)
return {
finalizeVotingLimit,
batchCount,
batchDoneCount,
beingFinalized,
beingEvaluated,
batchedVotes,
}
}

View File

@ -8,6 +8,8 @@ export type CurrentVoting = {
voteFor: BigNumber // number of snt for a vote
voteAgainst: BigNumber // number of snt against a vote
ID?: number // id of voting room
verificationEndAt: number
votingEndAt: number
}
export type CommunityDetail = {

View File

@ -27,4 +27,6 @@ export type FeaturedVoting = {
verificationStartAt: BigNumber
finalized: boolean
evaluatingPos: number
evaluated: boolean
endBlock: BigNumber
}

View File

@ -1,12 +1,14 @@
import React from 'react'
import { VotingCards } from '../components/votes/VotingCards'
import { VotesInfo } from '../components/votes/VotesInfo'
import { NotificationsList } from '../components/NotificationsList'
export function Votes() {
return (
<div>
<VotesInfo />
<VotingCards />
<NotificationsList type="votes" />
</div>
)
}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'
import React, { useEffect, useState } from 'react'
import { DirectoryCard } from '../components/directory/DirectoryCard'
import { TopBarMobile } from '../componentsMobile/TopBarMobile'
import { useDirectoryCommunities } from '../hooks/useDirectoryCommunities'
@ -11,13 +11,42 @@ import { WeeklyFeature } from '../components/WeeklyFeature'
import { FilterList } from '../components/Filter'
import { useHistory } from 'react-router'
import { DirectorySkeletonMobile } from '../componentsMobile/DirectorySkeletonMobile'
import { useContractFunction, useEthers } from '@usedapp/core'
import { useContracts } from '../hooks/useContracts'
import { useFeaturedVotes } from '../hooks/useFeaturedVotes'
import { useFeaturedVotingState } from '../hooks/useFeaturedVotingState'
import { config } from '../config'
import { ConnectButton } from '../components/ConnectButton'
import { ProposeButton } from './VotesMobile'
import { useFeaturedBatches } from '../hooks/useFeaturedBatches'
import { mapFeaturesVotes, receiveWakuFeature } from '../helpers/receiveWakuFeature'
import { useTypedFeatureVote } from '../hooks/useTypedFeatureVote'
import { useWaku } from '../providers/waku/provider'
export function DirectoryMobile() {
const { account } = useEthers()
const { featuredVotingContract } = useContracts()
const { getTypedFeatureVote } = useTypedFeatureVote()
const { waku } = useWaku()
const { activeVoting } = useFeaturedVotes()
const featuredVotingState = useFeaturedVotingState(activeVoting)
const castVotes = useContractFunction(featuredVotingContract, 'castVotes')
const finalizeVoting = useContractFunction(featuredVotingContract, 'finalizeVoting')
const [filterKeyword, setFilterKeyword] = useState('')
const [sortedBy, setSortedBy] = useState(DirectorySortingEnum.IncludedRecently)
const [communities, publicKeys] = useDirectoryCommunities(filterKeyword, sortedBy)
const history = useHistory()
const { finalizeVotingLimit, batchCount, batchDoneCount, beingEvaluated, beingFinalized } = useFeaturedBatches()
useEffect(() => {
if (finalizeVoting.state.status === 'Success' || castVotes.state.status === 'Success') {
history.go(0)
}
}, [finalizeVoting.state.status, castVotes.state.status])
const renderCommunities = () => {
if (!publicKeys) {
return null
@ -61,6 +90,49 @@ export function DirectoryMobile() {
<Voting>
<WeeklyFeature />
{renderCommunities()}
<>
{!account && <ConnectButton />}
{account && featuredVotingState === 'verification' && (
<ProposeButton
onClick={async () => {
const { votesToSend } = await receiveWakuFeature(
waku,
config.wakuConfig.wakuFeatureTopic,
activeVoting!
)
const votes = mapFeaturesVotes(votesToSend, getTypedFeatureVote)
const batchedVotes = votes.slice(
batchDoneCount * config.votesLimit,
batchDoneCount * config.votesLimit + finalizeVotingLimit
)
await castVotes.send(batchedVotes)
}}
>
Verify Weekly featured{' '}
{batchCount > 1 && (
<>
({beingEvaluated ? batchDoneCount : 0}/{batchCount})
</>
)}
</ProposeButton>
)}
{account && featuredVotingState === 'ended' && (
<ProposeButton
onClick={() => {
finalizeVoting.send(finalizeVotingLimit < 1 ? 1 : finalizeVotingLimit)
}}
>
Finalize Weekly featured{' '}
{batchCount > 1 && (
<>
({beingFinalized ? batchDoneCount : 0}/{batchCount})
</>
)}
</ProposeButton>
)}
</>
</Voting>
</div>
)

View File

@ -2,10 +2,10 @@ import React from 'react'
import { DirectoryCard } from '../components/directory/DirectoryCard'
import { TopBarMobile } from '../componentsMobile/TopBarMobile'
import styled from 'styled-components'
import { SearchEmpty } from '../components/SearchEmpty'
import { useHistory } from 'react-router'
import { DirectorySkeletonMobile } from '../componentsMobile/DirectorySkeletonMobile'
import { useFeaturedCommunities } from '../hooks/useFeaturedCommunities'
import { Colors } from '../constants/styles'
export function FeaturedMobile() {
const [communities, publicKeys] = useFeaturedCommunities()
@ -17,7 +17,11 @@ export function FeaturedMobile() {
}
if (publicKeys.length === 0) {
return <SearchEmpty />
return (
<EmptyWrap>
<span>No communities were featured last week.</span>
</EmptyWrap>
)
}
if (communities.length === 0) {
@ -41,6 +45,43 @@ export function FeaturedMobile() {
)
}
const EmptyWrap = styled.div`
position: absolute;
top: 96px;
left: 50%;
transform: translateX(-50%);
padding: 0 32px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: calc(100vh - 96px);
background: ${Colors.White};
z-index: 99;
@media (max-width: 600px) {
height: 250px;
top: 50vh;
padding: 0 16px;
}
& > p {
font-weight: bold;
font-size: 64px;
line-height: 64%;
margin-bottom: 24px;
@media (max-width: 600px) {
font-size: 44px;
}
@media (max-width: 375px) {
font-size: 34px;
}
}
`
const Voting = styled.div`
padding: 256px 16px 16px;

View File

@ -7,9 +7,10 @@ describe('voting', () => {
describe('fromRoom', () => {
it('success', () => {
const votingRoom: VotingRoom = {
endAt: BigNumber.from(10000200),
startBlock: BigNumber.from(10000000),
startAt: BigNumber.from(10000050),
startBlock: BigNumber.from(10000000),
endAt: BigNumber.from(10000200),
endBlock: BigNumber.from(10000300),
verificationStartAt: BigNumber.from(10000100),
voteType: 0,
finalized: false,
@ -17,9 +18,8 @@ describe('voting', () => {
totalVotesFor: BigNumber.from(100),
totalVotesAgainst: BigNumber.from(100),
roomNumber: 1,
endBlock: BigNumber.from(0),
evaluatingPos: 0,
evaluated: false,
evaluatingPos: 1,
}
const room = voting.fromRoom(votingRoom)
@ -29,9 +29,10 @@ describe('voting', () => {
})
it('different type', () => {
const votingRoom: VotingRoom = {
endAt: BigNumber.from(10000200),
startBlock: BigNumber.from(10000000),
startAt: BigNumber.from(10000050),
startBlock: BigNumber.from(10000000),
endAt: BigNumber.from(10000200),
endBlock: BigNumber.from(10000300),
verificationStartAt: BigNumber.from(10000100),
voteType: 1,
finalized: false,
@ -39,9 +40,8 @@ describe('voting', () => {
totalVotesFor: BigNumber.from(1000),
totalVotesAgainst: BigNumber.from(100),
roomNumber: 1,
endBlock: BigNumber.from(0),
evaluatingPos: 0,
evaluated: false,
evaluatingPos: 1,
}
const room = voting.fromRoom(votingRoom)

View File

@ -12,6 +12,8 @@ describe('voting', () => {
type: 'Add',
voteFor: BigNumber.from(1000),
voteAgainst: BigNumber.from(100),
votingEndAt: 10000,
verificationEndAt: 10000,
}
expect(voting.getWinner(vote)).to.eq(2)
})
@ -22,6 +24,8 @@ describe('voting', () => {
type: 'Add',
voteFor: BigNumber.from(100),
voteAgainst: BigNumber.from(1000),
votingEndAt: 10000,
verificationEndAt: 10000,
}
expect(voting.getWinner(vote)).to.eq(1)
})
@ -32,6 +36,8 @@ describe('voting', () => {
type: 'Add',
voteFor: BigNumber.from(100),
voteAgainst: BigNumber.from(1000),
votingEndAt: 10000,
verificationEndAt: 10000,
}
expect(voting.getWinner(vote)).to.eq(undefined)
})
@ -42,6 +48,8 @@ describe('voting', () => {
type: 'Add',
voteFor: BigNumber.from(100),
voteAgainst: BigNumber.from(100),
votingEndAt: 10000,
verificationEndAt: 10000,
}
expect(voting.getWinner(vote)).to.eq(1)
})