fix(ios): stop repeated gateway errors from wobbling (#110493)

* fix(ios): keep gateway problem toast stationary

* chore(ios): sync native i18n inventory
This commit is contained in:
Peter Steinberger
2026-07-18 09:07:47 +01:00
committed by GitHub
parent e77140ab8e
commit af0d189774
4 changed files with 35 additions and 60 deletions
+27 -27
View File
@@ -24483,7 +24483,7 @@
},
{
"kind": "ui-call",
"line": 179,
"line": 174,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Talk",
"surface": "apple",
@@ -24491,7 +24491,7 @@
},
{
"kind": "ui-call",
"line": 192,
"line": 187,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Control",
"surface": "apple",
@@ -24499,7 +24499,7 @@
},
{
"kind": "ui-call",
"line": 201,
"line": 196,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Agent",
"surface": "apple",
@@ -24507,7 +24507,7 @@
},
{
"kind": "ui-call",
"line": 214,
"line": 209,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Settings",
"surface": "apple",
@@ -24515,7 +24515,7 @@
},
{
"kind": "ui-call",
"line": 313,
"line": 308,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "OpenClaw",
"surface": "apple",
@@ -24523,7 +24523,7 @@
},
{
"kind": "ui-localized-call",
"line": 345,
"line": 340,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "OpenClaw %@",
"surface": "apple",
@@ -24531,7 +24531,7 @@
},
{
"kind": "ui-localized-call",
"line": 352,
"line": 347,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Online",
"surface": "apple",
@@ -24539,7 +24539,7 @@
},
{
"kind": "ui-localized-call",
"line": 354,
"line": 349,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Connecting",
"surface": "apple",
@@ -24547,7 +24547,7 @@
},
{
"kind": "ui-localized-call",
"line": 356,
"line": 351,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Needs attention",
"surface": "apple",
@@ -24555,7 +24555,7 @@
},
{
"kind": "ui-localized-call",
"line": 358,
"line": 353,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Offline",
"surface": "apple",
@@ -24563,7 +24563,7 @@
},
{
"kind": "ui-named-argument",
"line": 437,
"line": 432,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Chat",
"surface": "apple",
@@ -24571,7 +24571,7 @@
},
{
"kind": "ui-named-argument",
"line": 450,
"line": 445,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Overview",
"surface": "apple",
@@ -24579,7 +24579,7 @@
},
{
"kind": "ui-named-argument",
"line": 474,
"line": 469,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Agents",
"surface": "apple",
@@ -24587,7 +24587,7 @@
},
{
"kind": "ui-named-argument",
"line": 481,
"line": 476,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Instances",
"surface": "apple",
@@ -24595,7 +24595,7 @@
},
{
"kind": "ui-named-argument",
"line": 492,
"line": 487,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Files",
"surface": "apple",
@@ -24603,7 +24603,7 @@
},
{
"kind": "ui-named-argument",
"line": 499,
"line": 494,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Dreaming",
"surface": "apple",
@@ -24611,7 +24611,7 @@
},
{
"kind": "ui-named-argument",
"line": 506,
"line": 501,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Usage",
"surface": "apple",
@@ -24619,7 +24619,7 @@
},
{
"kind": "ui-named-argument",
"line": 513,
"line": 508,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Automations",
"surface": "apple",
@@ -24627,7 +24627,7 @@
},
{
"kind": "ui-localized-call",
"line": 625,
"line": 620,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Show Sidebar",
"surface": "apple",
@@ -24635,7 +24635,7 @@
},
{
"kind": "ui-localized-call",
"line": 636,
"line": 631,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Back to %@",
"surface": "apple",
@@ -24643,7 +24643,7 @@
},
{
"kind": "ui-modifier",
"line": 661,
"line": 656,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Hide Sidebar",
"surface": "apple",
@@ -24651,7 +24651,7 @@
},
{
"kind": "ui-modifier",
"line": 788,
"line": 774,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Close canvas",
"surface": "apple",
@@ -24659,7 +24659,7 @@
},
{
"kind": "conditional-branch",
"line": 1042,
"line": 1026,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Gateway needs attention",
"surface": "apple",
@@ -24667,7 +24667,7 @@
},
{
"kind": "conditional-branch",
"line": 1042,
"line": 1026,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "OpenClaw iOS",
"surface": "apple",
@@ -24675,7 +24675,7 @@
},
{
"kind": "conditional-branch",
"line": 1078,
"line": 1062,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Available",
"surface": "apple",
@@ -24683,7 +24683,7 @@
},
{
"kind": "conditional-branch",
"line": 1078,
"line": 1062,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Gateway default",
"surface": "apple",
@@ -24691,7 +24691,7 @@
},
{
"kind": "conditional-branch",
"line": 1078,
"line": 1062,
"path": "apps/ios/Sources/RootTabs.swift",
"source": "Routed on this phone",
"surface": "apple",
+1 -1
View File
@@ -428,7 +428,7 @@ final class NodeAppModel {
var gatewayPairingPaused: Bool = false
var gatewayPairingRequestId: String?
// Bumped on every non-nil assignment, including re-reports of an equal problem;
// value equality alone cannot tell the UI to re-surface or shake the toast.
// value equality alone cannot tell the UI to re-surface a dismissed toast.
private(set) var gatewayProblemReportCount = 0
private(set) var lastGatewayProblem: GatewayConnectionProblem? {
didSet { if self.lastGatewayProblem != nil { self.gatewayProblemReportCount &+= 1 } }
+3 -29
View File
@@ -40,13 +40,8 @@ struct RootTabs: View {
@State private var presentedSheet: PresentedSheet?
@State private var showGatewayProblemDetails: Bool = false
@State private var gatewayToastDragOffset: CGFloat = 0
// Swipe-up hides the toast only until the next problem report; every report
// (even an equal problem) must re-surface it or shake the visible toast.
// Swipe-up hides the toast only until the next problem report.
@State private var isGatewayToastSwipeDismissed: Bool = false
@State private var gatewayToastShake: CGFloat = 0
// Mirror of the problem at the last handled report, used to tell a first
// appearance (animate in) from a re-report while visible (shake).
@State private var lastReportedGatewayProblem: GatewayConnectionProblem?
@State private var showOnboarding: Bool = false
@State private var onboardingAllowSkip: Bool = true
@State private var didEvaluateOnboarding: Bool = false
@@ -731,7 +726,6 @@ struct RootTabs: View {
.padding(.horizontal, 12)
.safeAreaPadding(.top, 10)
.offset(y: min(self.gatewayToastDragOffset, 0))
.modifier(GatewayToastShakeEffect(animatableData: self.gatewayToastShake))
.gesture(self.gatewayToastSwipeGesture)
// A drag cancelled by toast removal never fires onEnded; clear the
// offset so the next toast doesn't render shifted up.
@@ -756,16 +750,8 @@ struct RootTabs: View {
}
private func handleGatewayProblemReport() {
let toastWasVisible = self.lastReportedGatewayProblem != nil && !self.isGatewayToastSwipeDismissed
self.lastReportedGatewayProblem = self.appModel.lastGatewayProblem
if self.isGatewayToastSwipeDismissed {
self.isGatewayToastSwipeDismissed = false
return
}
guard toastWasVisible, self.activeGatewayProblemToast != nil else { return }
withAnimation(self.reduceMotion ? nil : .linear(duration: 0.4)) {
self.gatewayToastShake += 1
}
guard self.isGatewayToastSwipeDismissed else { return }
self.isGatewayToastSwipeDismissed = false
}
private var canvasPresentationOverlay: some View {
@@ -824,7 +810,6 @@ struct RootTabs: View {
private func rootAppearLifecycle(_ content: some View) -> some View {
content
.onAppear { self.updateIdleTimer() }
.onAppear { self.lastReportedGatewayProblem = self.appModel.lastGatewayProblem }
.onAppear { self.updateCanvasState() }
.onAppear { self.evaluateOnboardingPresentation(force: false) }
.onAppear { self.maybeAutoOpenSettings() }
@@ -857,7 +842,6 @@ struct RootTabs: View {
.onChange(of: self.appModel.lastGatewayProblem) { _, newValue in
if newValue == nil {
self.isGatewayToastSwipeDismissed = false
self.lastReportedGatewayProblem = nil
}
}
.onChange(of: self.appModel.gatewayProblemReportCount) { _, _ in
@@ -1459,16 +1443,6 @@ private struct RootTabsHomeCanvasAgentCard: Codable {
var isActive: Bool
}
/// Horizontal shake for re-reported gateway problems: three oscillations that
/// settle back to identity at integer trigger values.
private struct GatewayToastShakeEffect: GeometryEffect {
var animatableData: CGFloat
func effectValue(size _: CGSize) -> ProjectionTransform {
ProjectionTransform(CGAffineTransform(translationX: 7 * sin(self.animatableData * 6 * .pi), y: 0))
}
}
private struct RootCameraFlashOverlay: View {
var nonce: Int
@@ -99,10 +99,11 @@ extension RootTabsSourceGuardTests {
// root's only remediation surface must not depend on aggregate status.
#expect(activeProblemToast.contains("appModel.lastGatewayProblem"))
#expect(!activeProblemToast.contains("gatewayStatus"))
// Every problem report re-surfaces a swiped-away toast or shakes the
// visible one; value equality alone must not keep the toast hidden.
// Every problem report re-surfaces a swiped-away toast. Visible problem
// banners stay stationary when reconnects re-report the same failure.
#expect(rootSource.contains("self.appModel.gatewayProblemReportCount"))
#expect(rootSource.contains("GatewayToastShakeEffect"))
#expect(rootSource.contains("guard self.isGatewayToastSwipeDismissed else { return }"))
#expect(!rootSource.contains("GatewayToastShakeEffect"))
#expect(actionsSource.contains("await self.gatewayController.connectActiveGateway()"))
#expect(actionsSource.contains("self.gatewayController.refreshActiveGatewayRegistrationFromSettings()"))