Factor out client history & delete node buttons, and tidy up the available actions in the list & detail screen

This commit is contained in:
Blake McAnally 2024-07-08 19:21:01 -05:00
parent 7a7a225b8e
commit 4c448e08b5
5 changed files with 204 additions and 121 deletions

View file

@ -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 = "<group>"; };
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangePositionsButton.swift; sourceTree = "<group>"; };
2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteButton.swift; sourceTree = "<group>"; };
2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientHistoryButton.swift; sourceTree = "<group>"; };
251926912C3CB52300249DF5 /* DeleteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteNodeButton.swift; sourceTree = "<group>"; };
25AECD4E2C2F723200862C8E /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
6D825E612C34786C008DBEE4 /* CommonRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonRegex.swift; sourceTree = "<group>"; };
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
@ -486,6 +490,8 @@
251926892C3BB1B200249DF5 /* ExchangePositionsButton.swift */,
251926862C3BAE2200249DF5 /* NodeAlertsButton.swift */,
2519268B2C3BB52000249DF5 /* TraceRouteButton.swift */,
2519268F2C3CB44900249DF5 /* ClientHistoryButton.swift */,
251926912C3CB52300249DF5 /* DeleteNodeButton.swift */,
);
path = Actions;
sourceTree = "<group>";
@ -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 */,

View file

@ -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.")
}
}
}

View file

@ -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)")
}
}
}
}
}

View file

@ -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
)
}
}
}

View file

@ -13,9 +13,6 @@ struct NodeList: View {
@StateObject var appState = AppState.shared
@State private var columnVisibility = NavigationSplitViewVisibility.all
@State private var selectedNode: NodeInfoEntity?
@State private var isPresentingClientHistorySentAlert = false
@State private var isPresentingDeleteNodeAlert = false
@State private var deleteNodeId: Int64 = 0
@State private var searchText = ""
@State private var viaLora = true
@State private var viaMqtt = true
@ -35,92 +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<NodeInfoEntity>
@ViewBuilder
func contextMenuActions(
node: NodeInfoEntity,
connectedNode: NodeInfoEntity?
) -> some View {
FavoriteNodeButton(
bleManager: bleManager,
context: context,
node: node
)
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) {
// HStack {
// Button("Open Node") {
// UIApplication
// .shared
// .open(URL(string: "meshtastic://nodes?nodeNum=530606484")!)
// }
// }
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 +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 {
@ -294,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)