Repository navigation
W7: Open the web canonical permalinks in-app (/user/:u/status/:id, list and document permalinks) #61
Description
Part of #44 · Lane W7 — deep links · Size S–M · part bug, part parity
The problem
Links copied from the web mostly do not open in the app, and one of them lands on the wrong screen.
| Link a user can copy on the web | What it is | iOS today |
|---|---|---|
/user/{username}/status/{id} |
the web's canonical message permalink (components/messages/MessageShareMenu.tsx:41) |
❌ parses to .userProfile(username) — opens the author's profile, not the post |
/user/{username}/lists/{id} |
public list permalink (app/user/[username]/lists/[id]/page.tsx) |
❌ opens the profile |
/user/{username}/documents/{id} |
public document permalink | ❌ opens the profile |
/lists/{id} |
list permalink — iOS itself generates this (ILWebURL.list(_:), Services/ILWebURL.swift:24) |
❌ explicitly rejected by the parser |
/message/{id}, /documents/{id}, /lists/shared/{token}, /documents/shared/{token} |
✅ routed |
AppDeepLink.parse (InterlinedList/InterlinedListApp.swift:178-240) handles user, message,
documents, lists/shared and the auth links; the user case takes the first segment and drops
the rest, and the lists case rejects anything that isn't a share token.
The stale premise to delete
The lists case carries this comment:
A bare
/lists/<id>permalink is NOT routed — it carries no owner username, which the list-detail
endpoint requires.
That is no longer true (and the-gaps.md repeats it). GET /api/lists/{id} is Bearer-ready and
authorizes by role — owner / manager / collaborator / watcher
(app/api/lists/[id]/route.ts:29-52) — no owner username needed. Since #40 the app also knows about
lists shared to the user, so the destination exists for non-owned lists too.
Build this
- Extend
AppDeepLinkwith.message(id:)from/user/{u}/status/{id}, plus
.list(id:),.publicList(owner:id:),.publicDocument(owner:id:). - Parse: in the
usercase, look at the second segment (status/lists/documents)
before falling back to the profile; in thelistscase, accept a bare id. - Route
.list(id:)toListDetailView— resolve fromAppDataStorefirst, else add a
list(id:)fetch (GET /api/lists/{id}; there is no single-list fetcher today). A403/404
means no access → show a clear "You don't have access to this list" state, not a crash or a
silent no-op. - Route the public list/document permalinks to
PublicListDetailView/PublicDocumentReader
(/api/users/{u}/lists/{id},/api/users/{u}/documentsalready exist). - Fix the stale comment, and drop the matching claim from
the-gaps.md.
Acceptance criteria
- Each URL in the table above opens its own screen, for both
interlinedlist://and
https://interlinedlist.comforms (andwww.). - Unknown/foreign hosts still return
nil. - Unit tests in the existing deep-link test file, one per row of the table, including the
no-access path. - Sanity-check against Universal Links: these paths must be covered by the Associated Domains /
AASA setup tracked inApp-Store-Deployment-Checklist.md.
Files: InterlinedList/InterlinedListApp.swift, Services/ILWebURL.swift, Views/RootView.swift
(or wherever the deep-link destination is presented), Services/APIClient.swift (single-list fetch).
Activity