Skip to content

iOS↔web parity + GitHub OAuth/Universal Links: DMs, sharing, GitHub lists, offline sync, tag discovery, link previews, deep links (G1–G14, A1/A2) - #13

Merged
Adron merged 24 commits into
mainfrom
dev
Aug 17, 2026
Merged

iOS↔web parity + GitHub OAuth/Universal Links: DMs, sharing, GitHub lists, offline sync, tag discovery, link previews, deep links (G1–G14, A1/A2)#13
Adron merged 24 commits into
mainfrom
dev

Conversation

@Adron

@Adron Adron commented Aug 2, 2026

Copy link
Copy Markdown
Member

iOS ↔ web feature parity build-out

Closes the feature gaps between the iOS client and interlinedlist.com. Analysis + plan live in the-gaps.md (rewritten); offline-sync design in Offline-Document-Sync-Design.md.

Context

The prior analysis was blocked on "the backend rejects Bearer" for exports/GitHub/LinkedIn. That wall is gone — the backend now accepts Bearer everywhere (verified against source + live probes) — so all of it became buildable, and this PR builds it. Three shipped-but-broken writes (405s) are also fixed.

What's included

  • fix(api): correct HTTP verbs on 3 writes (/api/user/update, edit message, mark-notification-read were POST/PUT → PATCH); muted-users screen (G7); list-data-rows CSV export (G8, exports now work over Bearer).
  • feat(dm): Direct Messages (G1) — inbox + threads, near-realtime /updates polling, image attach, block/not-mutual states, envelope badge, profile "Message" button.
  • feat: document templates (G3) + people search (G6).
  • feat(compose): LinkedIn posting-target picker (G5) — rewrote LinkedInTarget to the real backend union.
  • feat(sharing): tokenized share-links + document collaborators (G2).
  • feat(lists): GitHub-backed lists (G4) — repo picker, refresh, sync-status (gated on a linked GitHub identity).
  • feat(settings): active-sessions management (G12).
  • feat(documents): offline read + delta pull (G9 slice 1) — /api/documents/sync returns the whole tree in one call; flag-gated, online path unchanged when off.
  • feat: content deep links + web-permalink share actions (G10).
  • fix: DM recipient navigation + follow-status decode (client model didn't match /api/follow/:id/status) + resource-aware share copy — from the interactive smoke-test.
  • ui(tabbar): user-avatar menu moved to the far right (Home · Lists · Documents · Messages · Notifications · Avatar).

Added since the initial description

  • feat(documents): offline edit queue + push and conflict-copy resolution (G9 slices 2–3) — completes the offline-sync design; local edits queue while offline and reconcile on reconnect, conflicts preserved as copies.
  • feat(editor): markdown formatting toolbar — H1–H6 heading dropdown, plus an icon-based Write/Preview toggle.
  • feat(sharing): email share-invites for lists & documents (owner-side send / list / revoke) + watcher-candidate search with debounced querying.
  • feat(lists): parent/child list relationships — the list-detail "Connections" section is now "Parents & Children," with an Add-Parent / Add-Child menu and role-aware rows. Corrected ListConnection to the backend's fromListId/toListId + raw (unwrapped) POST contract, fixing a mismatch that previously left the section permanently empty.
  • feat(lists): file, move, and rename lists into folders — folderId split out from parentId in the model; folder picker on create.
  • docs: backend asks A1 (GitHub in-app OAuth) + A2 (Universal Links) spec.

Verification

  • 800 unit tests, 0 failures (E2E excluded); clean build on iPhone 16 (iOS 18.0). Each feature added MockURLSession tests + #Previews and passed ios-review.
  • Interactive smoke-test (logged in as a subscriber): 6/7 new surfaces PASS, no crashes; the 2 defects found were fixed and re-verified. One residual: D-1's literal runtime tap wasn't re-driven (UI-automation tooling gap) — the fix mirrors the confirmed-working people-search flow; a quick manual DM tap-test is worth doing.

Follow-ons (not in this PR)

  • Backend A1 — GitHub in-app linking needs a mobile branch on the OAuth callback (spec written).
  • Backend A2 — Universal Links need apple-app-site-association hosted; custom-scheme deep links work today (spec written).
  • G11 live presence (optional). Multi-account / tag-discovery / realtime remain true dead-ends (no Bearer/endpoint).

Update — 2026-08-16 (tag discovery, link previews, document deep links, GitHub OAuth / Universal Links)

Further work landed on dev since the description above.

Product change

  • Lists-into-folders reverted (product decision). The list-folder concept was removed entirely — lists organize purely via parent/child parentId nesting. ListFolder, folderId, and the /api/folders client calls are gone; ListTreeNode.buildTree(lists:) nests on parentId only. (The backend's A5 folderId-in-list-GET did ship and was live-verified, but iOS no longer consumes it.)

New features

  • feat(tags): tag discovery / trending (G13)TrendingTag / TagSuggestion models + trendingTags(window:limit:) / tagAutocomplete(query:limit:). FeedView's tag strip now shows server-wide Trending tags (falls back to current-page tags on failure); typing #… in the search box switches to tag autocomplete that drives the existing tag: filter. Previously classified X2 "no endpoint exists" — the backend shipped /api/tags/{trending,autocomplete} (Bearer, live-verified).
  • feat(compose): live link preview (G14)linkMetadata(url:) + a live Open-Graph preview card in ComposeView. Detects the first URL as you type (NSDataDetector, 400 ms debounce), reuses the existing LinkPreviewBlock; 429 / failed → no card.
  • feat(deep-links): document permalinks + share-token resolver (G10 follow-on).document(id:)DocumentLinkView; .sharedDocument(token:) → read-only SharedDocumentView + resolveSharedDocument(token:). Parser routes /documents/:id and /documents/shared/:token (custom-scheme + https). List permalinks stay unrouted (a bare /lists/:id carries no owner username the list-detail endpoint requires).
  • feat(auth): GitHub native OAuth + Associated Domains (A1/A2)OAuthProvider.supportsNativeAuth is now true for all providers, enabling GitHub sign-in/linking (backend A1 mobile branch deployed + live-verified). Added the com.apple.developer.associated-domains (applinks:interlinedlist.com) entitlement; the AASA is deployed and lists BJA9558E4B.com.interlinedlist.app (verified), and the Associated Domains capability is enabled on the App ID.

Verification

  • 787 unit tests, 0 failures (E2E excluded); clean build on iPhone 16. New APIClient methods each have MockURLSession tests; new views have #Previews. (+31 tests this pass: 12 tags, 9 link-metadata, 6 share-resolver, +4 deep-link parse.)

Follow-ons update (supersedes the list above)

  • Backend A1/A2 delivered (deployed + live-verified 2026-08-14); the iOS tail is now in this PR (native GitHub OAuth + Associated Domains entitlement). Universal Links resolve once the app is installed with the regenerated profile.
  • Tag discovery is no longer a dead-end — shipped (G13).
  • G10 list follow-ons — list permalink inbound is backend-limited (no owner username in /lists/:id); list share-token resolve deferred behind the lists refactor.
  • G11 live presence (optional). Multi-account switching and a general realtime channel remain true dead-ends (session-only / no endpoint).

🤖 Generated with Claude Code

Adron and others added 24 commits July 31, 2026 16:10
…tion log

Full rewrite dated 2026-07-31. Cross-checked backend source, live docs, and the
iOS client; adds a prioritized gap list, the ready-now vs backend-blocked split,
live-probe evidence, and a phase-by-phase implementation plan/progress log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st-data-rows export

Tier-0 defects (D1/D2/D3) — routes verified PATCH-only against backend source:
- updateProfile/updateUserSettings: POST → patchCamel (/api/user/update)
- editMessage: PUT → patchCamel (/api/messages/:id)
- markNotificationRead: PUT → patch (/api/notifications/:id/read)
G7: MutedUsersView (API already wired) linked from Settings.
G8: exports now accept Bearer — add ExportType.listDataRows (4th CSV export).
Also corrects 3 stale tests to match backend truth (multipart 'file', 'folderId').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New DirectMessage models + 11 APIClient methods (/api/dm/*, Bearer, free).
MessagesInboxView (inbox/sent/deleted) + DMThreadView (chat bubbles, markdown,
image attach, block/not-mutual states, ~4s /updates poll). Envelope badge in
MainTabView from AppDataStore.dmUnreadCount; 'Message' button on profiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
G3: documentTemplates() + createDocumentFromTemplate() (subscriber-gated) with a
'Start from template' picker in DocumentsView; DocumentTemplate model.
G6: searchUsers() + FindPeopleView (searchable, debounced) linked from Profile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrote LinkedInTarget to the real backend union {kind,pageId?,personalPageId?}
(dropped the wrong 'organizationId'). Added LinkedInPostingTarget +
linkedInPostingTargets(); ComposeView multi-select (subscriber + LinkedIn
identity) maps selected targets into linkedInTargets on post, personal-fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Share-links (create/copy/revoke, watcher/collaborator/manager roles) for lists
AND documents via a reusable ShareLinksSheet; per-person DocumentCollaboratorsView
(reused WatcherRole). 8 APIClient methods (create subscriber-gated). Wired into
list detail and document detail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UserList gains source/githubRepo/githubMeta; GitHubRepo/GitHubIssue models;
githubRepos()/githubIssues()/refreshList() + createList(githubSource:). Repo
picker in CreateListView gated on AuthState.hasGitHubIdentity + subscriber
(default from user.githubDefaultRepo); Refresh + sync-status on list detail.
In-app GitHub linking still needs a backend mobile callback (ask A1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UserSession model + userSessions()/revokeSession(); SessionsView
('Where you're signed in') listing devices with current highlighted and
non-revocable, others revocable. Linked from Settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Registers the 22 new Swift sources/tests added across Phases 1-7 (DMs, sharing,
templates, people-search, LinkedIn targets, GitHub lists, sessions, muted-users).
Consolidated because the xcodeproj gem interleaves entries by hash order, so
per-phase splitting of project.pbxproj isn't cleanly separable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
documentSync(lastSyncAt:) delta pull (full state when no cursor); pure
DocumentSyncMerge applies upserts + deletedAt tombstones; SyncCache persists
folders/documents/cursor per user. AppDataStore uses the sync path behind the
ILOfflineDocSync flag (default on) — one call returns the whole tree vs. the
root-only GET /api/documents; the online path is unchanged when the flag is off.
Read-only slice: offline edit/push (slice 2) and conflict-copy (slice 3) deferred.
Design in Offline-Document-Sync-Design.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ILWebURL builds canonical /user, /message, /lists, /documents permalinks; a
'Share link' action on message/profile/list/document shares the web URL. Pure
AppDeepLink.parse routes both interlinedlist:// and https://interlinedlist.com
links for profiles and messages (message(id:) + MessageLinkView loader); auth
deep-links preserved. Universal Links (https→app) await backend AASA (ask A2);
list/doc inbound + shared-token resolution are follow-ons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…share copy

D-1 (smoke-test): DM 'New message' recipient tap didn't open the thread — the
onChange-after-dismiss nav was unreliable and the row wasn't fully tappable. Now
selection sets navigationDestination(item:) directly and the row uses
Spacer()+contentShape (mirrors the verified-good FindPeopleView pattern); recipient
filter extracted to a unit-tested DMRecipientFilter.
D-2 (pre-existing): FollowStatus failed to decode — /api/follow/:id/status returns
{status,isFollowing,isPending}, not {following,followedBy,pendingRequest}. Remapped
via a defensive init(from:) (also handles the nested {follow:{status}} shape from
POST /api/follow/:id); followedBy defaults false (not provided by these endpoints).
Nit: WatcherRole.detail(for:) makes share-link/collaborator copy resource-aware so
documents no longer read 'Can view this list'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Top bar order is now Home, Lists, Documents, Messages, Notifications, then the
user-avatar (profile) menu furthest right — previously the avatar sat 4th, before
Messages/Notifications.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Offline document/folder create/edit/delete route through an optimistic outbox
(SyncOperation + DocumentSyncOutbox coalescing: create+updates→create,
update+updates→update, delete-of-unsynced-create cancels) and replay via
pushDocumentSync (POST /api/documents/sync → {lastSyncAt}; 429 → APIError.rateLimited).
syncCycle pushes-first-then-pulls (the endpoint swallows per-op errors, so the pull
reconciles); NetworkReachability replays on reconnect, plus foreground/refresh/
debounced triggers. DocumentsView branches on the ILOfflineDocSync flag with a
pending-sync glyph; the flag-off online path is unchanged. LWW for now —
conflict-copy is Slice 3 (seam marked TODO).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes offline document sync. Conflict = a locally dirty doc whose server
updatedAt beats its baseline. Resolution keeps the local edit live and preserves
the server version as a new '(conflicted copy <date>)' doc (never lose data),
with a dismissible banner. syncCycle reordered to PULL-first-then-push so the
server's version is observed before a local push would clobber it (the endpoint
is LWW with no version guard). Adds DocumentSyncConflict (pure), baselines in
DocumentSyncState, and DocumentSyncMerge.apply(protectingIds:) to keep conflicting
dirty ids local during merge. Offline writes are no longer LWW, so the default-on
ILOfflineDocSync flag is safe to ship.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hand-off spec for the two remaining backend/ops dependencies for iOS parity:
exact backend diffs (GitHub authorize/callback mobile handoff; AASA route +
middleware bypass), the AASA JSON with appID BJA9558E4B.com.interlinedlist.app,
the iOS entitlement/flag, and the deploy + Apple-portal provisioning steps.
Implementations: backend PR (CompositeCode/interlinedlist) + iOS HOLD PR #14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ite/Preview

Turn the single H2 heading button into an H1–H6 menu, convert the text
"Write"/"Preview" segmented picker into a compact icon toggle, and extract a
reusable MarkdownFormatToolbar / MarkdownFieldEditor (image button optional) now
used by the list long-text field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arch

Add the newest web sharing generation to iOS: ShareInvite model + owner-side
send/list/revoke APIClient methods (camelCase, subscriber-gated create), a
reusable ShareInvitesSheet wired into list and document detail, and add-by-search
on the list-watcher flow to match the document-collaborator screen. The
session-only inbound claim is intentionally deferred (Bearer-only client).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createListConnection posted sourceListId/targetListId, but the backend expects
fromListId/toListId (+ label) — a silent latent bug where connection creation sent
the wrong fields. Rename the model + method, add a ListRelationship (parent/child)
helper and otherListId/relationship accessors, and surface parent/child direction
in the list-detail connections UI. Tests updated to the real contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…del split)

Wire up folder membership end-to-end on the client: folderId on createList/
updateList (camelCase; "" == root), a folder picker in create, move-to-folder in
the rename sheet, and an inline RenameFolderView. Split UserList into distinct
parentId (list nesting) and folderId (folder membership) — previously folderId
decoded from the "parentId" key, conflating the two — and update buildTree to group
by the real folderId (folder membership wins over nesting). folderId decodes to nil
until the list GET returns it; filed as backend ask A5 in the-gaps.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt (A1/A2 iOS)

A1: OAuthProvider.supportsNativeAuth now returns true for all providers (was
'self != .github'), so GitHub sign-in + linking enable in LoginView/RegisterView/
LinkedIdentitiesView. A2: add com.apple.developer.associated-domains
(applinks:interlinedlist.com) to the entitlements; G10's onOpenURL/AppDeepLink.parse
already route https permalinks. DEPLOY-GATED — see PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Adron Adron changed the title iOS↔web feature parity: DMs, sharing, GitHub lists, LinkedIn, templates, search, sessions, offline-read, deep links (+ defect fixes) iOS↔web parity + GitHub OAuth/Universal Links: DMs, sharing, GitHub lists, offline sync, tag discovery, link previews, deep links (G1–G14, A1/A2) Aug 16, 2026
@Adron
Adron merged commit ec5d47e into main Aug 17, 2026
1 check passed
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.

1 participant