From eaec51171eba71ecb97ea2ca2a05a3b63e12a7da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o=20Gon=C3=A7alves?= <5808343+bgoncal@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:26:34 +0100 Subject: [PATCH] Improve open page intent, avoid reload webview when possible (#3435) ## Summary ## Screenshots ## Link to pull request in Documentation repository Documentation: home-assistant/companion.home-assistant# ## Any other notes --- Sources/App/WebView/WebViewController.swift | 10 +++- .../App/WebView/WebViewWindowController.swift | 60 +++++++++++++++---- .../Widgets/Custom/WidgetCustom.swift | 18 +++--- .../Widgets/OpenPage/OpenPageAppIntent.swift | 28 +++------ .../Common/Extensions/URL+Extensions.swift | 17 ++++-- Sources/Shared/Environment/AppConstants.swift | 12 ++++ 6 files changed, 98 insertions(+), 47 deletions(-) diff --git a/Sources/App/WebView/WebViewController.swift b/Sources/App/WebView/WebViewController.swift index 6226d7dcf..aca0c0f1b 100644 --- a/Sources/App/WebView/WebViewController.swift +++ b/Sources/App/WebView/WebViewController.swift @@ -468,7 +468,8 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg } } - public func open(inline url: URL) { + /// avoidUnecessaryReload Avoids reloading when the URL is the same as the current one + public func open(inline url: URL, avoidUnecessaryReload: Bool = false) { loadViewIfNeeded() // these paths do not show frontend pages, and so we don't want to display them in our webview @@ -481,6 +482,13 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg ] if ignoredPaths.allSatisfy({ !url.path.hasPrefix($0) }) { + if avoidUnecessaryReload, webView.url?.isEqualIgnoringQueryParams(to: url) == true { + Current.Log + .info( + "Not reloading WebView when open(inline) was requested, URL is the same as current and avoidUnecessaryReload is true" + ) + return + } webView.load(URLRequest(url: url)) } else { openURLInBrowser(url, self) diff --git a/Sources/App/WebView/WebViewWindowController.swift b/Sources/App/WebView/WebViewWindowController.swift index 701ad05c0..ca3cb8a8f 100644 --- a/Sources/App/WebView/WebViewWindowController.swift +++ b/Sources/App/WebView/WebViewWindowController.swift @@ -117,11 +117,11 @@ final class WebViewWindowController { return currentController } - func navigate(to url: URL, on server: Server) { + func navigate(to url: URL, on server: Server, avoidUnecessaryReload: Bool = false) { open(server: server).done { webViewController in // Dismiss any overlayed controllers webViewController.dismissOverlayController(animated: true, completion: nil) - webViewController.open(inline: url) + webViewController.open(inline: url, avoidUnecessaryReload: avoidUnecessaryReload) } } @@ -212,19 +212,45 @@ final class WebViewWindowController { skipConfirm: Bool = false, queryParameters: [URLQueryItem]? = nil ) { - let serverName = queryParameters?.first(where: { $0.name == "server" })?.value + let serverNameOrId = queryParameters?.first(where: { $0.name == "server" })?.value + let avoidUnecessaryReload = { + if let avoidUnecessaryReloadString = + queryParameters?.first(where: { $0.name == "avoidUnecessaryReload" })?.value { + return Bool(avoidUnecessaryReloadString) ?? false + } else { + return false + } + }() let servers = Current.servers.all - if let first = servers.first, Current.servers.all.count == 1 || serverName != nil { - if serverName == "default" || serverName == nil { - open(from: from, server: first, urlString: openUrlRaw, skipConfirm: skipConfirm) + if let first = servers.first, Current.servers.all.count == 1 || serverNameOrId != nil { + if serverNameOrId == "default" || serverNameOrId == nil { + open( + from: from, + server: first, + urlString: openUrlRaw, + skipConfirm: skipConfirm + ) } else { if let selectedServer = servers.first(where: { server in - server.info.name.lowercased() == serverName?.lowercased() + server.info.name.lowercased() == serverNameOrId?.lowercased() || + server.identifier.rawValue == serverNameOrId }) { - open(from: from, server: selectedServer, urlString: openUrlRaw, skipConfirm: skipConfirm) + open( + from: from, + server: selectedServer, + urlString: openUrlRaw, + skipConfirm: skipConfirm, + avoidUnecessaryReload: avoidUnecessaryReload + ) } else { - open(from: from, server: first, urlString: openUrlRaw, skipConfirm: skipConfirm) + open( + from: from, + server: first, + urlString: openUrlRaw, + skipConfirm: skipConfirm, + avoidUnecessaryReload: avoidUnecessaryReload + ) } } } else if Current.servers.all.count > 1 { @@ -246,7 +272,13 @@ final class WebViewWindowController { } } - func open(from: OpenSource, server: Server, urlString openUrlRaw: String, skipConfirm: Bool = false) { + func open( + from: OpenSource, + server: Server, + urlString openUrlRaw: String, + skipConfirm: Bool = false, + avoidUnecessaryReload: Bool = false + ) { let webviewURL = server.info.connection.webviewURL(from: openUrlRaw) let externalURL = URL(string: openUrlRaw) @@ -256,7 +288,8 @@ final class WebViewWindowController { urlString: openUrlRaw, webviewURL: webviewURL, externalURL: externalURL, - skipConfirm: skipConfirm + skipConfirm: skipConfirm, + avoidUnecessaryReload: avoidUnecessaryReload ) } @@ -270,7 +303,8 @@ final class WebViewWindowController { urlString openUrlRaw: String, webviewURL: URL?, externalURL: URL?, - skipConfirm: Bool + skipConfirm: Bool, + avoidUnecessaryReload: Bool = false ) { guard webviewURL != nil || externalURL != nil else { return @@ -278,7 +312,7 @@ final class WebViewWindowController { let triggerOpen = { [self] in if let webviewURL { - navigate(to: webviewURL, on: server) + navigate(to: webviewURL, on: server, avoidUnecessaryReload: avoidUnecessaryReload) } else if let externalURL { openURLInBrowser(externalURL, presentedViewController) } diff --git a/Sources/Extensions/Widgets/Custom/WidgetCustom.swift b/Sources/Extensions/Widgets/Custom/WidgetCustom.swift index dc452f990..6153faddc 100644 --- a/Sources/Extensions/Widgets/Custom/WidgetCustom.swift +++ b/Sources/Extensions/Widgets/Custom/WidgetCustom.swift @@ -199,10 +199,11 @@ struct WidgetCustom: Widget { if path.hasPrefix("/") { path.removeFirst() } - if let url = - URL( - string: "\(AppConstants.deeplinkURL.absoluteString)navigate/\(path)?server=\(magicItem.serverId)" - ) { + if let url = AppConstants.navigateDeeplinkURL( + path: path, + serverId: magicItem.serverId, + avoidUnecessaryReload: true + ) { return .widgetURL(url) } else { return .appIntent(.refresh) @@ -211,10 +212,11 @@ struct WidgetCustom: Widget { private func assistIntent(serverId: String, pipelineId: String, startListening: Bool) -> WidgetBasicViewModel .InteractionType { - if let url = - URL( - string: "\(AppConstants.deeplinkURL.absoluteString)assist?serverId=\(serverId)&pipelineId=\(pipelineId)&startListening=\(startListening)" - ) { + if let url = AppConstants.assistDeeplinkURL( + serverId: serverId, + pipelineId: pipelineId, + startListening: startListening + ) { return .widgetURL(url) } else { return .appIntent(.refresh) diff --git a/Sources/Extensions/Widgets/OpenPage/OpenPageAppIntent.swift b/Sources/Extensions/Widgets/OpenPage/OpenPageAppIntent.swift index c4cd857dd..ab391716d 100644 --- a/Sources/Extensions/Widgets/OpenPage/OpenPageAppIntent.swift +++ b/Sources/Extensions/Widgets/OpenPage/OpenPageAppIntent.swift @@ -22,27 +22,15 @@ struct OpenPageAppIntent: AppIntent { guard let page, let server = Current.servers.all.first(where: { $0.identifier.rawValue == page.serverId }) ?? Current .servers.all.first else { return .result() } - - let urlString = "/" + page.panel.path - #if !WIDGET_EXTENSION - DispatchQueue.main.async { - if Current.isCatalyst, Current.settingsStore.macNativeFeaturesOnly { - if let activeURL = server.info.connection.activeURL(), - let pageURL = URL(string: "\(activeURL)\(urlString)") { - UIApplication.shared.open(pageURL) - } else { - Current.Log.error("Failed to open page \(urlString) on server \(server.info.name)") - } - } else { - Current.sceneManager.webViewWindowControllerPromise.done { windowController in - windowController.open( - from: .deeplink, - server: server, - urlString: urlString, - skipConfirm: true - ) - } + if let url = + AppConstants.navigateDeeplinkURL( + path: page.panel.path, + serverId: server.identifier.rawValue, + avoidUnecessaryReload: true + ) { + DispatchQueue.main.async { + UIApplication.shared.open(url) } } #endif diff --git a/Sources/Shared/Common/Extensions/URL+Extensions.swift b/Sources/Shared/Common/Extensions/URL+Extensions.swift index f81bee523..4b88d0e48 100644 --- a/Sources/Shared/Common/Extensions/URL+Extensions.swift +++ b/Sources/Shared/Common/Extensions/URL+Extensions.swift @@ -1,8 +1,8 @@ import Foundation -extension URL { +public extension URL { /// Return true if receiver's host and scheme is equal to `otherURL` - public func baseIsEqual(to otherURL: URL) -> Bool { + func baseIsEqual(to otherURL: URL) -> Bool { host?.lowercased() == otherURL.host?.lowercased() && portWithFallback == otherURL.portWithFallback && scheme?.lowercased() == otherURL.scheme?.lowercased() @@ -10,8 +10,15 @@ extension URL { && password == otherURL.password } + /// Return true if receiver's URL is equal to `otherURL` ignoring query params + func isEqualIgnoringQueryParams(to otherURL: URL) -> Bool { + baseIsEqual(to: otherURL) && + (path == otherURL.path || path == "\(otherURL.path)/0") + // Workaround for Home Assistant behavior where /0 is added to the end + } + // port will be removed if 80 or 443 by WKWebView, so we provide defaults for comparison - var portWithFallback: Int? { + internal var portWithFallback: Int? { if let port { return port } @@ -23,7 +30,7 @@ extension URL { } } - public func sanitized() -> URL { + func sanitized() -> URL { guard path.hasSuffix("/"), var components = URLComponents(url: self, resolvingAgainstBaseURL: false) else { return self @@ -36,7 +43,7 @@ extension URL { return components.url ?? self } - func adapting(url: URL) -> URL { + internal func adapting(url: URL) -> URL { guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), var futureComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { diff --git a/Sources/Shared/Environment/AppConstants.swift b/Sources/Shared/Environment/AppConstants.swift index 91afdecdd..5bfb44337 100644 --- a/Sources/Shared/Environment/AppConstants.swift +++ b/Sources/Shared/Environment/AppConstants.swift @@ -65,6 +65,18 @@ public enum AppConstants { } } + public static func navigateDeeplinkURL(path: String, serverId: String, avoidUnecessaryReload: Bool) -> URL? { + URL( + string: "\(AppConstants.deeplinkURL.absoluteString)navigate/\(path)?server=\(serverId)&avoidUnecessaryReload=\(avoidUnecessaryReload)" + ) + } + + public static func assistDeeplinkURL(serverId: String, pipelineId: String, startListening: Bool) -> URL? { + URL( + string: "\(AppConstants.deeplinkURL.absoluteString)assist?serverId=\(serverId)&pipelineId=\(pipelineId)&startListening=\(startListening)" + ) + } + /// The App Group ID used by the app and extensions for sharing data. public static var AppGroupID: String { "group." + BundleID.lowercased()