Skip to content

iOS↔web feature parity: DMs, sharing, GitHub lists, LinkedIn, templates, search, sessions, offline-read, deep links (+ defect fixes) - #13

Open
Adron wants to merge 17 commits into
mainfrom
dev
Open

iOS↔web feature parity: DMs, sharing, GitHub lists, LinkedIn, templates, search, sessions, offline-read, deep links (+ defect fixes)#13
Adron wants to merge 17 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).

Verification

  • 694 unit tests, 0 failures (E2E excluded); clean build throughout; 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)

  • G9 Slices 2–3 — offline edit + push (conflict-copy policy chosen) — starting next.
  • Backend A1 — GitHub in-app linking needs a mobile branch on the OAuth callback.
  • Backend A2 — Universal Links need apple-app-site-association hosted; custom-scheme deep links work today.
  • G11 live presence (optional). Multi-account / tag-discovery / realtime remain true dead-ends (no Bearer/endpoint).

🤖 Generated with Claude Code

Adron and others added 17 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>
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