-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1177 from AutarkLabs/1114-dotvoting-aragonDS
1114 dotvoting aragon ds
- Loading branch information
Showing
36 changed files
with
1,639 additions
and
1,617 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,201 +1,135 @@ | ||
import React from 'react' | ||
import React, { useCallback, useEffect, useState } from 'react' | ||
import PropTypes from 'prop-types' | ||
import { AppBar, AppView, Main, SidePanel } from '@aragon/ui' | ||
import Decisions from './Decisions' | ||
import tokenBalanceOfAbi from './abi/token-balanceof.json' | ||
import tokenDecimalsAbi from './abi/token-decimals.json' | ||
import tokenSymbolAbi from './abi/token-symbol.json' | ||
import { hasLoadedVoteSettings } from './utils/vote-settings' | ||
import { NewPayoutVotePanelContent } from './components/Panels' | ||
import { VotePanelContent } from './components/Panels' | ||
import { AppTitle, networkContextType } from '../../../shared/ui' | ||
import { ASSETS_URL, EmptyStateCard, Header, Main } from '@aragon/ui' | ||
import { useAragonApi } from '@aragon/api-react' | ||
import { isBefore } from 'date-fns' | ||
import { getQuorumProgress, getTotalSupport } from './utils/vote-utils' | ||
import { safeDiv } from './utils/math-utils' | ||
import { IdentityProvider } from '../../../shared/identity' | ||
import Decisions from './Decisions' | ||
import emptyStatePng from './assets/voting-empty-state.png' | ||
|
||
const tokenAbi = [].concat(tokenBalanceOfAbi, tokenDecimalsAbi, tokenSymbolAbi) | ||
|
||
const initialState = { | ||
template: null, | ||
templateData: {}, | ||
stepIndex: 0, | ||
settingsLoaded: false, | ||
panelActive: false, | ||
currentVoteId: -1, | ||
currentVote: null, | ||
voteSidebarOpened: false, | ||
} | ||
const illustration = <img src={emptyStatePng} alt="" height="160" /> | ||
|
||
class App extends React.Component { | ||
static propTypes = { | ||
api: PropTypes.object, | ||
displayMenuButton: PropTypes.bool.isRequired, | ||
votes: PropTypes.arrayOf(PropTypes.object).isRequired, | ||
entries: PropTypes.arrayOf(PropTypes.object).isRequired, | ||
connectedAccount: PropTypes.string.isRequired, | ||
network: PropTypes.object, | ||
tokenAddress: PropTypes.string.isRequired, | ||
minParticipationPct: PropTypes.number.isRequired, | ||
pctBase: PropTypes.number.isRequired, | ||
voteTime: PropTypes.number.isRequired, | ||
} | ||
|
||
static defaultProps = { | ||
network: {}, | ||
votes: [], | ||
entries: [], | ||
tokenAddress: '', | ||
voteTime: 0, | ||
minParticipationPct: 0, | ||
pctBase: 0, | ||
} | ||
|
||
static childContextTypes = { | ||
network: networkContextType, | ||
} | ||
|
||
constructor(props) { | ||
super(props) | ||
this.state = { | ||
...initialState, | ||
tokenContract: this.getTokenContract(this.props.tokenAddress), | ||
} | ||
} | ||
const useVoteCloseWatcher = () => { | ||
const { votes = [], voteTime = 0 } = useAragonApi().appState | ||
const [ now, setNow ] = useState(new Date().getTime()) | ||
|
||
getChildContext() { | ||
const { network } = this.props | ||
return { | ||
network: { | ||
type: network.type, | ||
}, | ||
} | ||
} | ||
|
||
componentWillReceiveProps(nextProps) { | ||
const { settingsLoaded } = this.state | ||
// Is this the first time we've loaded the settings? | ||
if (!settingsLoaded && hasLoadedVoteSettings(nextProps)) { | ||
this.setState({ | ||
settingsLoaded: true, | ||
}) | ||
} | ||
if (nextProps.tokenAddress !== this.props.tokenAddress) { | ||
this.setState({ | ||
tokenContract: this.getTokenContract(nextProps.tokenAddress), | ||
}) | ||
useEffect(() => { | ||
const timeouts = {} | ||
|
||
votes.forEach(({ voteId: id, data: { startDate } }) => { | ||
const endTime = new Date(startDate + voteTime).getTime() | ||
|
||
if (endTime < now) return // ignore; voting has closed | ||
|
||
timeouts[id] = setTimeout( | ||
() => setNow(new Date().getTime()), | ||
endTime - now | ||
) | ||
}) | ||
|
||
return function cleanup() { | ||
for (let id in timeouts) { | ||
clearTimeout(timeouts[id]) | ||
} | ||
} | ||
} | ||
|
||
getTokenContract(tokenAddress) { | ||
return tokenAddress && this.props.api.external(tokenAddress, tokenAbi) | ||
} | ||
handlePanelOpen = () => { | ||
this.setState({ panelActive: true }) | ||
} | ||
|
||
handlePanelClose = () => { | ||
this.setState({ panelActive: false }) | ||
} | ||
|
||
handleResolveLocalIdentity = address => { | ||
return this.props.api.resolveAddressIdentity(address).toPromise() | ||
} | ||
|
||
handleShowLocalIdentityModal = address => { | ||
return this.props.api | ||
}, [ votes, voteTime ]) | ||
} | ||
|
||
const Wrap = ({ children }) => ( | ||
<Main assetsUrl={ASSETS_URL}> | ||
<Header primary="Dot Voting" /> | ||
{children} | ||
</Main> | ||
) | ||
|
||
Wrap.propTypes = { | ||
children: PropTypes.node.isRequired, | ||
} | ||
|
||
const Empty = () => ( | ||
<Wrap> | ||
<div | ||
css={` | ||
position: fixed; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate(-50%, -50%); | ||
z-index: -1; | ||
`} | ||
> | ||
<EmptyStateCard | ||
title="You do not have any dot votes." | ||
text="Use the Allocations app to get started." | ||
onActivate={() => <div />} | ||
illustration={illustration} | ||
/> | ||
</div> | ||
</Wrap> | ||
) | ||
|
||
const App = () => { | ||
useVoteCloseWatcher() | ||
|
||
const { api, appState = {} } = useAragonApi() | ||
|
||
const handleResolveLocalIdentity = useCallback(address => { | ||
return api.resolveAddressIdentity(address).toPromise() | ||
}, [api]) | ||
|
||
const handleShowLocalIdentityModal = useCallback(address => { | ||
return api | ||
.requestAddressIdentityModification(address) | ||
.toPromise() | ||
} | ||
|
||
handleVoteOpen = selectedVote => { | ||
this.setState({ | ||
currentVoteId: selectedVote.voteId, | ||
currentVote: selectedVote, | ||
voteVisible: true, | ||
voteSidebarOpened: false, | ||
}) | ||
} | ||
|
||
handleVote = (voteId, supports) => { | ||
this.props.api.vote(voteId, supports).toPromise() | ||
this.handleVoteClose() | ||
} | ||
handleVoteTransitionEnd = opened => { | ||
this.setState(opened ? { voteSidebarOpened: true } : { currentVoteId: -1, currentVote: null }) | ||
} | ||
|
||
handleVoteClose = () => { | ||
this.setState({ | ||
currentVoteId: -1, | ||
currentVote: null, | ||
}) | ||
} | ||
|
||
render() { | ||
const { displayMenuButton = false } = this.props | ||
return ( | ||
<Main> | ||
<IdentityProvider | ||
onResolve={this.handleResolveLocalIdentity} | ||
onShowLocalIdentityModal={this.handleShowLocalIdentityModal}> | ||
<AppView | ||
appBar={ | ||
<AppBar> | ||
<AppTitle | ||
title="Dot Voting" | ||
displayMenuButton={displayMenuButton} | ||
css="padding-left: 30px" | ||
/> | ||
</AppBar> | ||
} | ||
> | ||
<Decisions | ||
onActivate={this.handlePanelOpen} | ||
app={this.props.api} | ||
votes={this.props.votes !== undefined ? this.props.votes : []} | ||
entries={this.props.entries !== undefined ? this.props.entries : []} | ||
voteTime={this.props.voteTime} | ||
minParticipationPct={this.props.minParticipationPct / 10 ** 16} | ||
pctBase={this.props.pctBase / 10 ** 16} | ||
tokenAddress={this.props.tokenAddress} | ||
userAccount={this.props.connectedAccount} | ||
onSelectVote={this.handleVoteOpen} | ||
/> | ||
</AppView> | ||
|
||
<SidePanel | ||
title={''} | ||
opened={this.state.panelActive} | ||
onClose={this.handlePanelClose} | ||
> | ||
<NewPayoutVotePanelContent /> | ||
</SidePanel> | ||
|
||
{this.state.currentVote && (<SidePanel | ||
title={'Dot Vote #' + this.state.currentVoteId} | ||
opened={!!this.state.currentVote} | ||
onClose={this.handleVoteClose} | ||
onTransitionEnd={this.handleVoteTransitionEnd} | ||
> | ||
<VotePanelContent | ||
app={this.props.api} | ||
vote={this.state.currentVote} | ||
user={this.props.connectedAccount} | ||
ready={this.state.voteSidebarOpened} | ||
tokenContract={this.state.tokenContract} | ||
onVote={this.handleVote} | ||
minParticipationPct={this.props.minParticipationPct / 10 ** 16} | ||
/> | ||
</SidePanel> | ||
)} | ||
|
||
</IdentityProvider> | ||
</Main> | ||
) | ||
} | ||
}, [api]) | ||
|
||
const { | ||
votes = [], | ||
entries = [], | ||
voteTime = 0, | ||
minParticipationPct = 0, | ||
pctBase = 0, | ||
} = appState | ||
|
||
const getAddressLabel = useCallback(option => { | ||
const entry = entries.find(entry => entry.addr === option.label) | ||
return entry ? entry.data.name : option.label | ||
}, [entries]) | ||
|
||
// TODO: move this logic to script.js so it's available app-wide by default | ||
const decorateVote = useCallback(vote => { | ||
const endDate = new Date(vote.data.startDate + voteTime) | ||
vote.data.options = vote.data.options.map(option => ({ | ||
...option, | ||
label: getAddressLabel(option) | ||
})) | ||
return { | ||
...vote, | ||
endDate, | ||
open: isBefore(new Date(), endDate), | ||
quorum: safeDiv(vote.data.minAcceptQuorum, pctBase), | ||
quorumProgress: getQuorumProgress(vote.data), | ||
minParticipationPct: minParticipationPct / 10 ** 16, | ||
description: vote.data.metadata, | ||
totalSupport: getTotalSupport(vote.data), | ||
type: vote.data.type, | ||
} | ||
}, [ voteTime, getAddressLabel, pctBase, minParticipationPct ]) | ||
|
||
if (!votes.length) return <Empty /> | ||
|
||
return ( | ||
<Wrap> | ||
<IdentityProvider | ||
onResolve={handleResolveLocalIdentity} | ||
onShowLocalIdentityModal={handleShowLocalIdentityModal}> | ||
|
||
<Decisions decorateVote={decorateVote} /> | ||
</IdentityProvider> | ||
</Wrap> | ||
) | ||
} | ||
|
||
// eslint-disable-next-line react/display-name | ||
export default () => { | ||
const { api, appState, connectedAccount, displayMenuButton } = useAragonApi() | ||
return <App api={api} {...appState} connectedAccount={connectedAccount} displayMenuButton={displayMenuButton} /> | ||
} | ||
export default App |
Oops, something went wrong.