mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
docs(commons): reuse the app's user row for poll voters
Three revisions to the poll results proposal and its mockup. Voter rows are no longer a bespoke row. A voter is a user, and the app already draws users one way: SlimListItem with UserPicture / UsernameDisplay / AboutDisplay, which is what UserCompose is. The results row wants the first three verbatim and differs only in the trailing slot, where the vote goes instead of the follow buttons — so the proposal adds a trailingContent parameter to UserCompose with a default that leaves every existing call site unchanged, rather than forking the row. The second line is the profile's about text, and the bespoke follows/you chips are gone; ordering already carries that. Drops the privacy call-out entirely. Moves the audience filter, sort control and voter search out of the first version into their own later phase; option chips remain the only filter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AQorYBSv9oMvKa8hF1uGBv
This commit is contained in:
@@ -190,59 +190,110 @@ more than 100 responses truncates silently.
|
||||
│ You voted: nos.lol [Change vote] │ only when signed in + voted
|
||||
├─────────────────────────────────────────────┤
|
||||
│ [All] [nos.lol] [damus] [purplepag.es] │ option filter chips
|
||||
│ [Everyone ▾] [Recent ▾] 🔍 search │ audience + sort + search
|
||||
├─────────────────────────────────────────────┤
|
||||
│ [avatar] you nos.lol 2d ago │ voter rows
|
||||
│ [avatar] Bob (follows) damus 2d ago │
|
||||
│ [avatar] Carol nos.lol 1d ago │
|
||||
│ ⬤ Ana Reis nos.lol 2d ago │ voter rows — SlimListItem,
|
||||
│ Building things on nostr. │ same anatomy as UserCompose
|
||||
│ ⬤ Bruno Sá damus 2d ago │ (§6.1): picture / name /
|
||||
│ Relay operator, Lisbon. │ about, vote in the trailing
|
||||
│ ⬤ npub1q8f…j3xw damus 22h ago│ slot
|
||||
│ … │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 380 of ~412 loaded from 6 relays · 3 ignored│ completeness footer (§4)
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Audience filter, sort and voter search are **deliberately not in the first
|
||||
version** — see §5.3.
|
||||
|
||||
### 5.2 Behaviour decisions
|
||||
|
||||
| Question | Decision | Why |
|
||||
|---|---|---|
|
||||
| Percent basis | single-choice: % of voters; multi-choice: % of voters, bars may exceed 100% in sum, each row captioned "N of M voters" | §3.1 — matches how people read a poll |
|
||||
| Voter list default | flat list of **all** voters, each row showing which option(s) they chose | answers "who voted for what" in one screen without drilling |
|
||||
| Voter row | `SlimListItem` with the same slots `UserCompose` fills — `UserPicture` / `UsernameDisplay` / `AboutDisplay` — and the vote in `trailingContent` (§6.1) | a voter is a user; it should look like every other user row in the app |
|
||||
| Option chips | filter the voter list to one option; the summary block never filters | keeps totals stable while browsing |
|
||||
| Audience filter | `Everyone` (default) / `Following` / `Following + WoT` | reuses `account.allFollows.flow`, already the tally's priority set |
|
||||
| Muted users | excluded by default via `account.isHidden`, with a footer line "2 hidden by your mute list" and a toggle | consistent with every other user list in the app |
|
||||
| Sort | `Relevance` (you → follows → pubkey — today's `filterTo` comparator), `Newest`, `Oldest` | relevance is already implemented; the other two are one comparator each |
|
||||
| Ordering of you | your own row always pinned first in Relevance, and your vote echoed in the "You voted" strip | already the `filterTo` behaviour |
|
||||
| Muted users | excluded by default via `account.isHidden`, with a footer line "2 hidden by your mute list" | consistent with every other user list in the app |
|
||||
| Ordering | you first, then follows, then pubkey — today's `filterTo` comparator, no sort control | the ordering already exists and already does the useful thing |
|
||||
| Live updates | the page is a live view of `pollState().responses`; new votes animate in | it's a StateFlow already; no refresh button needed except for the relay backfill |
|
||||
| Closed polls | status chip flips to "Ended · 2d ago", "Change vote" disappears, late votes excluded (§3.3) with an "N late votes excluded" footnote | spec compliance, and it explains a number that would otherwise look wrong |
|
||||
| Empty state | "No votes yet" + the option rows at 0 | |
|
||||
|
||||
### 5.3 Privacy call-out
|
||||
### 5.3 Deferred to a later pass
|
||||
|
||||
NIP-88 responses are **public, unencrypted events** — this page doesn't disclose
|
||||
anything new, but it makes a fact legible that many users have not internalised.
|
||||
Two small additions belong with this work:
|
||||
Three controls are specified here so the layout leaves room for them, but are
|
||||
**not** part of the first version:
|
||||
|
||||
- an ⓘ affordance on the results header: *"Poll votes are public. Anyone can see
|
||||
who voted for what."*
|
||||
- the same one-liner next to the vote controls in `RenderPollCard` **before** the
|
||||
first vote — the honest place to say it is where the choice is made, not after.
|
||||
- **audience filter** (`Everyone` / `Following` / `Following + WoT`),
|
||||
- **sort control** (`Newest` / `Oldest` on top of the default relevance order),
|
||||
- **voter search**.
|
||||
|
||||
There is no "anonymous vote" option to offer; saying so plainly is the whole fix.
|
||||
Each is cheap on its own — the follow set and the comparator both already exist —
|
||||
but none of them is needed to answer "how many votes did each answer get, and who
|
||||
voted for what". They land once the page is real and it's clear which of them
|
||||
people actually reach for. Until then the list is ordered you → follows → rest,
|
||||
and the option chips are the only filter.
|
||||
|
||||
## 6. Code structure
|
||||
|
||||
Following `commons/ARCHITECTURE.md` (state + ViewModels + shared Compose in
|
||||
`commons`, screens/nav platform-native):
|
||||
|
||||
### 6.1 The voter row is an existing user row
|
||||
|
||||
A voter is a user, and the app already has one way to draw a user in a list.
|
||||
`UserCompose` (`amethyst/…/ui/note/UserCompose.kt:52`) is `SlimListItem`
|
||||
(`ui/layouts/listItem/SlimListItemLayout.kt:168`) with four slots filled:
|
||||
|
||||
| Slot | Filled with | Renders |
|
||||
|---|---|---|
|
||||
| `leadingContent` | `UserPicture(user, Size55dp, …)` | avatar, with its own loading/blank handling |
|
||||
| `headlineContent` | `UsernameDisplay(user, accountViewModel)` | the account's petname → `bestName()` → `pubkeyDisplayHex()`, with custom-emoji support via `CreateTextWithEmoji` |
|
||||
| `supportingContent` | `AboutDisplay(user, accountViewModel)` | the profile's about text, one line, ellipsized, in `placeholderText` |
|
||||
| `trailingContent` | `UserActionOptions(…)` | follow/unfollow + list buttons |
|
||||
|
||||
The results row wants the first three **verbatim** and only differs in the
|
||||
fourth, where the vote goes instead of the follow buttons. So don't write a new
|
||||
row — add a slot to the existing one:
|
||||
|
||||
```kotlin
|
||||
@Composable
|
||||
fun UserCompose(
|
||||
baseUser: User,
|
||||
modifier: Modifier = Modifier,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
trailingContent: @Composable () -> Unit = { UserActionOptions(baseUser, accountViewModel, nav) },
|
||||
) { … }
|
||||
```
|
||||
|
||||
Every existing call site is unchanged by the default; the poll screen passes the
|
||||
option label + relative timestamp. This also means voter rows inherit, for free,
|
||||
the things a hand-rolled row would silently lose: petname overrides, emoji in
|
||||
display names, the npub fallback when metadata hasn't arrived, and the
|
||||
tap-to-profile navigation from `routeFor(user)`.
|
||||
|
||||
Two consequences worth stating:
|
||||
|
||||
- **The second line is the user's about text, not their npub.** That is what the
|
||||
rest of the app shows, and the npub already appears in the headline when there
|
||||
is no metadata to show instead.
|
||||
- **No bespoke "follows" or "you" chip.** `UserCompose` has never had one, and the
|
||||
ordering (you → follows → rest) already carries that information. If a follow
|
||||
marker is wanted later it should come from the existing follow-state renderer
|
||||
used elsewhere, not a new badge invented for this screen.
|
||||
|
||||
### 6.2 New and edited files
|
||||
|
||||
**New in `commons` (shared):**
|
||||
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `model/nip88Polls/PollTallyPolicy.kt` | §3.5 |
|
||||
| `model/nip88Polls/PollResponsesCache.kt` *(edit)* | poll-aware `ResponseTally`, `totalVoters()`, `totalSelections()`, `rejected`, `lateVotes` |
|
||||
| `viewmodels/nip88Polls/PollResultsViewModel.kt` | holds the poll `Note`; exposes `StateFlow<PollResultsState>` (header + option rows + filtered/sorted voter rows + completeness meta); owns filter/sort/search `MutableStateFlow`s; drives the backfill. Sibling in style to `viewmodels/LiveStreamTopZappersViewModel.kt`, with the usual nested `Factory` |
|
||||
| `viewmodels/nip88Polls/PollResultsViewModel.kt` | holds the poll `Note`; exposes `StateFlow<PollResultsState>` (header + option rows + voter rows + completeness meta); owns the option-chip selection; drives the backfill. Sibling in style to `viewmodels/LiveStreamTopZappersViewModel.kt`, with the usual nested `Factory` |
|
||||
| `relayClient/…/polls/PollResponsesFilterAssembler.kt` | §4 |
|
||||
| `ui/note/polls/PollResultsHeader.kt`, `PollOptionBreakdown.kt`, `PollVoterRow.kt` | slot-based shared composables — the user avatar/name cell is a `@Composable` slot supplied by each platform (`UserCompose` on Android, desktop's own row), per `compose-slot-api-pattern` |
|
||||
| `ui/note/polls/PollResultsHeader.kt`, `PollOptionBreakdown.kt` | slot-based shared composables for the header and the option bars, per `compose-slot-api-pattern`. **No shared voter-row composable** — each platform's own user row fills that job (§6.1) |
|
||||
|
||||
**New in `amethyst` (Android):**
|
||||
|
||||
@@ -259,10 +310,14 @@ Following `commons/ARCHITECTURE.md` (state + ViewModels + shared Compose in
|
||||
- `DeckColumnType.PollResults(noteId)` in `ui/deck/DeckColumnType.kt` (plus the
|
||||
`title()` and `typeKey()` branches — those `when`s are exhaustive) and a render
|
||||
branch in `DeckColumnContainer.kt`. Keep `VoterListPopup` as the quick peek and
|
||||
add a **"See all voters"** footer that opens the column.
|
||||
add a **"See all voters"** footer that opens the column. Its voter rows follow
|
||||
the same rule as §6.1 against desktop's own user-row composable rather than the
|
||||
bespoke avatar+name pair in `VoterListPopup`.
|
||||
|
||||
**Edited (entry points):**
|
||||
|
||||
- `UserCompose.kt` — add the `trailingContent` slot (§6.1). Default keeps every
|
||||
existing call site identical.
|
||||
- `Poll.kt` — make the totals line and the `UserGallery` `+N` chip clickable →
|
||||
`nav.nav(Route.PollResults(noteId))`; add a "N votes" count next to the
|
||||
percentage (currently absent).
|
||||
@@ -295,8 +350,8 @@ very different tally models.
|
||||
the denominator (§3.4); multi-choice `totalVoters` vs `totalSelections` (§3.1);
|
||||
policy arriving *after* responses recomputes the tally (§3.5); duplicate
|
||||
pubkey latest-wins (existing behaviour, lock it in).
|
||||
- New `PollResultsViewModelTest` with Turbine: filter/sort/search transitions,
|
||||
mute exclusion, live insertion of a new vote.
|
||||
- New `PollResultsViewModelTest` with Turbine: option-chip transitions, mute
|
||||
exclusion, live insertion of a new vote.
|
||||
- Manual: a >100-response poll (paging), a poll whose `relay` tags the viewer
|
||||
isn't connected to (completeness), a multi-choice poll (denominators).
|
||||
|
||||
@@ -306,9 +361,10 @@ very different tally models.
|
||||
|---|---|---|
|
||||
| 0 | Poll-aware tally (§3) — commons + tests | yes — fixes the inline card's numbers on both platforms |
|
||||
| 1 | Shared subscription + backfill (§4) | yes — Android tallies stop being short |
|
||||
| 2 | `PollResultsViewModel` + shared composables + Android screen + route + entry points | yes — the feature |
|
||||
| 3 | Filters, sort, search, completeness footer, privacy call-out (§5.2–5.3) | polish |
|
||||
| 4 | Desktop column; then zap polls (§7) | parity, then breadth |
|
||||
| 2 | `PollResultsViewModel` + shared composables + `UserCompose` trailing slot + Android screen + route + entry points | yes — the feature |
|
||||
| 3 | Desktop column | parity |
|
||||
| 4 | Audience filter, sort, voter search (§5.3) | once it's clear which are actually reached for |
|
||||
| 5 | Zap polls (§7) | breadth |
|
||||
|
||||
Phases 0 and 1 are worth landing even if the page itself is deferred: they are
|
||||
bug fixes wearing a feature's clothes.
|
||||
@@ -323,7 +379,9 @@ bug fixes wearing a feature's clothes.
|
||||
(`Poll.kt:298-336`, `hasViewedPollResults`). Proposal: the page respects the
|
||||
same gate — reaching it counts as opting in and calls `markPollResultsViewed`,
|
||||
exactly like today's "View results" link.
|
||||
3. **WoT tier in the audience filter** — worth it, or do `Everyone` / `Following`
|
||||
cover the need?
|
||||
3. **Trailing slot vs. a second entry point.** §6.1 adds a `trailingContent`
|
||||
parameter to `UserCompose` with a default that preserves every call site. The
|
||||
alternative is a separate `UserComposeWithTrailing` — more files, no behaviour
|
||||
difference. Flagging it because it edits a composable used across the app.
|
||||
4. **Export/share results** (copy as text, or a rendered image) — desirable, but
|
||||
listed as out of scope until the page exists.
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
--app-accent: #6200ee;
|
||||
--app-good: #339900;
|
||||
--app-warn: #c09b14;
|
||||
--app-following: #00897b;
|
||||
--app-placeholder: color-mix(in srgb, #1c1c1c 42%, transparent);
|
||||
--app-shadow: 0 18px 40px rgba(0, 0, 0, .16), 0 2px 6px rgba(0, 0, 0, .08);
|
||||
|
||||
--font-ui: ui-sans-serif, system-ui, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
@@ -78,7 +78,7 @@
|
||||
--app-accent: #bb86fc;
|
||||
--app-good: #99cc33;
|
||||
--app-warn: #e1c419;
|
||||
--app-following: #03dac5;
|
||||
--app-placeholder: color-mix(in srgb, #e6e6e6 42%, transparent);
|
||||
--app-shadow: 0 18px 40px rgba(0, 0, 0, .6), 0 0 0 1px #2b2b2b;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@
|
||||
--app-accent: #bb86fc;
|
||||
--app-good: #99cc33;
|
||||
--app-warn: #e1c419;
|
||||
--app-following: #03dac5;
|
||||
--app-placeholder: color-mix(in srgb, #e6e6e6 42%, transparent);
|
||||
--app-shadow: 0 18px 40px rgba(0, 0, 0, .6), 0 0 0 1px #2b2b2b;
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
--app-accent: #6200ee;
|
||||
--app-good: #339900;
|
||||
--app-warn: #c09b14;
|
||||
--app-following: #00897b;
|
||||
--app-placeholder: color-mix(in srgb, #1c1c1c 42%, transparent);
|
||||
--app-shadow: 0 18px 40px rgba(0, 0, 0, .16), 0 2px 6px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
@@ -323,7 +323,8 @@
|
||||
letter-spacing: .02em;
|
||||
flex: none;
|
||||
}
|
||||
.avatar-sm { width: 22px; height: 22px; font-size: 9px; }
|
||||
/* UserPicture at Size55dp, the size every user list in the app uses. */
|
||||
.avatar-user { width: 48px; height: 48px; font-size: 15px; }
|
||||
.avatar-xs {
|
||||
width: 21px; height: 21px; font-size: 8.5px;
|
||||
box-shadow: 0 0 0 2px var(--app-bg);
|
||||
@@ -514,74 +515,35 @@
|
||||
.fchip:focus-visible,
|
||||
.app-btn:focus-visible { outline: 2px solid var(--app-accent); outline-offset: 2px; }
|
||||
|
||||
.filter-row { display: flex; gap: 7px; align-items: center; }
|
||||
.select {
|
||||
font-size: 11.5px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--app-outline-2);
|
||||
color: var(--app-on-2);
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.search {
|
||||
flex: 1;
|
||||
font-size: 11.5px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--app-outline-2);
|
||||
color: var(--app-on-2);
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* voter list */
|
||||
/* Voter list. Row anatomy is UserCompose's, not a new one: SlimListItem with
|
||||
UserPicture / UsernameDisplay / AboutDisplay, and the vote in the trailing
|
||||
slot where the follow buttons normally sit. */
|
||||
.voters { display: flex; flex-direction: column; }
|
||||
.voter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
gap: 12px;
|
||||
padding: 11px 16px;
|
||||
border-bottom: 1px solid var(--app-outline-2);
|
||||
}
|
||||
.voter:last-child { border-bottom: 0; }
|
||||
.voter-id { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
||||
.voter-name {
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14.5px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.voter-handle { font-size: 11.5px; color: var(--app-on-2); }
|
||||
.tag-follows {
|
||||
font-size: 9.5px;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--app-following);
|
||||
border: 1px solid color-mix(in srgb, var(--app-following) 45%, transparent);
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
font-weight: 700;
|
||||
flex: none;
|
||||
}
|
||||
.tag-you {
|
||||
font-size: 9.5px;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--app-accent);
|
||||
border: 1px solid color-mix(in srgb, var(--app-accent) 45%, transparent);
|
||||
border-radius: 4px;
|
||||
padding: 0 4px;
|
||||
font-weight: 700;
|
||||
flex: none;
|
||||
/* AboutDisplay: profile about text, one line, ellipsized, placeholderText
|
||||
(= onSurface at 42%). */
|
||||
.voter-about {
|
||||
font-size: 12.5px;
|
||||
color: var(--app-placeholder);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.voter-vote {
|
||||
text-align: right;
|
||||
@@ -590,8 +552,8 @@
|
||||
gap: 1px;
|
||||
flex: none;
|
||||
}
|
||||
.voter-choice { font-size: 12px; font-weight: 600; }
|
||||
.voter-when { font-size: 11px; color: var(--app-on-2); font-variant-numeric: tabular-nums; }
|
||||
.voter-choice { font-size: 12.5px; font-weight: 600; }
|
||||
.voter-when { font-size: 11px; color: var(--app-placeholder); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.app-foot {
|
||||
padding: 11px 16px 14px;
|
||||
@@ -716,21 +678,7 @@
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.privacy-line {
|
||||
font-size: 11.5px;
|
||||
color: var(--app-on-2);
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: baseline;
|
||||
}
|
||||
.privacy-line b { color: var(--app-warn); font-weight: 700; }
|
||||
|
||||
.vote-opt {
|
||||
border: 1px solid var(--app-outline-2);
|
||||
border-radius: 10px;
|
||||
padding: 10px 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ── Decision A/B ───────────────────────────────────────── */
|
||||
.ab {
|
||||
@@ -1062,7 +1010,8 @@
|
||||
<div class="avatar avatar-xs a2">BS</div>
|
||||
<div class="avatar avatar-xs a5">LN</div>
|
||||
<div class="avatar avatar-xs a8">RW</div>
|
||||
<div class="opt-more">+130</div>
|
||||
<div class="avatar avatar-xs a6">CF</div>
|
||||
<div class="opt-more">+129</div>
|
||||
</div>
|
||||
<div class="opt-pct">35%</div>
|
||||
</div>
|
||||
@@ -1074,7 +1023,9 @@
|
||||
<div class="opt-gallery">
|
||||
<div class="avatar avatar-xs a6">HM</div>
|
||||
<div class="avatar avatar-xs a4">NF</div>
|
||||
<div class="opt-more">+70</div>
|
||||
<div class="avatar avatar-xs a2">PS</div>
|
||||
<div class="avatar avatar-xs a3">EK</div>
|
||||
<div class="opt-more">+68</div>
|
||||
</div>
|
||||
<div class="opt-pct">19%</div>
|
||||
</div>
|
||||
@@ -1089,22 +1040,64 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-tag card-tag-new">Proposed — the same card, two additions</div>
|
||||
<div class="card-tag card-tag-new">Proposed — the same card, one addition</div>
|
||||
<div class="miniframe">
|
||||
<div class="app-q" style="font-size:15px">Which relay should Amethyst suggest first to brand-new users?</div>
|
||||
<div class="vote-opt">nos.lol</div>
|
||||
<div class="vote-opt">relay.damus.io</div>
|
||||
<div class="vote-opt">purplepag.es</div>
|
||||
<div class="privacy-line"><b>ⓘ</b><span>Poll votes are public. Anyone can see who voted for what.</span></div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-size:12.5px;color:var(--app-accent);font-weight:600">View results</div>
|
||||
<div style="font-size:12px;color:var(--app-on-2)">380 votes</div>
|
||||
<div class="opts">
|
||||
<div class="opt opt-win" style="--pct:46%">
|
||||
<div class="opt-fill"></div>
|
||||
<div class="opt-row">
|
||||
<div class="opt-label"><div class="opt-name">nos.lol</div></div>
|
||||
<div class="opt-gallery">
|
||||
<div class="avatar avatar-xs a1">MK</div>
|
||||
<div class="avatar avatar-xs a3">AR</div>
|
||||
<div class="avatar avatar-xs a7">TS</div>
|
||||
<div class="avatar avatar-xs a4">JD</div>
|
||||
<div class="opt-more">+171</div>
|
||||
</div>
|
||||
<div class="opt-sub" style="min-width:26px;text-align:right">175</div>
|
||||
<div class="opt-pct">46%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opt" style="--pct:35%">
|
||||
<div class="opt-fill"></div>
|
||||
<div class="opt-row">
|
||||
<div class="opt-label"><div class="opt-name">relay.damus.io</div></div>
|
||||
<div class="opt-gallery">
|
||||
<div class="avatar avatar-xs a2">BS</div>
|
||||
<div class="avatar avatar-xs a5">LN</div>
|
||||
<div class="avatar avatar-xs a8">RW</div>
|
||||
<div class="avatar avatar-xs a6">CF</div>
|
||||
<div class="opt-more">+129</div>
|
||||
</div>
|
||||
<div class="opt-sub" style="min-width:26px;text-align:right">133</div>
|
||||
<div class="opt-pct">35%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opt" style="--pct:19%">
|
||||
<div class="opt-fill"></div>
|
||||
<div class="opt-row">
|
||||
<div class="opt-label"><div class="opt-name">purplepag.es</div></div>
|
||||
<div class="opt-gallery">
|
||||
<div class="avatar avatar-xs a6">HM</div>
|
||||
<div class="avatar avatar-xs a4">NF</div>
|
||||
<div class="avatar avatar-xs a2">PS</div>
|
||||
<div class="avatar avatar-xs a3">EK</div>
|
||||
<div class="opt-more">+68</div>
|
||||
</div>
|
||||
<div class="opt-sub" style="min-width:26px;text-align:right">72</div>
|
||||
<div class="opt-pct">19%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;justify-content:flex-end">
|
||||
<div style="font-size:12.5px;color:var(--app-accent);font-weight:600">380 votes ›</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-note">
|
||||
Two changes to the card itself: a <b>vote count</b> beside the percentage, and the privacy
|
||||
line placed <b>where the choice is made</b> rather than after it. The avatar stack and the
|
||||
count both become the entry point to the full results screen.
|
||||
One change to the card itself: the <b>vote count</b> beside each percentage. The count
|
||||
under the options and the avatar stack both become the entry point to the full results
|
||||
screen — so the <b>+171</b> stops being a dead end.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1165,11 +1158,12 @@
|
||||
<div class="opt-sub">175 of 380 voters</div>
|
||||
</div>
|
||||
<div class="opt-gallery">
|
||||
<div class="avatar avatar-xs a1">MK</div>
|
||||
<div class="avatar avatar-xs a3">AR</div>
|
||||
<div class="avatar avatar-xs a7">TS</div>
|
||||
<div class="opt-more">+172</div>
|
||||
</div>
|
||||
<div class="avatar avatar-xs a1">MK</div>
|
||||
<div class="avatar avatar-xs a3">AR</div>
|
||||
<div class="avatar avatar-xs a7">TS</div>
|
||||
<div class="avatar avatar-xs a4">JD</div>
|
||||
<div class="opt-more">+171</div>
|
||||
</div>
|
||||
<div class="opt-pct">46%</div>
|
||||
<div class="opt-check">✓</div>
|
||||
</div>
|
||||
@@ -1182,10 +1176,12 @@
|
||||
<div class="opt-sub">133 of 380 voters</div>
|
||||
</div>
|
||||
<div class="opt-gallery">
|
||||
<div class="avatar avatar-xs a2">BS</div>
|
||||
<div class="avatar avatar-xs a5">LN</div>
|
||||
<div class="opt-more">+131</div>
|
||||
</div>
|
||||
<div class="avatar avatar-xs a2">BS</div>
|
||||
<div class="avatar avatar-xs a5">LN</div>
|
||||
<div class="avatar avatar-xs a8">RW</div>
|
||||
<div class="avatar avatar-xs a6">CF</div>
|
||||
<div class="opt-more">+129</div>
|
||||
</div>
|
||||
<div class="opt-pct">35%</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1197,10 +1193,12 @@
|
||||
<div class="opt-sub">72 of 380 voters</div>
|
||||
</div>
|
||||
<div class="opt-gallery">
|
||||
<div class="avatar avatar-xs a6">HM</div>
|
||||
<div class="avatar avatar-xs a8">RW</div>
|
||||
<div class="opt-more">+70</div>
|
||||
</div>
|
||||
<div class="avatar avatar-xs a6">HM</div>
|
||||
<div class="avatar avatar-xs a4">NF</div>
|
||||
<div class="avatar avatar-xs a2">PS</div>
|
||||
<div class="avatar avatar-xs a3">EK</div>
|
||||
<div class="opt-more">+68</div>
|
||||
</div>
|
||||
<div class="opt-pct">19%</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1219,75 +1217,69 @@
|
||||
<button class="fchip" type="button" aria-pressed="false" data-opt="damus">relay.damus.io · 133</button>
|
||||
<button class="fchip" type="button" aria-pressed="false" data-opt="purple">purplepag.es · 72</button>
|
||||
</div>
|
||||
<div class="filter-row">
|
||||
<span class="select">Everyone ▾</span>
|
||||
<span class="select">Relevance ▾</span>
|
||||
<span class="search">⚲ Search voters</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="voters" id="voterList" data-pin="pin6">
|
||||
<div class="voter" data-opt="nos">
|
||||
<div class="avatar avatar-sm a8">VP</div>
|
||||
<div class="avatar avatar-user a8">VP</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">you <span class="tag-you">you</span></div>
|
||||
<div class="voter-handle">npub1gcx…wq7d</div>
|
||||
<div class="voter-name">you</div>
|
||||
<div class="voter-about">Android + KMP. Nostr all day.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">nos.lol</div><div class="voter-when">2d ago</div></div>
|
||||
</div>
|
||||
<div class="voter" data-opt="damus">
|
||||
<div class="avatar avatar-sm a2">BS</div>
|
||||
<div class="avatar avatar-user a2">BS</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">Bruno Sá <span class="tag-follows">follows</span></div>
|
||||
<div class="voter-handle">bruno@nostrplebs.com</div>
|
||||
<div class="voter-name">Bruno Sá</div>
|
||||
<div class="voter-about">Relay operator in Lisbon. Runs a small paid relay.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">relay.damus.io</div><div class="voter-when">2d ago</div></div>
|
||||
</div>
|
||||
<div class="voter" data-opt="nos">
|
||||
<div class="avatar avatar-sm a1">MK</div>
|
||||
<div class="avatar avatar-user a1">MK</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">mira.k <span class="tag-follows">follows</span></div>
|
||||
<div class="voter-handle">npub18ht…2m4v</div>
|
||||
<div class="voter-name">mira.k</div>
|
||||
<div class="voter-about">Designer. Making nostr less frightening.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">nos.lol</div><div class="voter-when">2d ago</div></div>
|
||||
</div>
|
||||
<div class="voter" data-opt="nos">
|
||||
<div class="avatar avatar-sm a3">AR</div>
|
||||
<div class="avatar avatar-user a3">AR</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">Ana Reis <span class="tag-follows">follows</span></div>
|
||||
<div class="voter-handle">npub1t4qz…p09h</div>
|
||||
<div class="voter-name">Ana Reis</div>
|
||||
<div class="voter-about">Photographer, mostly film.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">nos.lol</div><div class="voter-when">1d ago</div></div>
|
||||
</div>
|
||||
<div class="voter" data-opt="purple">
|
||||
<div class="avatar avatar-sm a8">RW</div>
|
||||
<div class="avatar avatar-user a8">RW</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">relaywrangler</div>
|
||||
<div class="voter-handle">npub1kk9m…4zzt</div>
|
||||
<div class="voter-about">Self-hosting everything since 2011.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">purplepag.es</div><div class="voter-when">1d ago</div></div>
|
||||
</div>
|
||||
<div class="voter" data-opt="damus">
|
||||
<div class="avatar avatar-sm a5">LN</div>
|
||||
<div class="avatar avatar-user a5"></div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">npub1q8f…j3xw</div>
|
||||
<div class="voter-handle">no profile found</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">relay.damus.io</div><div class="voter-when">22h ago</div></div>
|
||||
</div>
|
||||
<div class="voter" data-opt="nos">
|
||||
<div class="avatar avatar-sm a7">TS</div>
|
||||
<div class="avatar avatar-user a7">TS</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">tomás</div>
|
||||
<div class="voter-handle">npub1w2ns…6vhc</div>
|
||||
<div class="voter-about">café, código, bicicleta</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">nos.lol</div><div class="voter-when">14h ago</div></div>
|
||||
</div>
|
||||
<div class="voter" data-opt="purple">
|
||||
<div class="avatar avatar-sm a6">HM</div>
|
||||
<div class="avatar avatar-user a6">HM</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">hodlmatrix</div>
|
||||
<div class="voter-handle">npub1xr7d…5ktp</div>
|
||||
<div class="voter-about">21M or bust.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">purplepag.es</div><div class="voter-when">9h ago</div></div>
|
||||
</div>
|
||||
@@ -1351,20 +1343,23 @@
|
||||
<div class="note-t">Filter the list, never the summary</div>
|
||||
<p class="note-d">
|
||||
Option chips scope the voter list only — the bars above stay put, so the totals can't
|
||||
appear to change while you browse. Audience is <b>Everyone / Following / Following + WoT</b>,
|
||||
reusing the follow set the tally already sorts by.
|
||||
appear to change while you browse. An audience filter, a sort control and voter search
|
||||
are <b>deliberately held back</b>; the list is ordered you → follows → rest, and it's
|
||||
not yet clear which of the three people would actually reach for.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note">
|
||||
<div class="note-n">6</div>
|
||||
<div class="note-body">
|
||||
<div class="note-t">One row per person, with their choice on it</div>
|
||||
<div class="note-t">A voter row is just a user row</div>
|
||||
<p class="note-d">
|
||||
The default is a flat list of everyone, so "who voted for what" is answered without
|
||||
drilling into an option. You are pinned first, follows next — that ordering already
|
||||
exists in <code class="ref">filterTo</code>. Multiple-choice rows list every option
|
||||
that person picked.
|
||||
Not a new row type — <code class="ref">SlimListItem</code> with exactly the slots
|
||||
<code class="ref">UserCompose</code> fills: <code>UserPicture</code>,
|
||||
<code>UsernameDisplay</code>, <code>AboutDisplay</code>. Only the trailing slot differs,
|
||||
carrying the vote where the follow buttons normally sit. So the second line is the
|
||||
person's <b>about text</b>, petnames and display-name emoji work, and the npub fallback
|
||||
(row six) appears on its own when no profile has arrived — all for free.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1680,23 +1675,26 @@
|
||||
</div>
|
||||
<div class="voters" style="border:1px solid var(--app-outline-2);border-radius:10px;overflow:hidden">
|
||||
<div class="voter">
|
||||
<div class="avatar avatar-sm a8">VP</div>
|
||||
<div class="avatar avatar-user a8">VP</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">you <span class="tag-you">you</span></div>
|
||||
<div class="voter-name">you</div>
|
||||
<div class="voter-about">Android + KMP. Nostr all day.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">nos.lol</div><div class="voter-when">2d ago</div></div>
|
||||
</div>
|
||||
<div class="voter">
|
||||
<div class="avatar avatar-sm a2">BS</div>
|
||||
<div class="avatar avatar-user a2">BS</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">Bruno Sá <span class="tag-follows">follows</span></div>
|
||||
<div class="voter-name">Bruno Sá</div>
|
||||
<div class="voter-about">Relay operator in Lisbon.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">relay.damus.io</div><div class="voter-when">2d ago</div></div>
|
||||
</div>
|
||||
<div class="voter">
|
||||
<div class="avatar avatar-sm a1">MK</div>
|
||||
<div class="avatar avatar-user a1">MK</div>
|
||||
<div class="voter-id">
|
||||
<div class="voter-name">mira.k <span class="tag-follows">follows</span></div>
|
||||
<div class="voter-name">mira.k</div>
|
||||
<div class="voter-about">Designer. Making nostr less frightening.</div>
|
||||
</div>
|
||||
<div class="voter-vote"><div class="voter-choice">nos.lol</div><div class="voter-when">2d ago</div></div>
|
||||
</div>
|
||||
@@ -1789,18 +1787,23 @@
|
||||
</div>
|
||||
<div class="phase">
|
||||
<div class="phase-n">P2</div>
|
||||
<div class="phase-t">The screen<span>ViewModel + shared composables + Android route + entry points from the card.</span></div>
|
||||
<div class="phase-t">The screen<span>ViewModel + shared composables + the <code class="ref">UserCompose</code> trailing slot + Android route + entry points from the card.</span></div>
|
||||
<div class="phase-v ships">The feature</div>
|
||||
</div>
|
||||
<div class="phase">
|
||||
<div class="phase-n">P3</div>
|
||||
<div class="phase-t">Filters, sort, search, completeness footer, privacy line<span>Everything in §02 notes 5 and 7.</span></div>
|
||||
<div class="phase-v">Polish</div>
|
||||
<div class="phase-t">Desktop deck column<span>Same ViewModel, same option bars, desktop's own user row.</span></div>
|
||||
<div class="phase-v">Parity</div>
|
||||
</div>
|
||||
<div class="phase">
|
||||
<div class="phase-n">P4</div>
|
||||
<div class="phase-t">Desktop column, then zap polls<span>Kind 6969 shares the shell but not the tally — it is weighted by sats, not by people.</span></div>
|
||||
<div class="phase-v">Parity, then breadth</div>
|
||||
<div class="phase-t">Audience filter, sort, voter search<span>Held back on purpose — added once it's clear which of the three people reach for.</span></div>
|
||||
<div class="phase-v">Only if wanted</div>
|
||||
</div>
|
||||
<div class="phase">
|
||||
<div class="phase-n">P5</div>
|
||||
<div class="phase-t">Zap polls<span>Kind 6969 shares the shell but not the tally — it is weighted by sats, not by people.</span></div>
|
||||
<div class="phase-v">Breadth</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user