diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0c8fab99..d4d38083 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -7662,6 +7662,10 @@ } } }, + "Client Base should only favorite other nodes you control. Improper use will hurt your local mesh." : { + "comment" : "A message displayed in a confirmation dialog when trying to favorite a node as a CLIENT_BASE.", + "isCommentAutoGenerated" : true + }, "Client Hidden" : { "localizations" : { "de" : { @@ -41885,6 +41889,10 @@ } } }, + "Yes, I control this node" : { + "comment" : "A button label that appears in a confirmation sheet when favoriting a node as a CLIENT_BASE.", + "isCommentAutoGenerated" : true + }, "Yesterday" : { "localizations" : { "de" : { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index ca219430..7c928b3d 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -165,6 +165,7 @@ extension AccessoryManager { nodeMeshPacket.decoded = dataNodeMessage // Update local database with the new node info + // FUTURE: after https://github.com/meshtastic/firmware/pull/8495 is merged, `favorite: true` becomes `favorite: (connectedDeviceRole != DeviceRoles.clientBase)` upsertNodeInfoPacket(packet: nodeMeshPacket, favorite: true, context: context) } } catch { diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift index 8cc356e1..2dd5fb35 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager.swift @@ -706,6 +706,13 @@ extension AccessoryManager { return activeConnection?.device.firmwareVersion } + var connectedDeviceRole: DeviceRoles? { + guard let connectedNodeNum = activeDeviceNum else { return nil } + guard let connectedNode = getNodeInfo(id: connectedNodeNum, context: context) else { return nil } + guard let connectedNodeUser = connectedNode.user else { return nil } + return DeviceRoles(rawValue: Int(connectedNodeUser.role)) + } + func checkIsVersionSupported(forVersion: String) -> Bool { let myVersion = connectedVersion ?? "0.0.0" let supportedVersion = UserDefaults.firmwareVersion == "0.0.0" || diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift index 4ca8009b..83bac1d3 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift @@ -8,39 +8,20 @@ struct FavoriteNodeButton: View { @Environment(\.managedObjectContext) var context @ObservedObject var node: NodeInfoEntity + @State var isShowingClientBaseConfirmation = false var body: some View { + let connectedRoleIsClientBase = accessoryManager.connectedDeviceRole == DeviceRoles.clientBase Button { + // Special case for CLIENT_BASE: show confirmation when attempting to favorite a node + if connectedRoleIsClientBase && !node.favorite { + isShowingClientBaseConfirmation = true + return + } + // Normal case: perform action immediately guard let connectedNodeNum = accessoryManager.activeDeviceNum else { return } Task { - do { - if node.favorite { - try await accessoryManager.removeFavoriteNode( - node: node, - connectedNodeNum: Int64(connectedNodeNum) - ) - } else { - try await accessoryManager.setFavoriteNode( - node: node, - connectedNodeNum: Int64(connectedNodeNum) - ) - } - - Task { @MainActor in - // Update CoreData - node.favorite = !node.favorite - - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save Node Favorite Error") - } - Logger.data.debug("Favorited a node") - } - } catch { - - } + await assignFavorite(node: node, setToFavorite: !node.favorite, connectedNodeNum: Int64(connectedNodeNum)) } } label: { Label { @@ -50,5 +31,51 @@ struct FavoriteNodeButton: View { .symbolRenderingMode(.multicolor) } } + .confirmationDialog( + "Are you sure?", + isPresented: $isShowingClientBaseConfirmation, + titleVisibility: .visible + ) { + Button("Yes, I control this node") { + guard let connectedNodeNum = accessoryManager.activeDeviceNum else { return } + Task { + await assignFavorite(node: node, setToFavorite: true, connectedNodeNum: Int64(connectedNodeNum)) + } + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Client Base should only favorite other nodes you control. Improper use will hurt your local mesh.") + } + } + + private func assignFavorite (node: NodeInfoEntity, setToFavorite: Bool, connectedNodeNum: Int64) async { + do { + if setToFavorite { + try await accessoryManager.setFavoriteNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } else { + try await accessoryManager.removeFavoriteNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } + + Task { @MainActor in + // Update CoreData + node.favorite = setToFavorite + + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save Node Favorite Error") + } + Logger.data.debug("Favorited a node") + } + } catch { + + } } } diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 80fe3dba..a03c8a22 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -29,8 +29,9 @@ struct DeviceConfig: View { @State var ledHeartbeatEnabled = true @State var tripleClickAsAdHocPing = true @State var tzdef = "" - @State private var showRouterWarning = false - + @State private var showSpecialRoleWarning = false + @State private var showSpecialRoleWarningForRole: Int = 0 + var body: some View { Form { ConfigHeader(title: "Device", config: \.deviceConfig, node: node, onAppear: setDeviceValues) @@ -43,13 +44,14 @@ struct DeviceConfig: View { } } .onChange(of: deviceRole) { _, newRole in - if hasChanges && [DeviceRoles.router.rawValue, DeviceRoles.routerLate.rawValue].contains(newRole) { - showRouterWarning = true + if hasChanges && [DeviceRoles.router.rawValue, DeviceRoles.routerLate.rawValue, DeviceRoles.clientBase.rawValue].contains(newRole) { + showSpecialRoleWarningForRole = newRole + showSpecialRoleWarning = true } } .confirmationDialog( "Are you sure?", - isPresented: $showRouterWarning, + isPresented: $showSpecialRoleWarning, titleVisibility: .visible ) { @@ -60,7 +62,7 @@ struct DeviceConfig: View { setDeviceValues() } } message: { - Text("The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh.") + Text(specialRoleWarningMessage(newRole: showSpecialRoleWarningForRole)) } Text(DeviceRoles(rawValue: deviceRole)?.description ?? "") .foregroundColor(.gray) @@ -332,4 +334,14 @@ struct DeviceConfig: View { self.tzdef = node?.deviceConfig?.tzdef ?? "" hasChanges = false } + + private func specialRoleWarningMessage(newRole: Int) -> String { + if [DeviceRoles.router.rawValue, DeviceRoles.routerLate.rawValue].contains(newRole) { + return "The Router roles are only for high vantage locations like mountaintops and towers with few nearby nodes, not for use in urban areas. Improper use will hurt your local mesh." + } else if newRole == DeviceRoles.clientBase.rawValue { + return "Switching to Client Base will clear this node's favorites. Client Base should only favorite other nodes you control. Improper use will hurt your local mesh." + } else { + return "" + } + } }