eleven / ocho Public

ios: reply in thread — the verb on the message menus (#1468)

Thread-pane spec §3.5 on iOS: "Reply in thread…" joins the long-press
message menu (the feed's FeedMenuOverlay sheet) and the SwiftUI context
menu the thread view's rows use, right after Reply, same gating. It
resolves the tapped message to its thread ROOT (ChatFeedModel.replyTarget
— replying to a reply continues its thread), opens the existing
presentation — today's lifted overlay, unchanged — pinned to that root
(a re-tap inside the open thread just re-arms the root reply), and then
puts the caret in the reply box: unlike a tally/quote tap this IS a reply
intent, so it's the one thread-open path that pops the keyboard on touch.

MessageActions grows onReplyInThread (LChatCore, optional, defaulted —
every existing call site, the Mac's included, is untouched; the Mac menu
wires the verb in its own slice). No wire/vector changes; the native
changelog is held for the release mint.


Claude-Session: https://claude.ai/code/session_01EXy6kzmYE9rChy3KgciJvZ
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by codex agent, operated by paul@keymail.dev 8a0fccf8d4b3f7c4879dfdc09394166957ad77eb parent e9c7c6a
2 files changed, +23 −0
  • ios/LChat/Views/NativeChatScreen.swift +17 −0
  • ios/LChat/Views/StockChatFeed.swift +6 −0
diff --git a/ios/LChat/Views/NativeChatScreen.swift b/ios/LChat/Views/NativeChatScreen.swift
index 5006780..920f87a 100644
--- a/ios/LChat/Views/NativeChatScreen.swift
+++ b/ios/LChat/Views/NativeChatScreen.swift
@@ -866,6 +866,7 @@ struct NativeChatScreen: View {
var actions = MessageActions(
canEdit: model.canEdit(m),
onReply: { startReply(m) },
+ onReplyInThread: { replyInThread(m) },
onEdit: { startEdit(m) },
onDelete: { model.deleteMessage(m.id) },
onCopy: { UIPasteboard.general.string = m.text },
@@ -894,6 +895,22 @@ struct NativeChatScreen: View {
stageThreadReply()
}
+ // "Reply in thread" (thread-pane spec §3.5): resolve the tapped message
+ // to its thread's ROOT (replyTarget — replying to a reply continues its
+ // thread), open the thread view pinned to it (a re-tap inside the open
+ // thread just re-arms the root reply), then put the caret in the reply
+ // box. Unlike a tally/quote tap this IS a reply intent, so the keyboard
+ // pops — the one thread-open path that focuses on touch.
+ private func replyInThread(_ m: ChatFeedModel.Message) {
+ guard let rootID = model.replyTarget(for: m).id, rootID > 0 else { return }
+ if threadRootID == rootID {
+ stageThreadReply()
+ } else {
+ openThread(rootID)
+ }
+ composerFocused = true
+ }
+
// Stage the composer's reply on the thread's root — the view's composer
// contract ("Replying to <root author>"). A reply displaces an in-flight
// edit, but the keyboard is deliberately NOT popped: it would cover the
diff --git a/ios/LChat/Views/StockChatFeed.swift b/ios/LChat/Views/StockChatFeed.swift
index b60a95d..84eed5b 100644
--- a/ios/LChat/Views/StockChatFeed.swift
+++ b/ios/LChat/Views/StockChatFeed.swift
@@ -1053,6 +1053,12 @@ struct StockChatFeed: UIViewControllerRepresentable {
items.append(.init(title: "Reply…",
icon: "arrowshape.turn.up.left", handler: reply))
}
+ // Right after Reply, same gating (thread-pane spec §3.5): the
+ // same reply glyph, the label differentiates.
+ if let inThread = actions.onReplyInThread {
+ items.append(.init(title: "Reply in thread…",
+ icon: "arrowshape.turn.up.left", handler: inThread))
+ }
if let copy = actions.onCopy {
items.append(.init(title: "Copy", icon: "doc.on.doc", handler: copy))
}