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.
The toggle role was derived from maxVotesAllowed alone, so an enforceUniqueVote poll with no vote limit read "Select one" while TalkBack still announced its options as checkboxes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Folded in the PollOptionVotingRow toggleRole fix, since leaving it would mean the subtitle and TalkBack disagree on the same poll. CLA is signed now. spotlessCheck and detekt pass on the compose module; no snapshot changes, as Role is semantics only and PreviewPollData.poll1 sets both fields so its role is unchanged either way. |
andremion
left a comment
There was a problem hiding this comment.
Code looks good. One small thing: the description still only covers the Poll.kt change. Could you update it for the Compose one too?
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Description updated. The implementation section is now split into the subtitle change and the TalkBack role change, the canCastVote part is rewritten to record your answer and why the reverse case is out of scope here, and the testing section notes that the role flip has no new test. KDoc fixed in f18bdbd. Thanks for the review. |
|
🚀 Available in v7.12.0 |
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
The subtitle
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).The TalkBack role
PollOptionVotingRow.ktin the Compose kit derived its toggle role frommaxVotesAllowedalone, so the same poll that now reads "Select one" was still announced to TalkBack as a checkbox. Raised by @andremion in review; folded in here rather than left for a follow-up, since shipping the subtitle fix alone would leave the visible text and the screen reader disagreeing about the same poll:The row's KDoc is updated to match.
Roleis semantics-only, so this changes what TalkBack announces, not what is drawn. The cast/remove gating on the following lines is untouched and still readsmaxVotesAllowedthroughcanCastVote().canCastVote()is deliberately left aloneI asked whether this needed the same treatment. Answered in review: returning
truefor anenforceUniqueVotepoll with a nullmaxVotesAllowedis correct, because the server treats a vote on a different option as a vote change.The reverse case is a real bug, but a separate one. Polls created by the Android SDK carry both
maxVotesAllowed = 1andenforceUniqueVote = true, socanCastVote()returnsfalseonce the user holds a vote and a tap on another option is swallowed atPollOptionVotingRow.kt:111andPollView.kt:308; the user has to deselect first, where iOS lets the switch through. That gates real vote casting across both UI kits and turns on server semantics, so it is out of scope here and left with the Stream team.🎨 UI Changes
Text-only change on the poll subtitle line, for
enforceUniqueVotepolls with nomaxVotesAllowed:No visual change beyond that line: the accompanying Compose change affects only the announced TalkBack role.
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.The Compose change is not covered by a new test. It is a semantics-only role flip with no assertion on
Rolein the existing suites, and adding the harness for one felt disproportionate; happy to add one if you would rather have it pinned.I ran module-scoped tasks only, not the full
./gradlew checkor the instrumented/E2E suites.☑️Contributor Checklist
General
developbranchCode & documentation
Summary by CodeRabbit