Skip to content

feat(lists): shared-with-me, contributors, add-watcher and invite landing - G23 #48

Description

@Adron

Summary

Five live Lists routes the client does not build. Together they are the whole "lists other people gave me access to" story, which the macOS sidebar has no equivalent of at all.

The gap, route by route

Route Live status What it unlocks
GET /api/lists/watching ✅ 200 with real rows (probed 2026-09-07) Watched / shared-with-me lists — the datagrid the web shows on /lists
GET /api/lists/{id}/contributors exists The full ranked contributor list
POST /api/lists/{id}/watchers exists, subscriber-gated Add a watcher. The client can read and delete watchers but not add one
GET /api/lists/shared/{token}/data exists, public Row data for a token-shared list — the read-only viewer's missing half
GET /api/lists/invite/{token} exists, public The email-invite landing page

Live shape for GET /api/lists/watching (2026-09-07)

{"lists":[{"id":"1df0fe30-…","userId":"c65092fa-…","messageId":null,
           "parentId":"7835c874-…","folderId":null,
           "title":"Shows Upcoming & Seen","description":null,"isPublic":true,
           "metadata":null,"source":"local","githubRepo":null,"githubRepoPrivate":null,
           "createdAt":"…","updatedAt":"…","deletedAt":null,
           "user":{"id":"c65092fa-…","username":"adron","displayName":"Adron Hall"},
           "parent":{"id":"7835c874-…","title":"The Metal"},
           "children":[],"role":"collaborator"}]}

Note it carries role (the caller's role on that list), an embedded user (the owner), and a parent projection — everything the web's datagrid column set needs ("Each entry shows the list title, owner, your role, and a link to view the list", /help/lists).

It also carries folderId and githubRepoPrivate, which matter to two sibling issues (list folders; GitHub-backed lists).

⚠️ Backend constraint on the invite pair

GET /api/lists/invite/{token} is public, but POST /api/lists/invite/{token} (the accept half) is declared x-auth-type: session in the live spec — a Bearer-only client cannot claim an invite. Same for POST /api/lists/shared/{token}.

So this issue delivers the landing experience (show what the invite grants, who sent it, which list) and must hand the accept step to the browser until the backend exposes a Bearer-reachable claim route. That backend ask is filed separately. Do not ship a native Accept button that 401s.

Accepting an invite is documented as always free, so no entitlement gate belongs on the landing view.

Roles vocabulary (/help/lists)

UI label → API role name: Read-only → watcher, Edit → collaborator, Admin → manager. The domain already uses these names in Sharing.swift; keep the UI labels aligned with the web's.

Division of labor

Kit

  • Lists.watching() → GET /api/lists/watching
  • Lists.contributors(id:) → GET /api/lists/{id}/contributors
  • Lists.addWatcher(listID:userID:) → POST /api/lists/{id}/watchers
  • Lists.sharedData(token:) → GET /api/lists/shared/{token}/data
  • Lists.invite(token:) → GET /api/lists/invite/{token}
  • DTOs for each, plus contract tests. watching rows are a superset of the owned-list shape — reuse the existing ListDTO where it fits rather than forking it, and add role/user/parent as optionals.

Domain

  • A WatchedList projection (list + owner + caller's role) and a ListsService.watching().
  • contributors(listID:) and addWatcher(...), the latter gated on the subscriber entitlement (see the entitlement issue).
  • Extend the existing share-token resolve path with the row-data call so a read-only viewer sees rows, not just the schema.

App

  • A Shared with me section in the Lists sidebar (or a segmented control on the Lists root), showing title, owner, and your role — mirroring the web's datagrid.
  • Wire AddWatcherSheetView (already present) to the real add-watcher call.
  • Contributors view on a list's detail.
  • The read-only token-shared list renders its rows.
  • An invite-landing view reachable from interlinedlist://…/invite/{token}, ending in "Accept in your browser" until the claim route is Bearer-reachable.

Tests

  • happy — watching returns rows grouped by role; contributors ranked; watcher added
  • invalid — add-watcher for a user who is already a watcher; an unknown token
  • upstream-failure — watching 500s → the sidebar section shows an error, owned lists still render
  • boundary — zero watched lists; a watched list whose parent is not itself watched (the parent projection must not imply access)

Acceptance criteria

  • A list someone shared with me appears in the macOS sidebar without me knowing its URL.
  • Opening a share-link as a viewer shows the rows.
  • Full E2E gate green.

Notes

work-consolidation.md tracks this as G23. Size M.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestparityWeb-parity gap with the InterlinedList web app

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions