Skip to content

feat(lists): surface list contributors on the list detail header - #68

Open
Adron wants to merge 1 commit into
mainfrom
feat/list-contributors
Open

feat(lists): surface list contributors on the list detail header#68
Adron wants to merge 1 commit into
mainfrom
feat/list-contributors

Conversation

@Adron

@Adron Adron commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

iOS showed list watchers — people who have been granted access — but never contributors, the people who have actually written rows. On a shared list that is the more useful signal, and the web has surfaced it on the list header since ContributorAvatarStack landed. This adds the iOS equivalent: a tappable avatar stack in the ListDetailView header that opens a ranked sheet.

Closes #58

What's included

  • API (Services/APIClient+ListContributors.swift, new): listContributors(listId:) hitting GET /api/lists/{id}/contributors. Lives in a per-feature extension file rather than APIClient.swift, per CLAUDE.md. The route returns the full ranked set with no server paging, so the client neither pages nor re-sorts — re-sorting would put iOS out of step with the web's order.
  • Model (Models/ListContributor.swift, new): ListContributor (user + addedCount / editedCount / score) and a ListContributorsResult wrapper carrying totalContributors. Keys arrive camelCase off the backend's serialize(), which passes through the shared convertFromSnakeCase decoder unchanged.
  • Views (Views/ListContributorsView.swift, new): ContributorAvatarStack — cascading avatars with a "+N" overflow bubble — and ListContributorsSheet, whose rows mirror the WatchersListView row style (36pt circular avatar, display name, @username · added N · edited M). Both have #Previews.
  • Wiring (Views/ListsView.swift): the stack mounts as a tappable header section in ListDetailView, fetched concurrently with the existing schema/items/connections loads rather than adding a serial round-trip to every list open.
  • Project: all four new .swift files registered in project.pbxproj via the xcodeproj gem (no synced groups in this project).

Behavior gates

  • GitHub-backed lists: the affordance is hidden and the request is not even issued — the route returns an empty set for them by design.
  • Below two contributors: nothing renders, matching the web's totalContributors < 2 threshold, so a single-owner list stays uncluttered.
  • Failure is non-fatal: a failed or 403'd contributor fetch leaves the stack hidden rather than failing the whole list load.

Not changed

No row-write path was touched. GitHub row writes still go through the full-row updateItem — a partial PUT would rebuild the issue and rename it to "Untitled". The diff on ListsView.swift is purely additive; updateItem, setGitHubState, saveEdit, and updateField are untouched.

Testing

  • xcodebuild -scheme InterlinedList -destination 'platform=iOS Simulator,id=<pinned UDID>' -parallel-testing-enabled NO buildBUILD SUCCEEDED
  • Full unit suite (E2E skipped, serialized) — 954 tests, 0 failures
  • New APIClientListContributorsTests12 tests, 0 failures, covering: route + method, list-id percent-encoding, decode of the aggregation counts, preservation of server ranking, null and empty displayName fallback, the web-matching added N · edited M subtitle, the empty set GitHub-backed lists return, 401/403 propagation, and the stack's accessibility-label pluralization.

Accessibility

.accessibilityLabel on the stack reads "3 contributors" (singularizing at one), with a hint that it opens the full list; sheet rows announce name plus added/edited counts.

🤖 Generated with Claude Code

iOS surfaced watchers — who has been *granted* access — but never
contributors, who have actually *used* it. On a shared list the latter is
the more useful signal, and the web has shown it on the list header since
ContributorAvatarStack landed.

- add `listContributors(listId:)` in a new `APIClient+ListContributors`
  feature extension (not `APIClient.swift`), returning the route's full
  ranked set; `GET /api/lists/{id}/contributors` does not page
- add a `ListContributor` model carrying the server's added/edited counts
  and `score`, with a `ListContributorsResult` wrapper for the total
- add `ContributorAvatarStack` (cascading avatars, "+N" overflow) and
  `ListContributorsSheet`, whose rows mirror the WatchersListView style
- mount the stack as a tappable header section in `ListDetailView`,
  fetched concurrently with the existing schema/items/connections loads
- hide the affordance entirely for GitHub-backed lists, whose contributor
  set the route returns empty by design, and below two contributors, so a
  single-owner list stays uncluttered (matches the web's threshold)
- the contributor fetch is non-fatal: a failure or a 403 leaves the stack
  hidden rather than failing the whole list

Row writes are untouched — GitHub rows still go through the full-row
`updateItem`, since a partial PUT renames the issue to "Untitled".

Closes #58

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Xq12igmUH4tsNhKy54dY6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

W6: List contributors (P7)

1 participant