From af0d189774ef7db4054950b4fa0490e105fbc144 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 09:07:47 +0100 Subject: [PATCH] fix(ios): stop repeated gateway errors from wobbling (#110493) * fix(ios): keep gateway problem toast stationary * chore(ios): sync native i18n inventory --- apps/.i18n/native-source.json | 54 +++++++++---------- apps/ios/Sources/Model/NodeAppModel.swift | 2 +- apps/ios/Sources/RootTabs.swift | 32 ++--------- ...tTabsSourceGuardTests+GatewaySupport.swift | 7 +-- 4 files changed, 35 insertions(+), 60 deletions(-) diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index dbf73baa7384..77c835512dea 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -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", diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 116fac4fc3e2..a6879a2686dd 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -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 } } diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index b94abe36be2c..01bc79344342 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -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 diff --git a/apps/ios/Tests/RootTabsSourceGuardTests+GatewaySupport.swift b/apps/ios/Tests/RootTabsSourceGuardTests+GatewaySupport.swift index cabc12e3c3f6..824800da3fdb 100644 --- a/apps/ios/Tests/RootTabsSourceGuardTests+GatewaySupport.swift +++ b/apps/ios/Tests/RootTabsSourceGuardTests+GatewaySupport.swift @@ -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()"))