feat(scheduled): show a queued post's destinations, edit its time - #71
Merged
Merged
Conversation
GitHub #55 asked for a full scheduled-post editor — time, content and cross-post destinations — to match what the web help page describes. Read-only recon on 2026-09-09 found the content and destination halves have no route to call: • OPTIONS /api/messages/[id] -> Allow: DELETE, GET, HEAD, OPTIONS, PATCH, so PATCH is the only update verb a message has; • /api/messages/scheduled/[id] and /api/messages/[id]/schedule both 404, and /api/messages/scheduled allows GET, HEAD, OPTIONS only; • that PATCH honours scheduledAt alone — a content body returns 400 "No valid updates provided" and a content sent beside scheduledAt is silently discarded (2026-09-06 §1c V1 write-probe); • the deployed web client issues no PATCH /api/messages/[id] anywhere in its bundles, so it does not ship that editor either. So this builds everything the API can actually support and is explicit about the rest rather than offering controls that could not save. Kit — model scheduledCrossPostConfig, a live message field the client did not decode: { mastodonProviderIds, crossPostToBluesky, crossPostToLinkedIn }, key names taken from the web's own badge component. crossPostToTwitter is decoded speculatively and documented as unconfirmed on read. Domain — add ScheduledDestinations plus Message.scheduledDestinations (fetch-time only, not persisted, matching the linkPreviews precedent). Replace the narrow reschedule with a single updateScheduled(messageId: edit:) taking a ScheduledPostEdit. It validates entirely client-side and before any request: an empty edit, a past date, or a content/destination change each throw a typed MessagesError naming exactly what was refused, so a doomed call is never sent and an unsupported edit can never look like it saved. App — scheduled rows now show their destinations without opening anything; the sheet becomes EditScheduledPostSheet, with the time editable and the content and destinations shown read-only alongside a line saying why. The composer's schedule dialog gains a destinations summary and a "Post now instead" link, mirroring the web's combined dialog additively rather than restructuring the composer. The optimistic reschedule copy now routes through replacing(...), fixing a latent bug where rescheduling dropped the row's link previews and cross-post pills. Also answers the question the issue raised about a second defect: the composer does NOT drop cross-post selections on a scheduled post — it sends them in the same create body as scheduledAt. Pinned by a test. A new live contract test fails on purpose if a scheduled-post editor route ever appears, which is the signal to reopen #55. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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
Closes the buildable half of #55 and reports the rest as an upstream gap.
#55 asked for a full scheduled-post editor — time, content and cross-post destinations — to match what
/help/messagesdescribes. Read-only recon (GET/OPTIONS only, zero writes) found the content and destination halves have no route to call, and that the web does not ship that editor either. So this builds everything the API can actually support, models a live field the client was silently ignoring, and is explicit about the rest rather than offering controls that could never save.The probe (2026-09-09, read-only)
OPTIONS /api/messages/{id}Allow: DELETE, GET, HEAD, OPTIONS, PATCH— PATCH is the only update verb a message hasOPTIONS /api/messages/scheduledAllow: GET, HEAD, OPTIONS(read-only)GET/OPTIONS /api/messages/scheduled/{id}OPTIONS /api/messages/{id}/scheduleThat
PATCHhonoursscheduledAtalone (2026-09-06 §1c · V1 write-probe): a content body returns400 "No valid updates provided", andcontentsent alongsidescheduledAtis accepted but silently discarded.And the decisive one: the deployed web client issues no
PATCH /api/messages/{id}anywhere in its bundles — its only PATCHes are/api/user/updateand/api/notifications/*. The scheduled-post content/destination editor the help page describes is not shipped on the web either. This is an upstream API gap, not a macOS one.Changes
scheduledCrossPostConfig, a live message field the client never decoded (nullonce published). Shape taken from the web's own destination-badge component:{ mastodonProviderIds, crossPostToBluesky, crossPostToLinkedIn }.crossPostToTwitteris decoded speculatively and documented as accepted-on-create / unconfirmed-on-read.ScheduledDestinations+Message.scheduledDestinations(fetch-time projection, not persisted — same trade-off aslinkPreviews, so no SwiftData migration). Replace the narrowreschedulewith a singleupdateScheduled(messageId:edit:)taking aScheduledPostEdit. Every guard is client-side and runs before the request is built: an empty edit, a past date, or a content/destination change each throw a typedMessagesErrornaming exactly what was refused — so a doomed call is never sent and an unsupported edit can never look like it saved.ScheduledDestinationsLabeldistinguishes "not loaded" / "InterlinedList only" / a named list).RescheduleSheetbecomesEditScheduledPostSheet: time editable, content and destinations shown read-only with a line explaining why. ComposerscheduleSectiongains a destinations summary and a "Post now instead" link, mirroring the web's combined schedule dialog additively — the per-network toggles stay where they are, so the composer is not restructured (deliberate: bug(compose): nothing gates on email verification, so posting fails at publish time (G38) #41 is in flight on that surface).crossPostResults,crossPostLocationsandlinkPreviews, so rescheduling visibly wiped a row's previews and cross-post pills until the next refresh. Now routes through the exhaustivereplacing(...)helper.test_givenLiveCredentials_whenOptioningScheduledRoutes_thenNoEditorRouteHasAppearedfails on purpose the day a scheduled-post editor route appears. That is the signal to reopen feat(scheduled): edit a queued post content and destinations, not just its time #55, so the constraint cannot quietly outlive its reason.The second defect #55 asked about — ruled out
They are carried.
ComposerViewModel.submitNewPostsends the cross-post flags in the samecreatePostbody asscheduledAt. Now pinned bytest_givenScheduledPost_whenSubmitting_thenCrossPostFlagsTravelWithTheScheduleso a future composer refactor cannot quietly break it.Tests
BDD quartets across four surfaces:
nullconfig (nil ≠.none)null/ key-absentVerification
Run on the merged tree (
origin/devmerged in — sibling PR #37 landed mid-flight; clean merge, no conflicts):xcodebuild build→** BUILD SUCCEEDED **xcodebuild test(App) →Executed 760 tests, with 0 failures (0 unexpected)→** TEST SUCCEEDED **swift test InterlinedKit→Executed 403 tests, with 0 failures (0 unexpected)swift test InterlinedDomain→Executed 783 tests, with 0 failures (0 unexpected)swift test InterlinedPersistence→Executed 135 tests, with 0 failures (0 unexpected)grep -rE "^\s*import InterlinedKit" App/Features App/Navigation App/MenuCommands→ zero hits (Decision 0003)Follow-ups
linkedInTargetsandlinkedInLinkAsFirstComment;CreateMessageRequestmodels neither. That is the Integrations/Organizations projection feat(scheduled): edit a queued post content and destinations, not just its time #55 said to sequence behind — it does not exist yet, so it was left alone rather than half-built.🤖 Generated with Claude Code
https://claude.ai/code/session_01D5HxPbjMhuxf1BkKUdqK9e