11'use client'
22
3- import { useCallback , useEffect , useState } from 'react'
3+ import { useCallback , useEffect , useRef , useState } from 'react'
44import { logger } from '@/lib/logger'
55import toast from 'react-hot-toast'
66import { ChartBarIcon } from '@heroicons/react/24/outline'
@@ -12,7 +12,7 @@ import { cn, formatNumber } from '@/lib/utils'
1212import { categorizeError } from '@/lib/error-utils'
1313import { pollrPollUrl } from '@/lib/poll-embed'
1414import { pollrIsV4 , pollrIsV5 } from '@/lib/constants'
15- import { choiceDelta , normalizeChoices , sameChoices } from '@/lib/pollr-rules'
15+ import { choiceDelta , confirmsPendingVote , normalizeChoices , sameChoices , type BallotRead , type PendingVote } from '@/lib/pollr-rules'
1616import type { Poll , PollTally } from '@/lib/services'
1717import { pollIsClosed , tallyIsFinal } from '@/lib/services/pollr-vote-service'
1818
@@ -73,7 +73,12 @@ export function PollCard({ pollId, postContent, postAuthorId, className }: PollC
7373 // v5: the selection a submission was asked for when one of its writes went
7474 // unconfirmed. The run stopped there, so later picks were never sent; the
7575 // ballot stays open on this selection until a re-read shows it recorded.
76- const [ pendingWanted , setPendingWanted ] = useState < number [ ] | null > ( null )
76+ const [ pendingVote , setPendingVote ] = useState < PendingVote | null > ( null )
77+ // The last completed, successful read of the voter's ballots — null while a
78+ // load is in flight or when it failed. Only this may confirm a pending vote:
79+ // `myVotes` is reset to an empty placeholder before every read.
80+ const [ ballotRead , setBallotRead ] = useState < BallotRead | null > ( null )
81+ const readGeneration = useRef ( 0 )
7782
7883 const userId = user ?. identityId ?? null
7984 // v5 ballots stay editable until the poll closes; v3 ballots are permanent.
@@ -92,6 +97,7 @@ export function PollCard({ pollId, postContent, postAuthorId, className }: PollC
9297 // failed lookup can't leave the previous account's (or poll's) answer on
9398 // screen as if it belonged to the one now being loaded.
9499 setMyVotes ( [ ] )
100+ setBallotRead ( null )
95101 setTally ( null )
96102 try {
97103 const { pollrPollService, pollrVoteService } = await import ( '@/lib/services' )
@@ -122,6 +128,8 @@ export function PollCard({ pollId, postContent, postAuthorId, className }: PollC
122128 // ballot instead of guessing that the user hasn't voted.
123129 if ( votesResult . status === 'fulfilled' ) {
124130 setMyVotes ( votesResult . value )
131+ readGeneration . current += 1
132+ setBallotRead ( { choices : votesResult . value , generation : readGeneration . current } )
125133 } else {
126134 logger . error ( 'PollCard: failed to load own votes' , votesResult . reason )
127135 setVotesUnavailable ( true )
@@ -145,7 +153,7 @@ export function PollCard({ pollId, postContent, postAuthorId, className }: PollC
145153 const stopEditing = useCallback ( ( ) => {
146154 setSelected ( [ ] )
147155 setEditing ( false )
148- setPendingWanted ( null )
156+ setPendingVote ( null )
149157 } , [ ] )
150158
151159 // Reset any pending selection when switching polls or signing in/out.
@@ -157,14 +165,15 @@ export function PollCard({ pollId, postContent, postAuthorId, className }: PollC
157165 setClosedOnChain ( false )
158166 } , [ pollId ] )
159167
160- // An unconfirmed submission is settled once a fresh read shows the voter's
161- // ballots selecting what was asked for. Until then the ballot stays open on
162- // it, so "Update vote" sends whatever part has not landed.
168+ // An unconfirmed submission is settled once a successful ballot read, made
169+ // after it went unconfirmed, shows the voter's ballots selecting what was
170+ // asked for. A failed or in-flight read settles nothing. Until then the
171+ // ballot stays open on it, so "Update vote" sends whatever has not landed.
163172 useEffect ( ( ) => {
164- if ( loading || ! pendingWanted || ! sameChoices ( myVotes , pendingWanted ) ) return
173+ if ( ! confirmsPendingVote ( pendingVote , ballotRead ) ) return
165174 stopEditing ( )
166175 toast . success ( 'Vote confirmed' )
167- } , [ loading , myVotes , pendingWanted , stopEditing ] )
176+ } , [ pendingVote , ballotRead , stopEditing ] )
168177
169178 const isClosed = closedOnChain || ( poll ? pollIsClosed ( poll ) : false )
170179 const hasVoted = myVotes . length > 0
@@ -212,7 +221,7 @@ export function PollCard({ pollId, postContent, postAuthorId, className }: PollC
212221 // Sent, but not seen yet. Leave the vote and tally as they were rather
213222 // than replace them with a read that likely predates the write.
214223 toast ( 'Your vote was sent but is not confirmed yet.' , { icon : '⏳' , duration : 6000 } )
215- setPendingWanted ( wanted )
224+ setPendingVote ( { wanted, afterRead : readGeneration . current } )
216225 setSelected ( wanted )
217226 setEditing ( true )
218227 return
@@ -513,7 +522,7 @@ export function PollCard({ pollId, postContent, postAuthorId, className }: PollC
513522 ) }
514523 </ div >
515524
516- { pendingWanted && (
525+ { pendingVote && (
517526 < p className = "text-xs text-gray-500 dark:text-gray-400" >
518527 Not confirmed yet.{ ' ' }
519528 < button
0 commit comments