Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: rebrand ProposalDetailsView #1985

Merged
merged 6 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export { default as ManageVotingPowerPane } from './ManageVotingPowerPane.svelte
export { default as ProposalAnswer } from './ProposalAnswer.svelte'
export { default as ProposalCard } from './ProposalCard.svelte'
export { default as ProposalDetailsMenu } from './ProposalDetailsMenu.svelte'
export { default as ProposalInformationPane } from './ProposalInformationPane.svelte'
export { default as ProposalList } from './ProposalList.svelte'
export { default as ProposalListDetails } from './ProposalListDetails.svelte'
export { default as ProposalQuestion } from './ProposalQuestion.svelte'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import { Table, Text } from '@bloomwalletio/ui'
import {
participationOverviewForSelectedAccount,
selectedParticipationEventStatus,
selectedProposal,
} from '@contexts/governance/stores'
import { calculateTotalVotesForTrackedParticipations } from '@contexts/governance/utils'
import { selectedAccount } from '@core/account/stores'
import { localize } from '@core/i18n'
import { networkStatus } from '@core/network/stores'
import { activeProfile } from '@core/profile/stores'
import { formatTokenAmountBestMatch } from '@core/token'
import { visibleSelectedAccountTokens } from '@core/token/stores'
import { EventStatus } from '@iota/sdk/out/types'
import { Pane } from '@ui'

const { metadata } = $visibleSelectedAccountTokens?.[$activeProfile?.network?.id]?.baseCoin ?? {}

let totalVotes = BigInt(0)
const hasMounted = false

$: selectedProposalOverview = $participationOverviewForSelectedAccount?.participations?.[$selectedProposal?.id]
$: trackedParticipations = Object.values(selectedProposalOverview ?? {})
$: currentMilestone = $networkStatus.currentMilestone

// Reactively start updating votes once component has mounted and participation overview is available.
$: hasMounted && $selectedParticipationEventStatus && trackedParticipations && currentMilestone && setTotalVotes()

function setTotalVotes(): void {
switch ($selectedParticipationEventStatus?.status) {
case EventStatus.Upcoming:
totalVotes = BigInt(0)
break
case EventStatus.Commencing:
totalVotes = BigInt(0)
break
case EventStatus.Holding:
totalVotes = calculateTotalVotesForTrackedParticipations(trackedParticipations)
break
case EventStatus.Ended:
totalVotes = calculateTotalVotesForTrackedParticipations(trackedParticipations)
break
}
}
</script>

<Pane classes="p-6 h-fit shrink-0 space-y-5">
<Text type="body2">
{localize('views.governance.details.yourVote.title')}
</Text>
<Table
items={[
{
key: localize('views.governance.details.yourVote.total'),
value: formatTokenAmountBestMatch(totalVotes, metadata),
},
{
key: localize('views.governance.details.yourVote.power'),
value: formatTokenAmountBestMatch($selectedAccount?.votingPower, metadata),
},
]}
/>
</Pane>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import { MarkdownBlock, Text } from '@bloomwalletio/ui'
import { Pane } from '@ui'
import { ProposalDetailsMenu, ProposalStatusPill } from '../'
import { IProposal } from '@contexts/governance'

export let proposal: IProposal
</script>

<Pane classes="p-6 flex flex-col h-fit">
<header-container class="flex justify-between items-center mb-4">
<ProposalStatusPill {proposal} />
<ProposalDetailsMenu {proposal} modalPosition={{ right: '24px', top: '54px' }} />
</header-container>
<div class="flex flex-1 flex-col space-y-4 justify-between scrollable-y">
<Text type="h4">{proposal?.title}</Text>
{#if proposal?.additionalInfo}
<MarkdownBlock text={proposal?.additionalInfo} />
{/if}
</div>
</Pane>
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<script lang="ts">
import {
EventStatus,
ParticipationEventType,
VotingEventPayload,
TrackedParticipationOverview,
} from '@iota/sdk/out/types'
import { Alert, Button } from '@bloomwalletio/ui'
import { getVotingEvent } from '@contexts/governance/actions'
import { ABSTAIN_VOTE_VALUE } from '@contexts/governance/constants'
import {
participationOverviewForSelectedAccount,
selectedParticipationEventStatus,
selectedProposal,
} from '@contexts/governance/stores'
import { getActiveParticipation, isProposalVotable, isVotingForSelectedProposal } from '@contexts/governance/utils'
import { selectedAccount } from '@core/account/stores'
import { handleError } from '@core/error/handlers'
import { localize } from '@core/i18n'
import { networkStatus } from '@core/network/stores'
import { getBestTimeDuration, milestoneToDate } from '@core/utils'
import { PopupId, openPopup } from '@desktop/auxiliary/popup'
import { ProposalQuestion } from '../../components'
import { Pane } from '@ui'
import { onMount } from 'svelte'

export let statusLoaded: boolean = false
export let overviewLoaded: boolean = false

let selectedAnswerValues: number[] = []
let votedAnswerValues: number[] = []
let votingPayload: VotingEventPayload
let hasMounted = false
let alertText = ''
let proposalQuestions: HTMLElement
let isVotingForProposal: boolean = false
let openedQuestionIndex: number = -1
let isUpdatingVotedAnswerValues: boolean = false
let lastAction: 'vote' | 'stopVote'

$: selectedProposalOverview = $participationOverviewForSelectedAccount?.participations?.[$selectedProposal?.id]
$: trackedParticipations = Object.values(selectedProposalOverview ?? {})
$: currentMilestone = $networkStatus.currentMilestone

// Reactively start updating votes once component has mounted and participation overview is available.
$: hasMounted &&
$selectedParticipationEventStatus &&
trackedParticipations &&
currentMilestone &&
setVotedAnswerValues()
$: hasMounted && selectedProposalOverview && updateIsVoting()

$: questions = votingPayload?.questions

$: if (questions?.length > 0 && selectedAnswerValues?.length === 0) {
selectedAnswerValues = [
...(getActiveParticipation($selectedProposal?.id)?.answers ?? Array.from({ length: questions?.length })),
]
}

$: $selectedParticipationEventStatus, (alertText = getAlertText())

$: hasGovernanceTransactionInProgress =
$selectedAccount?.hasVotingPowerTransactionInProgress || $selectedAccount?.hasVotingTransactionInProgress

$: areSelectedAndVotedAnswersEqual = JSON.stringify(selectedAnswerValues) === JSON.stringify(votedAnswerValues)

$: {
if (hasGovernanceTransactionInProgress) {
isUpdatingVotedAnswerValues = true
}

const hasVoted = lastAction === 'vote' && areSelectedAndVotedAnswersEqual
const hasStoppedVoting = lastAction === 'stopVote' && !areSelectedAndVotedAnswersEqual
if (hasVoted || hasStoppedVoting) {
isUpdatingVotedAnswerValues = hasGovernanceTransactionInProgress
}
}

function hasSelectedNoAnswers(_selectedAnswerValues: number[]): boolean {
return (
_selectedAnswerValues.length === 0 ||
_selectedAnswerValues.every((answerValue) => answerValue === undefined)
)
}

async function setVotingEventPayload(eventId: string): Promise<void> {
try {
const event = await getVotingEvent(eventId)
if (!event) {
throw new Error('Event not found')
}

if (event.data?.payload?.type === ParticipationEventType.Voting) {
votingPayload = event.data.payload
} else {
throw new Error('Event is a staking event')
}
} catch (err) {
handleError(err)
}
}

function updateIsVoting(): void {
isVotingForProposal = isVotingForSelectedProposal()
}

function setVotedAnswerValues(): void {
let lastActiveOverview: TrackedParticipationOverview
switch ($selectedParticipationEventStatus?.status) {
case EventStatus.Commencing:
lastActiveOverview = trackedParticipations?.find((overview) => overview.endMilestoneIndex === 0)
break
case EventStatus.Holding:
lastActiveOverview = trackedParticipations?.find((overview) => overview.endMilestoneIndex === 0)
break
case EventStatus.Ended:
lastActiveOverview = trackedParticipations?.find(
(overview) => overview.endMilestoneIndex > $selectedProposal.milestones.ended
)
break
}
votedAnswerValues = lastActiveOverview?.answers ?? []
}

function onQuestionClick(questionIndex: number): void {
openedQuestionIndex = questionIndex === openedQuestionIndex ? null : questionIndex
}

function onStopVotingClick(): void {
lastAction = 'stopVote'
openPopup({
id: PopupId.StopVoting,
})
}

function onVoteClick(): void {
lastAction = 'vote'
const chosenAnswerValues = selectedAnswerValues.map((answerValue) =>
answerValue === undefined ? ABSTAIN_VOTE_VALUE : answerValue
)
openPopup({
id: PopupId.VoteForProposal,
props: { selectedAnswerValues: chosenAnswerValues },
})
}

function onAnswerClick(answerValue: number, questionIndex: number): void {
selectedAnswerValues[questionIndex] = answerValue

openedQuestionIndex = questionIndex + 1

const selectedQuestionElement: HTMLElement = proposalQuestions?.querySelector(
`proposal-question:nth-child(${openedQuestionIndex})`
)
setTimeout(() => {
proposalQuestions.scrollTo({ top: selectedQuestionElement?.offsetTop, behavior: 'smooth' })
}, 250)
}

function getAlertText(): string {
if (!$selectedProposal) {
return ''
}

const millis =
milestoneToDate(
$networkStatus.currentMilestone,
$selectedProposal.milestones[EventStatus.Commencing]
).getTime() - new Date().getTime()
const timeString = getBestTimeDuration(millis, 'second')
return localize('views.governance.details.hintVote', { values: { time: timeString } })
}

onMount(async () => {
await setVotingEventPayload($selectedProposal?.id)
openedQuestionIndex = votingPayload?.questions.length > 1 ? -1 : 0
hasMounted = true
})
</script>

<Pane classes="w-3/5 h-full p-6 pr-3 flex flex-col justify-between">
<proposal-questions
class="relative flex flex-1 flex-col space-y-5 overflow-y-scroll pr-3"
bind:this={proposalQuestions}
>
{#if questions}
{#each questions as question, questionIndex}
<ProposalQuestion
{question}
{questionIndex}
isOpened={openedQuestionIndex === questionIndex}
isLoading={!overviewLoaded || !statusLoaded}
selectedAnswerValue={selectedAnswerValues[questionIndex]}
votedAnswerValue={votedAnswerValues[questionIndex]}
answerStatuses={$selectedParticipationEventStatus?.questions[questionIndex]?.answers}
{onQuestionClick}
{onAnswerClick}
/>
{/each}
{/if}
</proposal-questions>
{#if $selectedProposal?.status === EventStatus.Upcoming}
<Alert variant="info" text={alertText} />
{:else if [EventStatus.Commencing, EventStatus.Holding].includes($selectedProposal?.status)}
{@const isLoaded = questions && overviewLoaded && statusLoaded}
{@const isStoppingVote = lastAction === 'stopVote' && hasGovernanceTransactionInProgress}
{@const isStopVotingDisabled = !isLoaded || !isVotingForProposal || isUpdatingVotedAnswerValues}
{@const isVoting = lastAction === 'vote' && hasGovernanceTransactionInProgress}
{@const isVotingDisabled =
!isLoaded ||
!isProposalVotable($selectedProposal?.status) ||
hasSelectedNoAnswers(selectedAnswerValues) ||
isUpdatingVotedAnswerValues ||
areSelectedAndVotedAnswersEqual}
<buttons-container class="flex w-full space-x-4 mt-6">
<Button
variant="outlined"
width="full"
on:click={onStopVotingClick}
disabled={isStopVotingDisabled}
busy={isStoppingVote}
text={localize('actions.stopVoting')}
/>
<Button
width="full"
on:click={onVoteClick}
disabled={isVotingDisabled}
busy={isVoting}
text={localize('actions.vote')}
/>
</buttons-container>
{/if}
</Pane>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { default as ProposalAccountVotingPane } from './ProposalAccountVotingPane.svelte'
export { default as ProposalDetailsPane } from './ProposalDetailsPane.svelte'
export { default as ProposalInformationPane } from './ProposalInformationPane.svelte'
export { default as ProposalQuestionListPane } from './ProposalQuestionListPane.svelte'
Loading
Loading