Files
amethyst/desktopApp/plans/2026-07-16-desktop-polls-manual-testing-sheet.md
T
nrobi144andClaude Opus 4.8 b2adb157ff feat(desktop): NIP-88 polls (render, vote, create) + "Polls" search facet
Adds full NIP-88 poll support to Amethyst Desktop and a search content-type
filter for polls.

Polls (DesktopPollCard):
- Render kind-1068 polls in feed + thread (and reposted/boosted polls) as an
  interactive card via NoteCard's bottomContent slot.
- Vote (single-choice radio / multi-choice checkbox), re-vote ("Change vote")
  seeded with the prior selection; hide-until-voted with a "View results" opt-in.
- Tallies reuse commons PollResponsesCache; responses are fetched from the
  poll's OWN declared relays (NIP-88 relay tags) unioned with connected relays,
  so the full tally loads regardless of the viewer's relay set. Votes are
  likewise published to the poll's relays (not just broadcastToAll).
- Result row marks the viewer's own choice (border + check), tap a row to see
  its voters, footer shows distinct-voter count + deadline/ended state, and the
  voter gallery draws the viewer front-most with a ring.
- Create polls from the composer (options, single/multi, optional deadline);
  the dialog content scrolls with a pinned Cancel/Publish row; a poll requires
  a question and >=2 options.

Wiring:
- DesktopLocalCache.consume for kind 1068/1018 (response links into pollState).
- DesktopFeedFilters + FilterBuilders surface polls; feed/thread interaction
  subscriptions fetch kind-1018 responses.
- Thread + profile pass myPubKeyHex so the viewer's vote-state renders.

Search "Polls" facet:
- KindRegistry preset + alias for kind 1068 (auto-renders the filter chip and a
  NIP-50 kind filter); SearchResultsList renders poll results interactively and
  SearchScreen fetches their responses.

Also:
- Read-only accounts see results instead of dead vote controls.
- Cold-start: the response subscription re-evaluates as relays connect.
- Pull the upstream fix for the pre-existing RelayLatencyTracker.sweep
  ConcurrentModificationException (synchronized(pending)) so relay-health
  reclassify no longer crashes the UI during search.

Ripple/shaping: clickable elements clip to their shape for bounded ripple.

Tests: commons PollResponsesCache (dedup/tally/WoT sort) + DesktopLocalCache
response-linking.

Deferred (noted in review): wall-clock re-check of a poll expiring mid-view;
mention-dropdown now inside the composer scroll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:09:25 +03:00

9.1 KiB

Desktop Polls (NIP-88) — Manual Testing Sheet

Feature: render + vote + create polls on Amethyst Desktop Branch: worktree-feat+desktop-polls (worktree .claude/worktrees/feat+desktop-polls) Plan: docs/plans/2026-07-16-feat-desktop-polls-nip88-plan.md Status when written: code-complete; compile + unit tests + spotless GREEN; manual run not yet done.

Automated gates already passing

  • ./gradlew :commons:compileKotlinJvm :desktopApp:compileKotlin — clean
  • ./gradlew :commons:jvmTest --tests "*nip88Polls*" — 5 pass (dedup, tally %, WoT sort, hasVoted, idempotency)
  • ./gradlew :desktopApp:test --tests "*Poll*" — 2 pass (response links into tally; relay-echo dedup)
  • ./gradlew :commons:spotlessKotlinCheck :desktopApp:spotlessKotlinCheck — clean

How to run

cd .claude/worktrees/feat+desktop-polls
./gradlew :desktopApp:run

Log in (existing account, or NIP-46 bunker). Use a relay set that carries polls — good sources: relays where clients post NIP-88 polls, or create one yourself (Test C) and read it back. A second client (Amethyst Android, or amy) is useful to cross-verify events on the wire.


A. Rendering (feed + thread)

  • A1. A kind-1068 poll appears in the Home/Global feed as a poll card (description + options), NOT as plain text. (If polls never show: verify DesktopFeedFilters.isFeedNote includes PollEvent and FEED_KINDS has 1068.)
  • A2. Open the poll in a thread column → renders as a poll card there too.
  • A3. Single-choice poll shows radio-style option rows; multi-choice shows checkbox-style rows with a Submit button.
  • A4. Before voting, no percentages/tally are shown — only actionable options + a "View results" button.
  • A5. Tapping "View results" reveals the tally without casting a vote; a way back to voting exists (unless ended/author).
  • A6. A poll authored by you, seen in your own feed, shows results-only (you cannot vote).
  • A7. An ended poll (deadline in the past) shows results-only, no vote controls.
  • A8. Media/description of the poll render via the normal note card (links, images in the description behave as usual).

B. Voting

  • B1. Cast a single-choice vote → card immediately flips to results (optimistic), your option marked as your vote.
  • B2. Results show a % bar per option, a winning highlight, and voter avatars (up to ~4) + "+N".
  • B3. Voter avatars are ordered with people you follow first (WoT). Verify by having a followed account vote — their avatar should sort ahead of strangers.
  • B4. The bar does not do a distracting 0→N sweep when opening an already-tallied poll (first-frame animation guard).
  • B5. Multi-choice: select 2 options → Submit → both recorded; results reflect both.
  • B6. Multi-choice empty submit is rejected — with nothing selected, Submit does nothing / is disabled (no empty response event sent).
  • B7. Change vote: after voting, use "Change vote" → re-open options → pick a different option → tally updates so the new choice wins for you (newest response wins).
  • B8. Cross-check on a second client (Android/amy): your vote is a kind-1018 event referencing the poll via a lowercase e tag.
  • B9. Scroll-away during send (stress the scope fix): cast a vote and immediately scroll the poll out of view. Re-find it / check a second client — the vote should have broadcast to relays, not just shown locally. (This validates voteOnPoll runs on the long-lived appScope, not the card scope.)

C. Creating a poll

  • C1. Open the composer; toggle Poll on → poll option editor appears; image attachment is disabled while Poll is on.
  • C2. Add/remove options; minimum 2 non-blank options enforced before send is allowed.
  • C3. Toggle Single vs Multiple choice.
  • C4. Set a duration (Never / 1d / 3d / 7d). "Never" = open-ended (no deadline).
  • C5. Send → a kind-1068 PollEvent is published (verify on a second client): correct options, polltype, and endsAt (absent for "Never").
  • C6. The poll you created appears in your feed and is votable from another account/client; its tally updates as votes arrive.

D. Edge cases

  • D1. Poll with an unusually long option label wraps/renders without breaking layout.
  • D2. A poll received with 0 options (malformed) does not crash the feed (renders degraded / skipped).
  • D3. Receiving many responses from multiple relays converges to a stable, non-inflated tally (no double counting of the same response).
  • D4. Late votes arriving after a poll's deadline: they may still count in the tally, but the card stays results-only (no re-vote UI).

E. Regression (nothing else broke)

  • E1. Normal text notes, reposts, and reactions still render + behave in the feed.
  • E2. Composing a normal note (Poll toggle OFF) works exactly as before, including image attachment.
  • E3. Thread view still loads reactions/zaps/reposts for non-poll notes.


F. Search "Polls" content-type filter (added 2026-07-20)

Feature: filter search to only polls + interact with them. Plan: docs/plans/2026-07-20-feat-desktop-search-polls-facet-plan.md.

  • F1. Open the Search column → advanced filter panel shows a "Polls" chip alongside Notes/Articles/Media/Channels/Communities/Wiki.
  • F2. Enter a query, select Polls → results contain only poll notes (kind 1068); other content types are excluded.
  • F3. Results appear under a dedicated "Polls" section (poll icon) and render as interactive DesktopPollCard — options visible, not plain text.
  • F4. Vote from search (dedicated Search screen): cast a vote on a poll in results → flips to results/optimistic tally, and a kind-1018 event is published (cross-check on a 2nd client).
  • F5. Deselect the Polls chip → results return to mixed content; other facets still work.
  • F6. Section collapse/expand + "Show all N more" work like the other search sections.
  • F7. Feed header quick-search (the search box in the feed header): polls render as cards and are now votable (account threaded 2026-07-20).

G. Cross-context consistency fixes (2026-07-20)

Fixes for reported bugs: "can't always tap depending on how it's opened" + "only see my own answer, no other tallies".

  • G1. Thread view: open a poll into a thread → you can vote, and after voting the card correctly shows your choice as selected (previously your vote-state didn't register — missing myPubKeyHex).
  • G2. Thread tallies: a poll opened in a thread shows other people's votes, not just yours.
  • G3. Profile tabs (Notes/Replies): polls on a user's profile are votable and reflect your vote correctly.
  • G4. Dedicated Search tallies: filter to Polls → results now show existing tallies from others (search fetches kind-1018 responses via requestInteractions), not just your own vote.
  • G5. Feed header quick-search: polls there are now votable (account threaded).
  • G6. Consistency: the SAME poll shows consistent vote-state + tallies whether opened in feed, thread, profile, or search.

Remaining known gaps (expected):

  • Notifications tab renders polls as the compact notification card (not interactive) — out of scope.
  • Poll posted as a thread reply (not root) renders via the thread's custom reply card (not interactive) — edge case, deferred.
  • Feed-header quick-search fetches tallies only after the poll is also seen in a context that requests interactions; the dedicated Search column always fetches them.

Known caveats (expected, not bugs)

  • Same-second re-vote: if you change your vote within the same 1-second as the first, the tally may not flip until a later-second vote (tie-break on createdAt uses strict > with no id fallback). Wait ~1s between re-votes to see B7 flip reliably.
  • Option labels are plain text (v1): links/custom-emoji inside an option label are shown literally, not hyperlinked/rendered (no desktop rich-text path for option labels yet).
  • Deadline = preset chips (v1): Never/1d/3d/7d instead of a full date/time picker.
  • Not wired this PR (deferred): poll rendering in profile/bookmarks/search/notifications tabs (still show as plain notes there); poll-draft round-trip; zap-weighted polls.

If something fails — where to look

Symptom Check
Polls never appear in feed feeds/DesktopFeedFilters.kt isFeedNote (PollEvent), subscriptions/FilterBuilders.kt FEED_KINDS=…,1068
Poll shows but tally always empty kind-1018 sub: ui/FeedScreen.kt fetch-interactions filter + DesktopRelaySubscriptionsCoordinator.requestInteractions (e tag)
Vote shows locally but never reaches relays DesktopPollCard.castVote must launch on localCache.appScope; voteOnPoll in ui/NoteActions.kt
Double-counted votes DesktopLocalCache.consumePollResponse new-event gate (line ~385)
Created poll malformed ComposeNoteDialog.publishPoll → PollEvent.build options/type/endsAt