fix(ios): auto-scroll to the reply when sending with the keyboard open (#110944)

This commit is contained in:
Peter Steinberger
2026-07-18 21:41:47 +01:00
committed by GitHub
parent a9fb5b4e3f
commit 019e42e013
3 changed files with 63 additions and 15 deletions

View File

@@ -37947,7 +37947,7 @@
},
{
"kind": "ui-call",
"line": 504,
"line": 513,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Retry Send",
"surface": "apple",
@@ -37955,7 +37955,7 @@
},
{
"kind": "ui-call",
"line": 518,
"line": 527,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Delete",
"surface": "apple",
@@ -37963,7 +37963,7 @@
},
{
"kind": "ui-call",
"line": 552,
"line": 561,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Stop Listening",
"surface": "apple",
@@ -37971,7 +37971,7 @@
},
{
"kind": "ui-call",
"line": 555,
"line": 564,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Listen",
"surface": "apple",
@@ -37979,7 +37979,7 @@
},
{
"kind": "ui-modifier",
"line": 650,
"line": 659,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Jump to latest reply",
"surface": "apple",
@@ -37987,7 +37987,7 @@
},
{
"kind": "ui-named-argument",
"line": 670,
"line": 679,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Refresh",
"surface": "apple",
@@ -37995,7 +37995,7 @@
},
{
"kind": "ui-call",
"line": 1057,
"line": 1074,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Copy Message",
"surface": "apple",
@@ -38003,7 +38003,7 @@
},
{
"kind": "ui-call",
"line": 1080,
"line": 1097,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Open Full Message",
"surface": "apple",
@@ -38011,7 +38011,7 @@
},
{
"kind": "ui-localized-call",
"line": 1101,
"line": 1118,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Reply",
"surface": "apple",
@@ -38019,7 +38019,7 @@
},
{
"kind": "ui-localized-call",
"line": 1111,
"line": 1128,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "You",
"surface": "apple",
@@ -38027,7 +38027,7 @@
},
{
"kind": "ui-localized-call",
"line": 1113,
"line": 1130,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Assistant",
"surface": "apple",
@@ -38035,7 +38035,7 @@
},
{
"kind": "ui-call",
"line": 1179,
"line": 1196,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Loading chat",
"surface": "apple",
@@ -38043,7 +38043,7 @@
},
{
"kind": "ui-modifier",
"line": 1259,
"line": 1276,
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
"source": "Dismiss",
"surface": "apple",

View File

@@ -353,6 +353,37 @@ final class OpenClawSnapshotUITests: XCTestCase {
XCTAssertTrue(self.app?.keyboards.firstMatch.waitForNonExistence(timeout: 3) == true)
}
func testKeyboardOpenSendFollowsLiveEdge() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone keyboard proof only")
self.launchApp(for: ScreenshotTarget(
initialTab: "chat",
initialDestination: "chat",
name: "keyboard-follow"))
let app = try XCTUnwrap(self.app)
let input = app.textFields["chat-message-input"]
XCTAssertTrue(input.waitForExistence(timeout: 8))
input.tap()
input.typeText(
"Give me a long, detailed status update covering the release plan, review feedback, " +
"open follow-ups, and the next steps for the team.")
let send = app.buttons["chat-send-message"]
XCTAssertTrue(send.waitForExistence(timeout: 5))
send.tap()
// Regression proof for #108692: with the keyboard still up, the reply must scroll into
// view above the keyboard on its own no jump affordance, no manual scrolling.
let keyboard = app.keyboards.firstMatch
XCTAssertTrue(keyboard.exists)
let reply = app.staticTexts.matching(
NSPredicate(format: "label CONTAINS %@", "keep the mobile workflow connected to the gateway"))
.firstMatch
XCTAssertTrue(reply.waitForExistence(timeout: 8))
Thread.sleep(forTimeInterval: 1.0)
XCTAssertLessThanOrEqual(reply.frame.maxY, keyboard.frame.minY + 1)
XCTAssertFalse(app.buttons["Jump to latest reply"].exists)
}
func testChatPresentationInLightAppearance() throws {
try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone chat proof only")
self.launchApp(

View File

@@ -117,6 +117,7 @@ public struct OpenClawChatView: View {
@State private var followTarget: ScrollFollowTarget? = .latest
@State private var isAtLiveEdge = true
@State private var isUserScrolling = false
@State private var isKeyboardVisible = false
@State private var fullMessageRequest: ChatFullMessageReaderRequest?
private let showsSessionSwitcher: Bool
private let drawsBackground: Bool
@@ -400,6 +401,14 @@ public struct OpenClawChatView: View {
.onChange(of: self.viewModel.timelineRevision) { _, _ in
self.handleTimelineChange()
}
#if canImport(UIKit) && !os(macOS)
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
self.isKeyboardVisible = true
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
self.isKeyboardVisible = false
}
#endif
}
@ViewBuilder
@@ -848,9 +857,17 @@ public struct OpenClawChatView: View {
return
case let .added(latestUserMessageID):
self.lastUserMessageID = latestUserMessageID
self.followTarget = .user(latestUserMessageID)
self.hasNewerContentBelow = false
self.moveScrollPosition(to: latestUserMessageID, anchor: Layout.newTurnAnchor)
// The anchored-question layout assumes a viewport tall enough to read the turn
// below the anchor. With the keyboard up that space is gone and the reply streams
// straight past the fold (#108692), so follow the live edge instead.
if self.isKeyboardVisible {
self.followTarget = .latest
self.moveScrollPosition(to: self.scrollerBottomID)
} else {
self.followTarget = .user(latestUserMessageID)
self.moveScrollPosition(to: latestUserMessageID, anchor: Layout.newTurnAnchor)
}
return
case .unchanged:
break