From 4d68dc99a7b6b33e7bdefc6665f02e0645087687 Mon Sep 17 00:00:00 2001 From: Blake McAnally Date: Mon, 8 Jul 2024 19:21:01 -0500 Subject: [PATCH] Factor out client history & delete node buttons, and tidy up the available actions in the list & detail screen --- Meshtastic.xcodeproj/project.pbxproj | 8 + .../Helpers/Actions/ClientHistoryButton.swift | 33 +++ .../Helpers/Actions/DeleteNodeButton.swift | 51 +++++ .../Views/Nodes/Helpers/NodeDetail.swift | 25 ++- Meshtastic/Views/Nodes/NodeList.swift | 205 ++++++++---------- 5 files changed, 204 insertions(+), 118 deletions(-) create mode 100644 Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift create mode 100644 Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index aa1d3c857..4b1659449 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */; }; 2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */; }; 2519268C2C3BB52000249DF5 /* TraceRouteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */; }; + 251926902C3CB44900249DF5 /* ClientHistoryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */; }; + 251926922C3CB52300249DF5 /* DeleteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926912C3CB52300249DF5 /* DeleteNodeButton.swift */; }; 259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */; }; 259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; }; 259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */; }; @@ -230,6 +232,8 @@ 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeAlertsButton.swift; sourceTree = ""; }; 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangePositionsButton.swift; sourceTree = ""; }; 2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteButton.swift; sourceTree = ""; }; + 2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientHistoryButton.swift; sourceTree = ""; }; + 251926912C3CB52300249DF5 /* DeleteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNodeButton.swift; sourceTree = ""; }; 25AECD4E2C2F723200862C8E /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 6D825E612C34786C008DBEE4 /* CommonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRegex.swift; sourceTree = ""; }; 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = ""; }; @@ -486,6 +490,8 @@ 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */, 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */, 2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */, + 2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */, + 251926912C3CB52300249DF5 /* DeleteNodeButton.swift */, ); path = Actions; sourceTree = ""; @@ -1132,6 +1138,7 @@ DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */, DD41A61529AB0035003C5A37 /* NodeWeatherForecast.swift in Sources */, DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */, + 251926902C3CB44900249DF5 /* ClientHistoryButton.swift in Sources */, DDD5BB102C285FB3007E03CA /* AppLogFilter.swift in Sources */, DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */, DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */, @@ -1236,6 +1243,7 @@ DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */, DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */, DD268D8E2BCC90E2008073AE /* RouteEnums.swift in Sources */, + 251926922C3CB52300249DF5 /* DeleteNodeButton.swift in Sources */, DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */, DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift new file mode 100644 index 000000000..291eb336a --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ClientHistoryButton.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct ClientHistoryButton: View { + var bleManager: BLEManager + + var connectedNode: NodeInfoEntity + + var node: NodeInfoEntity + + @State + private var isPresentingAlert = false + + var body: some View { + Button { + isPresentingAlert = bleManager.requestStoreAndForwardClientHistory( + fromUser: connectedNode.user!, + toUser: node.user! + ) + } label: { + Label( + "Client History", + systemImage: "envelope.arrow.triangle.branch" + ) + }.alert( + "Client History Request Sent", + isPresented: $isPresentingAlert + ) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Any missed messages will be delivered again.") + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift new file mode 100644 index 000000000..247a43807 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Actions/DeleteNodeButton.swift @@ -0,0 +1,51 @@ +import CoreData +import OSLog +import SwiftUI + +struct DeleteNodeButton: View { + var bleManager: BLEManager + + var context: NSManagedObjectContext + + var connectedNode: NodeInfoEntity + + var node: NodeInfoEntity + + @State + private var isPresentingAlert = false + + var body: some View { + Button(role: .destructive) { + isPresentingAlert = true + } label: { + Label { + Text("Delete Node") + } icon: { + Image(systemName: "trash") + .symbolRenderingMode(.multicolor) + } + } + .confirmationDialog( + "are.you.sure", + isPresented: $isPresentingAlert, + titleVisibility: .visible + ) { + Button("Delete Node", role: .destructive) { + guard let deleteNode = getNodeInfo( + id: node.num, + context: context + ) else { + Logger.data.error("Unable to find node info to delete node \(node.num)") + return + } + let success = bleManager.removeNode( + node: deleteNode, + connectedNodeNum: connectedNode.num + ) + if !success { + Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "unknown".localized)") + } + } + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index c18a11657..74bab3010 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -16,7 +16,13 @@ struct NodeDetail: View { @State private var showingShutdownConfirm: Bool = false @State private var showingRebootConfirm: Bool = false - @ObservedObject var node: NodeInfoEntity + // The node the device is currently connected to + var connectedNode: NodeInfoEntity? + + // The node information being displayed on the detail screen + @ObservedObject + var node: NodeInfoEntity + var columnVisibility = NavigationSplitViewVisibility.all var favoriteNodeAction: some View { @@ -242,6 +248,23 @@ struct NodeDetail: View { bleManager: bleManager, node: node ) + + if let connectedNode { + if node.isStoreForwardRouter { + ClientHistoryButton( + bleManager: bleManager, + connectedNode: connectedNode, + node: node + ) + } + + DeleteNodeButton( + bleManager: bleManager, + context: context, + connectedNode: connectedNode, + node: node + ) + } } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index ab69e9984..cde4fd574 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -35,92 +35,72 @@ struct NodeList: View { @EnvironmentObject var bleManager: BLEManager @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "favorite", ascending: false), - NSSortDescriptor(key: "lastHeard", ascending: false), - NSSortDescriptor(key: "user.longName", ascending: true)], - animation: .default) - + sortDescriptors: [ + NSSortDescriptor(key: "favorite", ascending: false), + NSSortDescriptor(key: "lastHeard", ascending: false), + NSSortDescriptor(key: "user.longName", ascending: true), + ], + animation: .default + ) var nodes: FetchedResults - var body: some View { - NavigationSplitView(columnVisibility: $columnVisibility) { + @ViewBuilder + func contextMenuActions( + node: NodeInfoEntity, + connectedNode: NodeInfoEntity? + ) -> some View { + FavoriteNodeButton( + bleManager: bleManager, + context: context, + node: node + ) -// HStack { -// Button("Open Node") { -// UIApplication -// .shared -// .open(URL(string: "meshtastic://nodes?nodeNum=530606484")!) -// } -// } + if let user = node.user { + NodeAlertsButton( + context: context, + node: node, + user: user + ) + } + if let connectedNode { + DeleteNodeButton( + bleManager: bleManager, + context: context, + connectedNode: connectedNode, + node: node + ) + } + } + var body: some View { + NavigationSplitView(columnVisibility: $columnVisibility) { let connectedNodeNum = Int(bleManager.connectedPeripheral?.num ?? 0) let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) List(nodes, id: \.self, selection: $selectedNode) { node in - NodeListItem( node: node, - connected: bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral?.num ?? -1 == node.num, - connectedNode: (bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? -1 : -1) + connected: bleManager.connectedPeripheral?.num ?? -1 == node.num, + connectedNode: bleManager.connectedPeripheral?.num ?? -1 ) .contextMenu { - FavoriteNodeButton( - bleManager: bleManager, - context: context, - node: node + contextMenuActions( + node: node, + connectedNode: connectedNode ) - - if let user = node.user { - NodeAlertsButton( - context: context, - node: node, - user: user - ) - - if let connectedPeripheral = bleManager.connectedPeripheral, - node.num != connectedPeripheral.num { - - ExchangePositionsButton( - bleManager: bleManager, - node: node - ) - - TraceRouteButton( - bleManager: bleManager, - node: node - ) - - if node.isStoreForwardRouter { - Button { - let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!) - if success { - isPresentingClientHistorySentAlert = true - } - } label: { - Label("Client History", systemImage: "envelope.arrow.triangle.branch") - } - } - } - if bleManager.connectedPeripheral != nil { - Button(role: .destructive) { - deleteNodeId = node.num - isPresentingDeleteNodeAlert = true - } label: { - Label("Delete Node", systemImage: "trash") - } - } - } - } - .alert( - "Client History Request Sent", - isPresented: $isPresentingClientHistorySentAlert - ) { - Button("OK") { }.keyboardShortcut(.defaultAction) - } message: { - Text("Any missed messages will be delivered again.") } } .sheet(isPresented: $isEditingFilters) { - NodeListFilter(viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) + NodeListFilter( + viaLora: $viaLora, + viaMqtt: $viaMqtt, + isOnline: $isOnline, + isFavorite: $isFavorite, + distanceFilter: $distanceFilter, + maximumDistance: $maxDistance, + hopsAway: $hopsAway, + roleFilter: $roleFilter, + deviceRoles: $deviceRoles + ) } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { @@ -135,67 +115,58 @@ struct NodeList: View { .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - } .controlSize(.regular) .padding(5) } .padding(.bottom, 5) .searchable(text: $searchText, placement: .automatic, prompt: "Find a node") - .disableAutocorrection(true) - .scrollDismissesKeyboard(.immediately) + .disableAutocorrection(true) + .scrollDismissesKeyboard(.immediately) .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) - .listStyle(.plain) - .confirmationDialog( - - "are.you.sure", - isPresented: $isPresentingDeleteNodeAlert, - titleVisibility: .visible - ) { - Button("Delete Node") { - let deleteNode = getNodeInfo(id: deleteNodeId, context: context) - if connectedNode != nil { - if deleteNode != nil { - let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(connectedNodeNum)) - if !success { - Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)") - } - } - } - } - } .navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500) - .navigationBarItems(leading: - MeshtasticLogo(), - trailing: - ZStack { + .navigationBarItems( + leading: MeshtasticLogo(), + trailing: ZStack { ConnectedDevice( bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) - }) + name: bleManager.connectedPeripheral?.shortName ?? "?", + phoneOnly: true + ) + } + ) } content: { if let node = selectedNode { NavigationStack { - NodeDetail(node: node, columnVisibility: columnVisibility) - .edgesIgnoringSafeArea([.leading, .trailing]) - .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) - .navigationBarItems( - trailing: - ZStack { - if UIDevice.current.userInterfaceIdiom != .phone { - Button { - columnVisibility = .detailOnly - } label: { - Image(systemName: "rectangle") - } + NodeDetail( + connectedNode: nodes.first(where: { + let connectedNodeNum = Int(bleManager.connectedPeripheral?.num ?? 0) + return $0.num == connectedNodeNum + }), + node: node, + columnVisibility: columnVisibility + ) + .edgesIgnoringSafeArea([.leading, .trailing]) + .navigationBarTitle(String(node.user?.longName ?? "unknown".localized), displayMode: .inline) + .navigationBarItems( + trailing: ZStack { + if UIDevice.current.userInterfaceIdiom != .phone { + Button { + columnVisibility = .detailOnly + } label: { + Image(systemName: "rectangle") } - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", phoneOnly: true) - }) + } + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?", + phoneOnly: true + ) + } + ) } } else { @@ -294,7 +265,7 @@ struct NodeList: View { } } - private func searchNodeList() async -> Void { + private func searchNodeList() async { /// Case Insensitive Search Text Predicates let searchPredicates = ["user.userId", "user.numString", "user.hwModel", "user.longName", "user.shortName"].map { property in return NSPredicate(format: "%K CONTAINS[c] %@", property, searchText)