Design proposal · Amethyst · NIP‑88 polls · 2026‑08‑03

Poll results, with the counts and the people in them

A poll in Amethyst currently ends its life as a percentage bar and four avatars in the feed. This proposes a dedicated results screen that answers the two questions people actually ask — how many votes each answer got, and who voted for what — shared between Android and Desktop.

Event kinds
1068 · 1018
Core module
commons
Front ends
Android · Desktop
New event kinds
0
Bugs fixed en route
4
01 — The gap

What a finished poll looks like today

The inline card is deliberately compact, and that compactness is the whole problem: no vote counts anywhere, at most four voter avatars, and no route to the fifth. Desktop already grew a voter popup to work around it, which is the demand showing up as a divergence.

Today — inline feed card
Which relay should Amethyst suggest first to brand-new users?
nos.lol
46%
relay.damus.io
35%
purplepag.es
19%

The +171 chip is a dead end — it counts people you cannot reach. Percentages appear without their counts, and on a multiple-choice poll the denominator is wrong (PollResponsesCache.kt:52).

Proposed — the same card, one addition
Which relay should Amethyst suggest first to brand-new users?
nos.lol
175
46%
relay.damus.io
133
35%
purplepag.es
72
19%
380 votes ›

One change to the card itself: the vote count beside each percentage. The count under the options and the avatar stack both become the entry point to the full results screen — so the +171 stops being a dead end.

02 — The screen

Poll results, Android

Summary above detail: totals, then per-option breakdown, then every voter with the option they chose on the same row. Try the option chips — they filter the list below.

1 2 3 4 5 6 7
←
Poll results
↻
NF
Nadia Ferreira
npub1v0ry…8qk2 · 2d ago
Which relay should Amethyst suggest first to brand-new users?
Open · closes in 4h Single choice
380 voters · 380 selections
nos.lol
175 of 380 voters
46%
✓
relay.damus.io
133 of 380 voters
35%
purplepag.es
72 of 380 voters
19%
You voted nos.lol
VP
you
me✓nostrplebs.com
nos.lol
2d ago
BS
Bruno Sá
bruno✓nostrplebs.com
relay.damus.io
2d ago
MK
mira.k
mira✓nostrcheck.me
nos.lol
2d ago
AR
Ana Reis
ana✓nostrcheck.me
nos.lol
1d ago
RW
relaywrangler
wrangler✓relay.tools
purplepag.es
1d ago
KM
Katya M.
npub1q8f…j3xw
relay.damus.io
22h ago
TS
tomás
tomas✓nostr.pt
nos.lol
14h ago
HM
hodlmatrix
✓21m.io
purplepag.es
9h ago
⚠380 of ~412 responses loaded from 6 relays
3 responses ignored (unknown option) · 2 voters hidden by your mute list
1
Refresh is for relays, not for the tally

The tally is already a live StateFlow — new votes animate in without asking. The refresh action re-runs the backfill against the poll's own relays, which is the only part that can be stale.

2
Both totals, stated plainly

Voters is distinct pubkeys; selections is how many boxes were ticked. They are identical on a single-choice poll and diverge on multiple choice — which is exactly where today's percentages go wrong (§03).

3
Counts beside percentages, winner marked

Each row gains N of M voters. The winner keeps the existing green treatment from RenderClosedItem so the screen reads as the card it came from, and the bar keeps the same 800 ms tween.

4
Your own vote, and a way out of it

Desktop already allows re-voting; Android does not. This strip makes the behaviour the same on both, and puts your vote where you don't have to hunt the list for yourself.

5
Filter the list, never the summary

Option chips scope the voter list only — the bars above stay put, so the totals can't appear to change while you browse. An audience filter, a sort control and voter search are deliberately held back; the list is ordered you → follows → rest, and it's not yet clear which of the three people would actually reach for.

6
This is UserLine, unmodified

Not a new row, and not an edited one: the results screen calls the app's existing UserLine and passes the vote as its trailingContent, a parameter it already has. So the second line is the NIP-05 identifier the way the app draws it — local part, verified mark, domain, no @ — with a root identifier showing the domain alone (row eight) and the npub appearing only where there is no NIP-05 to show (row six).

7
Say how complete the tally is

A NIP-45 count against the poll's relays gives the "~412" — so a partial tally can admit it instead of presenting itself as final. The second line accounts for every response that was dropped and why.

03 — Open question, needs your call

What a multiple-choice percentage should mean

Same poll, same votes, two readings. 240 people voted and ticked 456 boxes between them. This is the one decision that blocks the tally work, because it changes the numbers on the existing feed card too.

Share of voters

proposed
Groups
168 of 240 voters
70%
Polls
132 of 240 voters
55%
Search
96 of 240 voters
40%
Wallet
60 of 240 voters
25%
bars sum to 190% — by design
"7 in 10 people want Groups." Matches how a poll bar is read. Costs you a total that doesn't add to 100, which the per-row caption explains.

Share of selections

what ships today
Groups
168 of 456 selections
37%
Polls
132 of 456 selections
29%
Search
96 of 456 selections
21%
Wallet
60 of 456 selections
13%
bars sum to 100%
"Groups took 37% of the vote." Tidier, and a voter who ticks four boxes counts four times. Desktop already contradicts this in its own footer, which uses distinct voters.
04 — States

The three the screen has to get right

Every one of these is a moment where a number could look like a bug unless the screen says what happened.

Loading · partial tally
←
Poll results
112 voters so far
nos.lol
58 of 112 voters
52%
relay.damus.io
34 of 112 voters
30%
⚠Loading page 2 of 5 from 6 relays…
Closed poll
←
Poll results
Ended 2d ago Single choice
412 voters · final
nos.lol
189 of 412 voters
46%
✓
relay.damus.io
142 of 412 voters
34%
No new votes accepted. 7 votes arrived after the deadline and are excluded.
No votes yet
←
Poll results
Open · closes in 3d
nos.lol
0 voters
0%
relay.damus.io
0 voters
0%
No votes yet Be the first — your vote will show up here.
05 — Desktop

The same screen as a deck column

Desktop navigates by DeckColumnType, not routes, so results open as a column beside the feed. The existing voter popup stays as the quick peek and grows a See all voters footer that opens this column.

☖
✉
◎
☆
Polls Open
Which relay should Amethyst suggest first?
Nadia Ferreira · 380 voters
Should the composer default to plain text?
Bruno Sá · 96 voters
Pick the next language for the UI
mira.k · 1,204 voters
Poll results 380 voters · 6 relays
Which relay should Amethyst suggest first to brand-new users?
Open · closes in 4h Single choice
nos.lol
175 of 380 voters
46%
✓
relay.damus.io
133 of 380 voters
35%
purplepag.es
72 of 380 voters
19%
All options nos.lol · 175 relay.damus.io · 133
VP
you
me✓nostrplebs.com
nos.lol
2d ago
BS
Bruno Sá
bruno✓nostrplebs.com
relay.damus.io
2d ago
MK
mira.k
mira✓nostrcheck.me
nos.lol
2d ago
Add a column
06 — Prerequisites

Four tally bugs, and one missing subscription

The results page would inherit all of these, so they land first. The four tally bugs share one root cause — ResponseTally never sees the PollEvent, so it cannot know the poll's type, its valid option codes, or its window. The fifth is separate: Android asks the wrong relays for votes. Both fixes improve the feed card on their own.

What breaks Impact Consequence Where
Multiple-choice denominator wrong number A voter who picks three options counts three times. Desktop's footer and its own bars already disagree about this. PollResponsesCache.kt:52
Every response tag counted spec NIP-88 keeps only the first tag on a single-choice poll. One malformed vote lands in every bucket. PollResponsesCache.kt:129
Votes after the deadline spec Only createdAt is compared, so a late vote beats a valid earlier one and closed polls keep moving. Note.kt:1685
Unknown option codes wrong number A code that isn't in the poll creates a phantom bucket that still drags every real percentage down. PollResponsesCache.kt:129
Poll relays never queried missing data Android reads votes from the author's inbox relays, not the relays the poll publishes votes to — so its tallies are short. Desktop patched this per-card. FilterRepliesAndReactionsToNotes.kt:89
07 — Sequence

What lands, in what order

The first two phases are bug fixes wearing a feature's clothes — worth shipping even if the screen never gets built.

P0
Poll-aware tallyNew PollTallyPolicy in commons + tests. Fixes all four numbers above.
Ships alone — corrects the feed card on both platforms
P1
Shared response subscriptionDesktop's per-card fix lifted into a shared assembler, plus fetchAllPages backfill.
Ships alone — Android tallies stop being short
P2
The screenViewModel + shared composables + Android route + entry points from the card. No existing composable is modified.
The feature
P3
Desktop deck columnSame ViewModel, same option bars, desktop's own user row.
Parity
P4
Audience filter, sort, voter searchHeld back on purpose — added once it's clear which of the three people reach for.
Only if wanted
P5
Zap pollsKind 6969 shares the shell but not the tally — it is weighted by sats, not by people.
Breadth