feat(messages): wire up DM trash and restore (W3, #50) - #73
Open
Adron wants to merge 1 commit into
Open
Conversation
`trashDM`, `restoreDM` and `directMessage` have been implemented in `APIClient` since the DM surface landed, and every one of them was called from nowhere. The folder picker in `MessagesInboxView` therefore shipped a **Deleted** tab that nothing could ever put a message into, and a message the user deleted on the web was unreachable from iOS. This connects the three folders to the two endpoints. - Swipe-to-delete (destructive, `POST /api/dm/:id/trash`) on Inbox and Sent rows; swipe-to-restore (`POST /api/dm/:id/restore`) on Deleted rows. One `swipeActions` block branching on `folder`, so the picker and row layout are untouched — #51 rebuilds this list on `/api/dm/conversations` and should inherit a small diff - A **Delete** context-menu action on a bubble in `DMThreadView`, matching the web thread view. Also exposed as an `accessibilityAction`, since a context menu alone is not reachable by assistive input - Both paths are optimistic: `DMFolderMutation` drops the row before the request goes out and puts it back at the index it came from if the request fails. It lives in the model layer next to `DMRecipientFilter` so the rollback is testable without SwiftUI; `reinserting` clamps the index and ignores a row a concurrent refresh already restored, so a rollback can neither trap nor duplicate - The unread badge is refreshed after each success. Trashing sets `recipientDeletedAt`, which `countUnread` filters on, so the server count moves the moment an unread inbox message is trashed - That refresh arrives as an environment value (`dmUnreadRefresh`) injected once in `MainTabView`, not as an `@EnvironmentObject`. `DMThreadView` is also presented from `UserProfileView`'s sheet, from a deep link and from a `#Preview`, none of which have an `AppDataStore` to hand; an absent hook has to no-op rather than trap, and none of those three call sites needed touching - Failures roll the row back and surface an alert. A feature-endpoint 401 goes through `authState.handleUnauthorized()` — the trash/restore routes 404 a non-participant and 401 only on a genuinely rejected credential, but the rule holds regardless: never `logout()` on a feature endpoint Tests: request shapes for both endpoints (empty `{}` body, Bearer, `application/json`, percent-encoded id, 401 → `.status(401)`, 404 → `.server`), and the rollback — original index restored, first/last rows, unknown id, duplicate suppression, index clamped when the list shrank underneath. Closes #50. Verified: build succeeds; 953 tests, 0 failures (iPhone 16 · 18ABC526-…, -parallel-testing-enabled NO, E2E skipped) — 942 on this base plus 11 new. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M65ZdXdGFRW3TyDKSBwhTd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
APIClient.trashDM(id:)andrestoreDM(id:)have existed since the DM surfacelanded and were called from nowhere, so
MessagesInboxViewshipped a three-wayfolder picker whose Deleted tab nothing could ever fill — and a message the
user deleted on the web had no way back. This PR connects the folders to the
endpoints: swipe-to-delete on Inbox/Sent, swipe-to-restore in Deleted, and a
per-message Delete in the thread view, matching the web.
What's included
Commit
3e0f554:MessagesInboxView— oneswipeActionsblock on the existing row,branching on
folder: destructive Delete (POST /api/dm/:id/trash) inInbox and Sent, Restore (
POST /api/dm/:id/restore) in Deleted. Thepicker, the row and the empty states are untouched, so W3: Grouped DM inbox via GET /api/dm/conversations #51's rebuild of this
list on
/api/dm/conversationsinherits a small diff.DMThreadView— a Delete context-menu action on a bubble, mirroringcomponents/messages-dm/MessageThread.tsx. Also exposed as anaccessibilityAction, since a context menu on its own isn't reachable byassistive input.
DMFolderMutation(Models/DirectMessage.swift) — the optimistic edit,extracted next to
DMRecipientFilterso the rollback is testable withoutSwiftUI. The row is dropped before the request goes out and reinserted at the
index it came from on failure;
reinsertingclamps the index and ignores a rowa concurrent refresh already restored, so a rollback can neither trap nor
duplicate.
recipientDeletedAt, which the server's unread query filters on, so the countreally does move when an unread inbox message is trashed.
dmUnreadRefreshenvironment value — injected once inMainTabViewrather than passing
AppDataStoredown.DMThreadViewis also reached fromUserProfileView's sheet, from a deep link, and from a#Preview, none ofwhich have a store in scope; an
@EnvironmentObjectwould trap there, anabsent environment hook no-ops. No other call site needed touching.
feature-endpoint 401 goes through
authState.handleUnauthorized(), neverlogout().Testing
xcodebuild -scheme InterlinedList -destination 'platform=iOS Simulator,id=18ABC526-…' -parallel-testing-enabled NO -skip-testing:InterlinedListTests/E2EReadOnlyTests test— 953 tests, 0 failures (942 on this base plus 11 new). Build succeeds.{}body, Bearer header,application/json, percent-encoded id, 401 →.status(401), 404 →.server),and the rollback (original index restored, first/last rows, unknown id,
duplicate suppression, index clamped when the list shrank underneath).
Follow-ups
APIClient.directMessage(id:)is still uncalled; nothing in this UI needs asingle-message fetch.
only the caller's side), so no client-side folder bookkeeping is needed — worth
re-checking when W3: Grouped DM inbox via GET /api/dm/conversations #51 reshapes the list around conversations.
Closes #50.
🤖 Generated with Claude Code