feat(timeline): web-parity message actions — Reply, I Dig!, Link, Push, Push & Comment - #32
Merged
Merged
Conversation
The "Link" message action (#27) needs a canonical web URL for a post, but the only permalink logic in the tree was private to CreateIssueFromMessageViewModel. Promote it to InterlinedDomain so the row's Link action, Push & Comment, and the create-issue body all agree on one shape. The builder is stricter than the code it replaces: it trims the id, returns nil for a blank one rather than emitting a half-formed URL, and percent-encodes "/" so a malformed id can never forge extra path segments outside /messages/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tm5htvRQsWxNeq7cmaAakW
…ctions MessageRowView took eight individual closure parameters and #27 adds three more (Reply, Push, Link), which pushes the call sites past readable. Gather them into one MessageRowActions value instead. Behaviour-neutral: every handler stays optional with the same "nil means the row hides that affordance" contract, and the row stays passive. SearchRootView needs no change — it already relied on the defaults, which are now `.none`. `none` is computed rather than a static let: the handlers are non-Sendable closures, so a shared static instance fails Swift 6 concurrency checking. Verified: App suite 627/627 green, unchanged from before the refactor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tm5htvRQsWxNeq7cmaAakW
Closes the web-parity gap in #27. No new networking — Kit and Domain already implemented every verb; only the App-layer affordances were missing. - Reply: navigates to the thread and opens its composer already expanded and focused, so there is one reply write surface rather than two. - Push: one-tap bare repost (nil commentary, public), matching the web. The API returns the new push rather than an updated original, so the source row's count is nudged locally and the push is prepended. - Push & Comment: unchanged path through RepostSheetView, now a distinct action instead of the only way to repost. - Link: SwiftUI.ShareLink over the new domain permalink. Unconditional, since it needs no host wiring. No NSPasteboard, no AppKit. All five are mirrored into the context menu and carry VoiceOver labels. Reply/Dig/Push degrade to plain count labels in read-only hosts. Also folds the two duplicated private `byTogglingDig` helpers into a shared Message+OptimisticUpdates. Both copies dropped crossPostResults, crossPostLocations and linkPreviews, so digging a post visibly lost its link preview cards until the next refetch; the shared version copies every field. Verified: App suite 636/636 green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tm5htvRQsWxNeq7cmaAakW
Approved by the repo owner on #27. User-facing strings only, plus the one action name that sat awkwardly next to its new sibling: - "Repost" -> "Push"; the commentary sheet is now titled "Push & Comment" - "Reposted from @x" -> "Pushed from @x" - dig VoiceOver labels -> "I Dig!" / "Undo I Dig!" - MessageRowActions.onRepost -> onPushAndComment Type and file names (RepostSheetView, ComposerEvent.messageReposted) and the domain's repostCount are deliberately untouched: repostCount mirrors the wire field, and renaming the types would balloon the diff for no user-visible gain. Verified: App 636/636, Kit 314/314, Domain 628/628, Persistence 135/135. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tm5htvRQsWxNeq7cmaAakW
…arity Resolves the conflict `dev` created by landing "Create from…" (G16, PR #31) on `MessageRowView` while this branch was collapsing that same row's handler list into `MessageRowActions`. The two sides disagreed about call *shape*, not behaviour: `dev` added an eleventh standalone closure parameter, this branch replaced the parameter tail with one value. Keeping either side alone would have been wrong — `dev`'s form reintroduces exactly the long tail this branch removes, and this branch's form silently drops a feature `dev` just shipped. So `onCreateFrom` is folded into `MessageRowActions` alongside the other host-sheet handlers, `MessageRowView` reads it through `actions`, and `TimelineRootView.rowActions(_:)` wires it to the existing `createFromTarget` sheet. The affordance behaves exactly as it does on `dev`; only where its closure is declared has changed. Note that git auto-merged `MessageRowView.swift` without a conflict into a hybrid — the collected `actions` value *and* a leftover standalone `onCreateFrom` property. It compiled, which is why it needed catching by reading rather than by the gate. Verified on the merged tree: build succeeded; 710 App tests, 388 Kit, 699 Domain, 135 Persistence, all passing; Decision 0003 grep clean; project.pbxproj untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYA3FQuyhHgvpbbeMFuYrx
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.
Issue: #27
Implements the five web message actions on the native client: Reply, I Dig!, Link, Push, and Push & Comment.
The short version
No new networking was needed. Kit and Domain already implemented every verb — the wire has spoken
pushedMessageIdall along. The gap was entirely App-layer affordances plus naming. That's why this is four small commits rather than a feature build.MessageDetailView's composerSwiftUI.ShareLinkover a new domain permalinkCommits
28c2dfc— domain permalink. Promotes the permalink builder out ofCreateIssueFromMessageViewModel, where it was private, intoInterlinedDomain. Stricter than what it replaces: trims the id, returnsnilfor a blank one instead of a half-formed URL, and percent-encodes/so a malformed id can't forge path segments outside/messages/.d8e7476—MessageRowActionsrefactor. The row took eight closure parameters and this PR adds three more. Collapsed into one value. Provably behaviour-neutral: 627/627 before and after.d07c152— the five affordances, mirrored into the context menu, each with a VoiceOver label.be26f57— the rename to web vocabulary.Decisions flagged for review
SwiftUI.ShareLink(spelled with the module prefix —InterlinedDomainexports its ownShareLink). Its share sheet includes Copy, so noNSPasteboardand no AppKit. Follows the precedent inShareLinksView. This PR adds no AppKit imports; the four existing ones are untouched..public, matchingRepostSheetViewModel's existing default.RepostSheetView. No newComposerModecase.RepostSheetView,ComposerEvent.messageReposted, and the domain'srepostCount(which mirrors the wire field). Only user-facing strings andonRepost→onPushAndCommentchanged. Renaming the types would balloon the diff for no user-visible gain; happy to do it as a follow-up if you'd rather.Bug fixed along the way
TimelineViewModelandMessageDetailViewModeleach carried a privatebyTogglingDig()that rebuilt theMessagewithoutcrossPostResults,crossPostLocations, orlinkPreviews— so digging a post visibly dropped its link-preview cards and cross-post pills until the next refetch. Push needed a second such helper, so both were folded into a sharedMessage+OptimisticUpdatesthat copies every field. Not in the issue's scope, but it was the exact code being extended.Verification — observed, not assumed
Re-run after merging
origin/dev(PRs #24 and #25 landed mid-branch):New BDD quartets:
MessagePermalinkTests(9),MessageRowActionsTests(5), plus push coverage on both view models — happy, invalid, upstream-failure, and boundary including the double-click de-bounce.Not done: the interactive pass over all three hosts (timeline / detail / search) needs a signed-in session, which I don't have. Worth a few minutes before merge.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Tm5htvRQsWxNeq7cmaAakW