From d43b501b484d697bdbb1ed5450a20083269389da Mon Sep 17 00:00:00 2001 From: Reactor Scram Date: Tue, 20 Aug 2024 11:44:00 -0500 Subject: [PATCH] fix(apple): show "Loading Resources..." instead of "No Resources" while loading (#6358) Closes #6356 --------- Signed-off-by: Reactor Scram Co-authored-by: Thomas Eizinger --- .../FirezoneKit/Managers/TunnelManager.swift | 8 ++--- .../Sources/FirezoneKit/Models/Resource.swift | 14 ++++++++ .../Sources/FirezoneKit/Stores/Store.swift | 2 +- .../Sources/FirezoneKit/Views/MenuBar.swift | 32 +++++++++---------- .../FirezoneKit/Views/SessionView.swift | 8 ++--- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/TunnelManager.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/TunnelManager.swift index 18a7caa73..cb2814804 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/TunnelManager.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/TunnelManager.swift @@ -82,7 +82,7 @@ public class TunnelManager { // Cache resources on this side of the IPC barrier so we can // return them to callers when they haven't changed. - private var resourcesListCache: [Resource] = [] + private var resourcesListCache: ResourceList = ResourceList.loading // Persists our tunnel settings private var manager: NETunnelProviderManager? @@ -272,7 +272,7 @@ public class TunnelManager { updateDisabledResources() } - func fetchResources(callback: @escaping ([Resource]) -> Void) { + func fetchResources(callback: @escaping (ResourceList) -> Void) { guard session().status == .connected else { return } do { @@ -281,7 +281,7 @@ public class TunnelManager { self.resourceListHash = Data(SHA256.hash(data: data)) let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - self.resourcesListCache = (try? decoder.decode([Resource].self, from: data)) ?? [] + self.resourcesListCache = ResourceList.loaded(try! decoder.decode([Resource].self, from: data)) } callback(self.resourcesListCache) @@ -323,7 +323,7 @@ public class TunnelManager { if session.status == .disconnected { // Reset resource list on disconnect resourceListHash = Data() - resourcesListCache = [] + resourcesListCache = ResourceList.loading } await statusChangeHandler?(session.status) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift index c96ed90ca..a7a50870f 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift @@ -8,6 +8,20 @@ import Foundation +public enum ResourceList { + case loading + case loaded([Resource]) + + public func asArray() -> [Resource] { + switch self { + case .loading: + [] + case .loaded(let x): + x + } + } +} + public struct Resource: Decodable, Identifiable, Equatable { public let id: String public var name: String diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift index 0d6e36d95..a7dabf8f5 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift @@ -142,7 +142,7 @@ public final class Store: ObservableObject { // Network Extensions don't have a 2-way binding up to the GUI process, // so we need to periodically ask the tunnel process for them. - func beginUpdatingResources(callback: @escaping ([Resource]) -> Void) { + func beginUpdatingResources(callback: @escaping (ResourceList) -> Void) { Log.app.log("\(#function)") tunnelManager.fetchResources(callback: callback) diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift index 6d2d8dca7..087c1a7db 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift @@ -18,7 +18,6 @@ import SwiftUI // https://developer.apple.com/documentation/swiftui/menubarextra public final class MenuBar: NSObject, ObservableObject { private var statusItem: NSStatusItem - private var resources: [Resource] = [] // Wish these could be `[String]` but diffing between different types is tricky private var lastShownFavorites: [Resource] = [] @@ -73,20 +72,18 @@ public final class MenuBar: NSObject, ObservableObject { if status == .connected { model.store.beginUpdatingResources { newResources in - // Handle resource changes - self.populateResourceMenus(newResources) - self.handleTunnelStatusOrResourcesChanged(status: status, resources: newResources) - self.resources = newResources + // Handle resource changes + self.populateResourceMenus(newResources.asArray()) + self.handleTunnelStatusOrResourcesChanged(status: status, resources: newResources) } } else { model.store.endUpdatingResources() populateResourceMenus([]) - resources = [] } // Handle status changes self.updateStatusItemIcon(status: status) - self.handleTunnelStatusOrResourcesChanged(status: status, resources: resources) + self.handleTunnelStatusOrResourcesChanged(status: status, resources: model.resources) }).store(in: &cancellables) } @@ -325,7 +322,7 @@ public final class MenuBar: NSObject, ObservableObject { (connectingAnimationImageIndex + 1) % connectingAnimationImages.count } - private func handleTunnelStatusOrResourcesChanged(status: NEVPNStatus, resources: [Resource]?) { + private func handleTunnelStatusOrResourcesChanged(status: NEVPNStatus, resources: ResourceList) { // Update "Sign In" / "Sign Out" menu items switch status { case .invalid: @@ -405,13 +402,16 @@ public final class MenuBar: NSObject, ObservableObject { }() } - private func resourceMenuTitle(_ resources: [Resource]?) -> String { - guard let resources = resources else { return "Loading Resources..." } - - if resources.isEmpty { - return "No Resources" - } else { - return "Resources" + private func resourceMenuTitle(_ resources: ResourceList) -> String { + switch resources { + case .loading: + return "Loading Resources..." + case .loaded(let x): + if x.isEmpty { + return "No Resources" + } else { + return "Resources" + } } } @@ -654,7 +654,7 @@ public final class MenuBar: NSObject, ObservableObject { // When the user clicks to add or remove a favorite, the menu will close anyway, so just recreate the whole menu. // This avoids complex logic when changing in and out of the "nothing is favorited" special case self.populateResourceMenus([]) - self.populateResourceMenus(resources) + self.populateResourceMenus(model.resources.asArray()) } private func copyToClipboard(_ string: String) { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift index f12d7d1a0..544b8b2a3 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift @@ -12,7 +12,7 @@ import SwiftUI @MainActor public final class SessionViewModel: ObservableObject { @Published private(set) var actorName: String? = nil - @Published private(set) var resources: [Resource]? = nil + @Published private(set) var resources: ResourceList = ResourceList.loading @Published private(set) var status: NEVPNStatus? = nil @@ -67,7 +67,8 @@ struct SessionView: View { var body: some View { switch model.status { case .connected: - if let resources = model.resources { + switch model.resources { + case .loaded(let resources): if resources.isEmpty { Text("No Resources. Contact your admin to be granted access.") } else { @@ -91,10 +92,9 @@ struct SessionView: View { .navigationTitle("All Resources") } } - .listStyle(GroupedListStyle()) } - } else { + case .loading: Text("Loading Resources...") } case .connecting: