diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0a272ecd9..66efac742 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -330,6 +330,9 @@ }, "Acknowledged by another node" : { + }, + "Actions" : { + }, "Active" : { @@ -400,6 +403,9 @@ }, "Add Channels" : { + }, + "Add to favorites" : { + }, "Additional help" : { @@ -8201,6 +8207,9 @@ }, "Help with App Development" : { + }, + "Hide alerts" : { + }, "Hide Alerts" : { @@ -17150,6 +17159,9 @@ }, "Remove" : { + }, + "Remove from favorites" : { + }, "Replace Channels" : { @@ -19618,6 +19630,9 @@ }, "Short Name: %@" : { + }, + "Show alerts" : { + }, "Show Alerts" : { @@ -20860,7 +20875,7 @@ "This conversation will be deleted." : { }, - "This could take a while, response will appear in the trace route log for the node it was sent to." : { + "This could take a while. The response will appear in the trace route log for the node it was sent to." : { }, "This determines the actual frequency you are transmitting on in the band. If set to 0 this value will be calculated automatically based on the primary channel name." : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index fa0de4c2e..4b1659449 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,6 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */; }; + 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 */; }; @@ -222,6 +228,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNodeButton.swift; sourceTree = ""; }; + 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 = ""; }; @@ -471,6 +483,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 251926882C3BAF2E00249DF5 /* Actions */ = { + isa = PBXGroup; + children = ( + 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */, + 251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */, + 251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */, + 2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */, + 2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */, + 251926912C3CB52300249DF5 /* DeleteNodeButton.swift */, + ); + path = Actions; + sourceTree = ""; + }; C9483F6B2773016700998F6B /* MapKitMap */ = { isa = PBXGroup; children = ( @@ -874,6 +899,7 @@ DDDB26452AACC0B7003AFCB7 /* NodeInfoItem.swift */, DDDB26412AABF655003AFCB7 /* NodeListItem.swift */, DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */, + 251926882C3BAF2E00249DF5 /* Actions */, ); path = Helpers; sourceTree = ""; @@ -1112,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 */, @@ -1168,6 +1195,7 @@ DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */, DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */, DDDB444629F8A96500EE2349 /* Character.swift in Sources */, + 2519268C2C3BB52000249DF5 /* TraceRouteButton.swift in Sources */, DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */, DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */, DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */, @@ -1188,6 +1216,7 @@ DDB6ABE028B13AC700384BA1 /* DeviceEnums.swift in Sources */, DD86D40C287F401000BAEB7A /* SaveChannelQRCode.swift in Sources */, D93068DD2B81CA820066FBC8 /* ConfigHeader.swift in Sources */, + 251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */, DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */, D9BC22DB2B7DE8E2006A37D5 /* TileDownloadStatus.swift in Sources */, DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */, @@ -1202,6 +1231,7 @@ DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */, DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */, + 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */, D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */, DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */, @@ -1213,9 +1243,11 @@ 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 */, + 2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */, DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */, DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */, diff --git a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved index 10db40a75..ae36a98be 100644 --- a/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "74b3ad6215f078d89f4436b6ce0e318f145842efa3453bbe055ab76057de7d6b", + "originHash" : "c5be9820b6e5add3da0e3bd134c3826b3eece5f926d667cb3800a26572f9e63c", "pins" : [ { "identity" : "cocoamqtt", 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/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift new file mode 100644 index 000000000..ed5fce470 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift @@ -0,0 +1,35 @@ +import CoreData +import SwiftUI + +struct ExchangePositionsButton: View { + var bleManager: BLEManager + + var node: NodeInfoEntity + + @State + private var isPresentingPositionSentAlert: Bool = false + + var body: some View { + Button { + isPresentingPositionSentAlert = bleManager.sendPosition( + channel: node.channel, + destNum: node.num, + wantResponse: true + ) + } label: { + Label { + Text("Exchange Positions") + } icon: { + Image(systemName: "arrow.triangle.2.circlepath") + .symbolRenderingMode(.hierarchical) + } + }.alert( + "Position Sent", + isPresented: $isPresentingPositionSentAlert + ) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("Your position has been sent with a request for a response with their position.") + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift new file mode 100644 index 000000000..096674c74 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Actions/FavoriteNodeButton.swift @@ -0,0 +1,45 @@ +import CoreData +import OSLog +import SwiftUI + +struct FavoriteNodeButton: View { + var bleManager: BLEManager + var context: NSManagedObjectContext + + @ObservedObject + var node: NodeInfoEntity + + var body: some View { + Button { + guard let connectedNodeNum = bleManager.connectedPeripheral?.num else { return } + let success = if node.favorite { + bleManager.removeFavoriteNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } else { + bleManager.setFavoriteNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } + if success { + node.favorite = !node.favorite + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save Node Favorite Error") + } + Logger.data.debug("Favorited a node") + } + } label: { + Label { + Text(node.favorite ? "Remove from favorites" : "Add to favorites") + } icon: { + Image(systemName: node.favorite ? "star.fill" : "star") + .symbolRenderingMode(.multicolor) + } + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift new file mode 100644 index 000000000..663d2900e --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Actions/NodeAlertsButton.swift @@ -0,0 +1,33 @@ +import CoreData +import OSLog +import SwiftUI + +struct NodeAlertsButton: View { + var context: NSManagedObjectContext + + @ObservedObject + var node: NodeInfoEntity + + @ObservedObject + var user: UserEntity + + var body: some View { + Button { + user.mute = !user.mute + context.refresh(node, mergeChanges: true) + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save User Mute Error") + } + } label: { + Label { + Text(user.mute ? "Show alerts" : "Hide alerts") + } icon: { + Image(systemName: user.mute ? "bell.slash" : "bell") + .symbolRenderingMode(.hierarchical) + } + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift new file mode 100644 index 000000000..64e2563aa --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Actions/TraceRouteButton.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct TraceRouteButton: View { + var bleManager: BLEManager + + var node: NodeInfoEntity + + @State + private var isPresentingTraceRouteSentAlert: Bool = false + + var body: some View { + Button { + isPresentingTraceRouteSentAlert = bleManager.sendTraceRouteRequest( + destNum: node.user?.num ?? 0, + wantResponse: true + ) + } label: { + Label { + Text("Trace Route") + } icon: { + Image(systemName: "signpost.right.and.left") + .symbolRenderingMode(.hierarchical) + } + }.alert( + "Trace Route Sent", + isPresented: $isPresentingTraceRouteSentAlert + ) { + Button("OK") { }.keyboardShortcut(.defaultAction) + } message: { + Text("This could take a while. The response will appear in the trace route log for the node it was sent to.") + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 03cd132f9..74bab3010 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -16,9 +16,49 @@ 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 { + let connectedNodeNum = bleManager.connectedPeripheral?.num ?? 0 + return Button { + let success = if node.favorite { + bleManager.removeFavoriteNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } else { + bleManager.setFavoriteNode( + node: node, + connectedNodeNum: Int64(connectedNodeNum) + ) + } + if success { + node.favorite = !node.favorite + do { + try context.save() + } catch { + context.rollback() + Logger.data.error("Save Node Favorite Error") + } + Logger.data.debug("Favorited a node") + } + } label: { + Label { + Text(node.favorite ? "Remove from favorites" : "Add to favorites") + } icon: { + Image(systemName: node.favorite ? "star.fill" : "star") + .symbolRenderingMode(.multicolor) + } + } + } + var body: some View { NavigationStack { List { @@ -182,6 +222,52 @@ struct NodeDetail: View { } } + Section("Actions") { + FavoriteNodeButton( + bleManager: bleManager, + context: context, + node: node + ) + + 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 let connectedNode { + if node.isStoreForwardRouter { + ClientHistoryButton( + bleManager: bleManager, + connectedNode: connectedNode, + node: node + ) + } + + DeleteNodeButton( + bleManager: bleManager, + context: context, + connectedNode: connectedNode, + node: node + ) + } + } + } + if let metadata = node.metadata, let connectedNode, self.bleManager.connectedPeripheral != nil { @@ -207,43 +293,47 @@ struct NodeDetail: View { } if metadata.canShutdown { - Label("Power Off", systemImage: "power") - .onTapGesture { - showingShutdownConfirm = true - } - .confirmationDialog( - "are.you.sure", - isPresented: $showingShutdownConfirm - ) { - Button("Shutdown Node?", role: .destructive) { - if !bleManager.sendShutdown( - fromUser: connectedNode.user!, - toUser: node.user!, - adminIndex: connectedNode.myInfo!.adminIndex - ) { - Logger.mesh.warning("Shutdown Failed") - } - } - } - } - Label("reboot", systemImage: "arrow.triangle.2.circlepath") - .onTapGesture { - showingRebootConfirm = true - } - .confirmationDialog( + Button { + showingShutdownConfirm = true + } label: { + Label("Power Off", systemImage: "power") + }.confirmationDialog( "are.you.sure", - isPresented: $showingRebootConfirm + isPresented: $showingShutdownConfirm ) { - Button("reboot.node", role: .destructive) { - if !bleManager.sendReboot( + Button("Shutdown Node?", role: .destructive) { + if !bleManager.sendShutdown( fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo!.adminIndex ) { - Logger.mesh.warning("Reboot Failed") + Logger.mesh.warning("Shutdown Failed") } } } + } + + Button { + showingRebootConfirm = true + } label: { + Label( + "reboot", + systemImage: "arrow.triangle.2.circlepath" + ) + }.confirmationDialog( + "are.you.sure", + isPresented: $showingRebootConfirm + ) { + Button("reboot.node", role: .destructive) { + if !bleManager.sendReboot( + fromUser: connectedNode.user!, + toUser: node.user!, + adminIndex: connectedNode.myInfo!.adminIndex + ) { + Logger.mesh.warning("Reboot Failed") + } + } + } } } } diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index f4c25ac6f..096261ad7 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -13,11 +13,6 @@ struct NodeList: View { @StateObject var appState = AppState.shared @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity? - @State private var isPresentingTraceRouteSentAlert = false - @State private var isPresentingClientHistorySentAlert = false - @State private var isPresentingDeleteNodeAlert = false - @State private var isPresentingPositionSentAlert = false - @State private var deleteNodeId: Int64 = 0 @State private var searchText = "" @State private var viaLora = true @State private var viaMqtt = true @@ -37,159 +32,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 + ) + } + } - let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) + 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)) + NodeListItem( + node: node, + connected: bleManager.connectedPeripheral?.num ?? -1 == node.num, + connectedNode: bleManager.connectedPeripheral?.num ?? -1 + ) .contextMenu { - - Button { - if !node.favorite { - - let success = bleManager.setFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum)) - if success { - node.favorite = !node.favorite - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save Node Favorite Error") - } - Logger.data.debug("Favorited a node") - } - } else { - let success = bleManager.removeFavoriteNode(node: node, connectedNodeNum: Int64(connectedNodeNum)) - if success { - node.favorite = !node.favorite - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save Node Favorite Error") - } - Logger.data.debug("Favorited a node") - } - } - - } label: { - Label(node.favorite ? "Un-Favorite" : "Favorite", systemImage: node.favorite ? "star.slash.fill" : "star.fill") - } - if node.user != nil { - Button { - node.user!.mute = !node.user!.mute - context.refresh(node, mergeChanges: true) - do { - try context.save() - } catch { - context.rollback() - Logger.data.error("Save User Mute Error") - } - } label: { - Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash") - } - if bleManager.connectedPeripheral != nil && node.num != connectedNodeNum { - Button { - let positionSent = bleManager.sendPosition( - channel: node.channel, - destNum: node.num, - wantResponse: true - ) - if positionSent { - isPresentingPositionSentAlert = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - isPresentingPositionSentAlert = false - } - } - } label: { - Label("Exchange Positions", systemImage: "arrow.triangle.2.circlepath") - } - } - if bleManager.connectedPeripheral != nil && connectedNodeNum != node.num { - Button { - let success = bleManager.sendTraceRouteRequest(destNum: node.user?.num ?? 0, wantResponse: true) - if success { - isPresentingTraceRouteSentAlert = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - isPresentingTraceRouteSentAlert = false - } - } - - } label: { - Label("Trace Route", systemImage: "signpost.right.and.left") - } - if node.isStoreForwardRouter { - - Button { - let success = bleManager.requestStoreAndForwardClientHistory(fromUser: connectedNode!.user!, toUser: node.user!) - if success { - isPresentingClientHistorySentAlert = true - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - isPresentingClientHistorySentAlert = false - } - } - } 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( - "Position Sent", - isPresented: $isPresentingPositionSentAlert - ) { - Button("OK") { }.keyboardShortcut(.defaultAction) - } message: { - Text("Your position has been sent with a request for a response with their position.") - } - .alert( - "Trace Route Sent", - isPresented: $isPresentingTraceRouteSentAlert - ) { - Button("OK") { }.keyboardShortcut(.defaultAction) - } message: { - Text("This could take a while, response will appear in the trace route log for the node it was sent to.") - } - .alert( - "Client History Request Sent", - isPresented: $isPresentingClientHistorySentAlert - ) { - Button("OK") { }.keyboardShortcut(.defaultAction) - } message: { - Text("Any missed messages will be delivered again.") + contextMenuActions( + node: node, + connectedNode: connectedNode + ) } } .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 { @@ -204,67 +112,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 { @@ -363,7 +262,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)