Show the single answer poll subtitle when enforceUniqueVote is set - #6704
ryanhurststrava wants to merge 1 commit into
Conversation
getSubtitle() branched on maxVotesAllowed alone, so a single-answer poll that carries enforceUniqueVote = true without an explicit maxVotesAllowed fell into the null branch and rendered "Select one or more". Check enforceUniqueVote before the maxVotesAllowed branches, matching the ordering the Swift SDK's PollAttachmentView already uses.
|
@CodeRabbit review |
✅ Action performedReview finished.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. Walkthrough
ChangesPoll subtitle behavior
Priority: ⬇️ Low Estimated code review effort: 1 (Trivial) | ~5 minutes Change: Bug fix Suggested reviewers: Merge Risk: ⚪ Minimal · up to Unique-vote polls now show the single-answer subtitle without changing closed-poll or non-unique behavior. The change is ready to merge. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit checks each voting choice Comment |
andremion
left a comment
There was a problem hiding this comment.
Looks good. The fix is right, and I confirmed on the iOS side that a single answer poll sends enforce_unique_vote: true and omits max_votes_allowed, so the null branch is exactly the problem you describe.
Two things that don't fit on a line:
- The same check exists once more, in
PollOptionVotingRow.kt:109in the Compose kit, outside this diff:
val toggleRole = if (poll.maxVotesAllowed == 1) Role.RadioButton else Role.CheckboxFor the same poll the subtitle will now read "Select one" while TalkBack still announces the options as checkboxes. Would you like to fix it here? It is the same one line shape. Also fine to leave it for a follow-up if you'd rather keep this PR small.
- Housekeeping done on our side: I filed AND-1539, linked it in the description, and adjusted the title. The
pr-checklistchecks themselves pass now, the job only stays red because it cannot post its own comment from a fork, so you can ignore it. The CLA is the one part we can't handle for you.
| if (closed) { | ||
| return context.getString(R.string.stream_ui_poll_description_closed) | ||
| } | ||
| if (enforceUniqueVote) { |
There was a problem hiding this comment.
On your canCastVote() question: you were right to leave it alone. With enforce_unique_vote the server treats a vote on a different option as a vote change, so returning true there is the correct behaviour.
The reverse case is the broken one. Our own single answer polls carry both maxVotesAllowed = 1 and enforceUniqueVote = true, so canCastVote() returns false after the first vote, and PollOptionVotingRow.kt:111 and PollView.kt:308 then swallow a tap on another option. The user has to deselect first. iOS lets the switch through.
On your offer to handle that separately: yes please, a follow-up PR would be welcome. Nothing needed in this one.
Goal
Poll.getSubtitle()shows "Select one or more" for polls that only allow a single answer, when those polls were created by the iOS SDK.getSubtitle()decides the subtitle frommaxVotesAllowedalone:Pollalso carriesenforceUniqueVote: Boolean, which is the authoritative "one answer only" signal, and it is ignored here.Why this only shows up for iOS-created polls. When the Android SDK creates a single-answer poll it sets both fields (
AttachmentsPickerPollUtils.kt:maxVotesAllowed = 1,enforceUniqueVote = true), so the1 ->branch is hit and the subtitle is correct. The iOS SDK sendsenforce_unique_vote: trueand omitsmax_votes_allowed(the nil value is skipped by Swift's synthesizedEncodable). The Android client therefore deserializesmaxVotesAllowed == null, falls into thenull ->branch, and renders the unlimited-answers string for a poll that permits exactly one vote.When this became reachable. #6209 moved
maxVotesAllowedfromInttoInt?so that null could mean "unlimited votes". That is a deliberate and correct meaning for polls created without a vote limit — this PR does not undo it. The problem is that a payload omittingmax_votes_allowedis currently treated as "unlimited" even whenenforce_unique_vote: trueis present, andenforceUniqueVoteis exactly the field that disambiguates the two cases.The broken branch ordering is unchanged as of the latest release,
v7.11.0, and unchanged on currentdevelop(this PR is based on929fd8c51f4).Cross-platform inconsistency. StreamChatSwiftUI's
PollAttachmentViewalready checksenforceUniqueVotebeforemaxVotesAllowed:So the same poll reads "Select one" on iOS and "Select one or more" on Android.
Both Android render paths are affected, since they share this helper:
stream-chat-android-compose/.../ui/components/messages/PollMessageContent.ktstream-chat-android-ui-components/.../messages/list/adapter/view/internal/PollView.ktTracked in AND-1539, filed by the Stream team after this PR was opened.
Implementation
One added branch in
stream-chat-android-ui-common/.../utils/extensions/Poll.kt— checkenforceUniqueVotebefore themaxVotesAllowedbranches, so it wins regardless of whethermaxVotesAllowedis present:The
closedshort-circuit and themin(maxVotesAllowed, options.size)clamp are unchanged. No public API signature changes (apiCheckpasses with no dump needed).A question about
canCastVote(), two functions aboveThis has the same blind spot: for an
enforceUniqueVotepoll with a nullmaxVotesAllowed, it returnstrueunconditionally, and it gates vote casting inPollOptionVotingRow,PollView, andAllPollOptionsDialogFragment. I deliberately did not touch it, because I couldn't determine the intended behaviour with confidence and the naive fix looks like it would be wrong: on a single-answer poll a user should presumably still be able to switch their vote to another option, and returningfalseonce they hold one vote would block that. Uniqueness may also be enforced server-side (replacing the existing vote) rather than client-side. Is that the case, and is the current behaviour here intentional? Happy to follow up in a separate PR if you'd like it changed.🎨 UI Changes
Text-only change on the poll subtitle line, for
enforceUniqueVotepolls with nomaxVotesAllowed:Existing Paparazzi snapshots are unaffected —
PreviewPollData.poll1setsmaxVotesAllowed = 1alongsideenforceUniqueVote = true, so its subtitle is unchanged.:stream-chat-android-compose:verifyPaparazziDebug --tests "*PollMessageContentTest*"passes without re-recording.Testing
To reproduce: create a poll from the iOS SDK with "Multiple answers" left off (and no per-person vote limit set), then view that poll in either Android UI kit. Before this change the subtitle reads "Select one or more"; after, "Select one".
PollExtensionsTestcovers the branch table. Added:enforceUniqueVote = true,maxVotesAllowed = null→ single-answer string (the bug)enforceUniqueVote = true,maxVotesAllowed = 1→ single-answer string (unchanged)enforceUniqueVote = false,maxVotesAllowed = null→ unlimited-answers string (regression guard: genuinely unlimited polls must not be caught by the new branch)The pre-existing
getSubtitlecases also had to pinenforceUniqueVote = falseexplicitly. They relied onrandomPoll, whoseenforceUniqueVotedefault israndomBoolean(), so once the new branch exists they would flake roughly half the time.I ran module-scoped tasks only, not the full
./gradlew checkor the instrumented/E2E suites.☑️Contributor Checklist
General
developbranchCode & documentation
Summary by CodeRabbit