feat(messaging): polls / voting (LC-66) #164
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/lc-66-polls"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Structured polls (LC-66). A poll is a message with a question + 2-10 options; room members vote one click, the block re-renders in place with tallies. Single- or multi-choice, optional anonymous mode, optional auto-close.
Design
0038_polls.sql):polls(message_id PK -> messages ON DELETE CASCADE, question, allows_multi, anonymous, closes_at, closed_at),poll_options,poll_votes. All cascade from the anchor message.db::polls::createinserts message + poll + options in one transaction (atomic); the create path callsfinalize_message_sendso the bubble broadcasts to the room.MessageViewgrowspoll: Option<PollView>, loaded bybuild_poll_viewat every render site. Block renders in#poll-{id}under the body. Non-anonymous lists voter names; anonymous shows counts only and never resolves identities (enforced inbuild_poll_view, so identity never leaves the server).POST /poll/{id}/votetoggles a row + broadcastsChatEvent::PollUpdated; per-connection WS render re-renders#poll-{id}innerHTML so each viewer's highlight stays correct. Single-choice moves/clears; multi permits several. Closed poll => 409.POST /room/{id}/poll) and the slash command/poll "Q" "A" "B" ...(parsed inpost_messageafter the existing gates).spawn_polls_closer(30s tick) closes due polls and broadcasts a re-render.Acceptance criteria
poll_votesand broadcasts a re-rendered fragment.allows_multi=false= one option;true= multiple./poll "Q" "A" "B".Open questions resolved for v1: counts always visible; max 10 options / 300-char question / 150-char option; edit-after-vote not exposed (rename/add deferred).
Tests
routes_polls.rs: atomic create (modal + slash), single move/toggle, multi, closed-poll 409, anonymous privacy, delete cascade. Migration0038appended to hand-rolled lists.just check,just test,just test-saasgreen.🤖 Generated with Claude Code
Add structured polls anchored to a message: a question + 2-10 options, one-click voting that re-renders the poll block in place across the room, single- or multi-choice, optional anonymous mode, and an optional auto-close deadline. Schema (migration 0038): polls(message_id PK -> messages ON DELETE CASCADE, question, allows_multi, anonymous, closes_at, closed_at), poll_options, poll_votes. All three cascade from the anchor message (FK enforcement is on via the sqlx connect default), so a hard delete removes the poll, options, and votes. A poll is anchored to a real messages row (body = question) so search, quote-reply, and pinning keep working. db::polls::create inserts the message + poll + options in one transaction (atomic per the acceptance criteria); the create path then calls finalize_message_send so the rendered bubble (with its poll block) broadcasts to the room. MessageView grows a poll: Option<PollView> field, loaded by build_poll_view at every render site (page history, live NewMessage, edits, threads, DMs). The poll block renders beneath the body in #poll-{id}; for non-anonymous polls it lists voter names, for anonymous polls it shows counts only and never resolves voter identities (enforced in build_poll_view, so identity never leaves the server through any path). Voting: POST /poll/{message_id}/vote toggles a poll_votes row and broadcasts ChatEvent::PollUpdated; the per-connection WS render re-renders #poll-{id} innerHTML so each viewer's "your vote" highlight stays correct. allows_multi=false enforces one option (a new pick moves the vote, re-clicking clears it); allows_multi=true permits several. A closed poll (closed_at set, or closes_at passed) rejects votes with 409 Conflict. Entry points: a "Create poll" compose-box button opens a modal (POST /room/{id}/poll), and the slash command /poll "Question" "Option A" "Option B" ... posts a poll through the same create path (parsed in post_message after the existing access / posting gates). Background: spawn_polls_closer (30s tick) closes polls whose closes_at has passed and broadcasts a re-render so the UI flips to results-only. Tests: routes_polls.rs covers atomic creation (modal + slash), single move/toggle, multi, the closed-poll 409, anonymous voter privacy, and message-delete cascade. Migration 0038 appended to the hand-rolled migration lists. just check, just test, just test-saas all green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>