diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index 95d2dae7..a63a243f 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -718,6 +718,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = 37C534H572; ENABLE_PREVIEWS = YES; @@ -727,11 +728,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.42; + MARKETING_VERSION = 1.43; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; }; @@ -745,6 +746,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = 37C534H572; ENABLE_PREVIEWS = YES; @@ -754,11 +756,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.42; + MARKETING_VERSION = 1.43; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,6"; }; diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index 85a7c0a5..69bf1501 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -2,6 +2,7 @@ import Foundation import CoreData import CoreBluetooth import SwiftUI +import MapKit // --------------------------------------------------------------------------------------- // Meshtastic BLE Device Manager @@ -27,6 +28,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph @Published var connectedPeripheral: Peripheral! //@Published var lastConnectedPeripheral: String @Published var lastConnectionError: String + @Published var lastConnnectionVersion: String @Published var isSwitchedOn: Bool = false @Published var isScanning: Bool = false @@ -54,8 +56,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph override init() { self.meshLoggingEnabled = true // UserDefaults.standard.object(forKey: "meshActivityLog") as? Bool ?? true - //self.lastConnectedPeripheral = "" self.lastConnectionError = "" + self.lastConnnectionVersion = "0.0.0" super.init() // let bleQueue: DispatchQueue = DispatchQueue(label: "CentralManager") centralManager = CBCentralManager(delegate: self, queue: nil) @@ -419,19 +421,52 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph myInfo.myNodeNum = Int64(decodedInfo.myInfo.myNodeNum) myInfo.hasGps = decodedInfo.myInfo.hasGps_p myInfo.numBands = Int32(bitPattern: decodedInfo.myInfo.numBands) - myInfo.firmwareVersion = decodedInfo.myInfo.firmwareVersion + + // Swift does strings weird, this does work + let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1) + var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(encodedOffset:6))] + version = version.dropLast() + myInfo.firmwareVersion = String(version) + lastConnnectionVersion = String(version) + myInfo.messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec) myInfo.minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion) myInfo.maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels) connectedPeripheral.num = myInfo.myNodeNum connectedPeripheral.firmwareVersion = myInfo.firmwareVersion ?? "Unknown" - + + let fetchBCUserRequest: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") + fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(decodedInfo.myInfo.myNodeNum)) + + do { + let fetchedUser = try context?.fetch(fetchBCUserRequest) as! [UserEntity] + + if fetchedUser.isEmpty { + // Save the broadcast user if it does not exist + let bcu: UserEntity = UserEntity(context: context!) + bcu.shortName = "ALL" + bcu.longName = "All - Broadcast" + bcu.hwModel = "UNSET" + bcu.num = Int64(broadcastNodeNum) + bcu.userId = "BROADCASTNODE" + print("💾 Saved the All - Broadcast User") + } + + } catch { + + print("đŸ’Ĩ Error Saving the All - Broadcast User") + } + } else { fetchedMyInfo[0].myNodeNum = Int64(decodedInfo.myInfo.myNodeNum) fetchedMyInfo[0].hasGps = decodedInfo.myInfo.hasGps_p fetchedMyInfo[0].numBands = Int32(bitPattern: decodedInfo.myInfo.numBands) - fetchedMyInfo[0].firmwareVersion = decodedInfo.myInfo.firmwareVersion + let lastDotIndex = decodedInfo.myInfo.firmwareVersion.lastIndex(of: ".")//.lastIndex(of: ".", offsetBy: -1) + var version = decodedInfo.myInfo.firmwareVersion[...(lastDotIndex ?? String.Index(encodedOffset:6))] + version = version.dropLast() + fetchedMyInfo[0].firmwareVersion = String(version) + lastConnnectionVersion = String(version) fetchedMyInfo[0].messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec) fetchedMyInfo[0].minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion) fetchedMyInfo[0].maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels) @@ -471,7 +506,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph let newNode = NodeInfoEntity(context: context!) newNode.id = Int64(decodedInfo.nodeInfo.num) newNode.num = Int64(decodedInfo.nodeInfo.num) - if decodedInfo.nodeInfo.lastHeard != nil && decodedInfo.nodeInfo.lastHeard > 0 { + if decodedInfo.nodeInfo.lastHeard > 0 { newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.nodeInfo.lastHeard))) } else { @@ -706,7 +741,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph fetchedNode[0].id = Int64(decodedInfo.packet.from) fetchedNode[0].num = Int64(decodedInfo.packet.from) - if decodedInfo.packet.rxTime != nil && decodedInfo.packet.rxTime > 0 { + if decodedInfo.packet.rxTime > 0 { fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(decodedInfo.packet.rxTime))) } else { @@ -786,20 +821,27 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph print("đŸ’Ĩ Error Fetching NodeInfoEntity for NODEINFO_APP") } - } else if decodedInfo.packet.decoded.portnum == PortNum.adminApp { + + // + } else if decodedInfo.packet.decoded.portnum == PortNum.storeForwardApp { + + if meshLoggingEnabled { MeshLogger.log("🚨 MESH PACKET received for Store Forward App UNHANDLED \(try decodedInfo.packet.jsonString())") } + print("â„šī¸ MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())") + + } else if decodedInfo.packet.decoded.portnum == PortNum.adminApp { if meshLoggingEnabled { MeshLogger.log("🚨 MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())") } - print("🚨 MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())") + print("â„šī¸ MESH PACKET received for Admin App UNHANDLED \(try decodedInfo.packet.jsonString())") } else if decodedInfo.packet.decoded.portnum == PortNum.routingApp { if meshLoggingEnabled { MeshLogger.log("🚨 MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") } - print("🚨 MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") + print("â„šī¸ MESH PACKET received for Routing App UNHANDLED \(try decodedInfo.packet.jsonString())") } else { if meshLoggingEnabled { MeshLogger.log("🚨 MESH PACKET received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())") } - print("🚨 MESH PACKET received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())") + print("â„šī¸ MESH PACKET received for Other App UNHANDLED \(try decodedInfo.packet.jsonString())") } } catch { @@ -823,8 +865,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph peripheral.readValue(for: FROMRADIO_characteristic) } - // Send Broadcast Message - public func sendMessage(message: String, toUserNum: Int64) -> Bool { + // Send Message + public func sendMessage(message: String, toUserNum: Int64, isTapback: Bool, replyID: Int64) -> Bool { var success = false @@ -839,18 +881,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil { connectTo(peripheral: preferredPeripheral!.peripheral) } -// else { -// -// // Try and connect to the last connected device -// let lastConnectedPeripheral = peripherals.filter({ $0.peripheral.identifier.uuidString == self.lastConnectedPeripheral }).first -// if lastConnectedPeripheral != nil && lastConnectedPeripheral?.peripheral != nil { -// connectTo(peripheral: lastConnectedPeripheral!.peripheral) -// } -// } - //print("đŸšĢ Message Send Failed, not properly connected to \(lastConnectedPeripheral)") - //if meshLoggingEnabled { MeshLogger.log("đŸšĢ Message Send Failed, not properly connected to \(lastConnectedPeripheral)") } + print("đŸšĢ Message Send Failed, not properly connected to \(preferredPeripheral?.name ?? "Unknown")") + if meshLoggingEnabled { MeshLogger.log("đŸšĢ Message Send Failed, not properly connected to \(preferredPeripheral?.name ?? "Unknown")") } success = false + } else if message.count < 1 { // Don't send an empty message @@ -880,6 +915,12 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph newMessage.receivedACK = false newMessage.direction = "IN" newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum }) + newMessage.isTapback = isTapback + + if replyID > 0 { + + newMessage.replyID = replyID + } if newMessage.toUser == nil { let bcu: UserEntity = UserEntity(context: context!) @@ -890,6 +931,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph bcu.userId = "BROADCASTNODE" newMessage.toUser = bcu } + newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum }) newMessage.messagePayload = message @@ -903,6 +945,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph var meshPacket = MeshPacket() meshPacket.to = UInt32(toUserNum) meshPacket.from = UInt32(fromUserNum) + if replyID > 0 { + meshPacket.replyID = UInt32(replyID) + } meshPacket.decoded = dataMessage meshPacket.wantAck = true diff --git a/MeshtasticClient/Helpers/Extensions.swift b/MeshtasticClient/Helpers/Extensions.swift index e1493d4a..8dab6f54 100644 --- a/MeshtasticClient/Helpers/Extensions.swift +++ b/MeshtasticClient/Helpers/Extensions.swift @@ -39,6 +39,22 @@ extension String { return data } + + func image(fontSize:CGFloat = 40, bgColor:UIColor = UIColor.clear, imageSize:CGSize? = nil) -> UIImage? + { + let font = UIFont.systemFont(ofSize: fontSize) + let attributes = [NSAttributedString.Key.font: font] + let imageSize = imageSize ?? self.size(withAttributes: attributes) + + UIGraphicsBeginImageContextWithOptions(imageSize, false, 0) + bgColor.set() + let rect = CGRect(origin: .zero, size: imageSize) + UIRectFill(rect) + self.draw(in: rect, withAttributes: [.font: font]) + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image + } } diff --git a/MeshtasticClient/Info.plist b/MeshtasticClient/Info.plist index 21a3f47f..b5b09295 100644 --- a/MeshtasticClient/Info.plist +++ b/MeshtasticClient/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) ITSAppUsesNonExemptEncryption LSApplicationCategoryType diff --git a/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents b/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents index 86be9a38..75fc2789 100644 --- a/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents +++ b/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents @@ -2,7 +2,7 @@ - + @@ -10,6 +10,9 @@ + + + @@ -65,12 +68,15 @@ + + + - + - + \ No newline at end of file diff --git a/MeshtasticClient/Persistence/Persistence.swift b/MeshtasticClient/Persistence/Persistence.swift index 9ba9d0f8..d8b6a973 100644 --- a/MeshtasticClient/Persistence/Persistence.swift +++ b/MeshtasticClient/Persistence/Persistence.swift @@ -34,9 +34,11 @@ class PersistenceController { init(inMemory: Bool = false) { container = NSPersistentContainer(name: "Meshtastic") + if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") } + container.loadPersistentStores(completionHandler: { (_, error) in // Merge policy that favors in memory data over data in the db @@ -44,17 +46,8 @@ class PersistenceController { self.container.viewContext.automaticallyMergesChangesFromParent = true if let error = error as NSError? { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - /* - Typical reasons for an error here include: - * The parent directory does not exist, cannot be created, or disallows writing. - * The persistent store is not accessible, due to permissions or data protection when the device is locked. - * The device is out of space. - * The store could not be migrated to the current model version. - Check the error message to determine what the actual problem was. - */ + print("đŸ’Ĩ CoreData Error: \(error.localizedDescription). Now attempting to truncate CoreData database. All app data will be lost.") self.clearDatabase() } }) diff --git a/MeshtasticClient/Protobufs/apponly.pb.swift b/MeshtasticClient/Protobufs/apponly.pb.swift new file mode 100644 index 00000000..5c3b13ed --- /dev/null +++ b/MeshtasticClient/Protobufs/apponly.pb.swift @@ -0,0 +1,73 @@ +// DO NOT EDIT. +// swift-format-ignore-file +// +// Generated by the Swift generator plugin for the protocol buffer compiler. +// Source: apponly.proto +// +// For information on using the generated types, please see the documentation: +// https://github.com/apple/swift-protobuf/ + +import Foundation +import SwiftProtobuf + +// If the compiler emits an error on this type, it is because this file +// was generated by a version of the `protoc` Swift plug-in that is +// incompatible with the version of SwiftProtobuf to which you are linking. +// Please ensure that you are building against the same version of the API +// that was used to generate this file. +fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { + struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} + typealias Version = _2 +} + +/// +/// This is the most compact possible representation for a set of channels. +/// It includes only one PRIMARY channel (which must be first) and +/// any SECONDARY channels. +/// No DISABLED channels are included. +/// This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL +struct ChannelSet { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + var settings: [ChannelSettings] = [] + + var unknownFields = SwiftProtobuf.UnknownStorage() + + init() {} +} + +// MARK: - Code below here is support for the SwiftProtobuf runtime. + +extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + static let protoMessageName: String = "ChannelSet" + static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "settings"), + ] + + mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &self.settings) }() + default: break + } + } + } + + func traverse(visitor: inout V) throws { + if !self.settings.isEmpty { + try visitor.visitRepeatedMessageField(value: self.settings, fieldNumber: 1) + } + try unknownFields.traverse(visitor: &visitor) + } + + static func ==(lhs: ChannelSet, rhs: ChannelSet) -> Bool { + if lhs.settings != rhs.settings {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index 897f8f21..a6930406 100644 --- a/MeshtasticClient/Views/Bluetooth/Connect.swift +++ b/MeshtasticClient/Views/Bluetooth/Connect.swift @@ -22,13 +22,28 @@ struct Connect: View { @State var isPreferredRadio: Bool = false var body: some View { + + let firmwareVersion = bleManager.lastConnnectionVersion + let minimumVersion = "1.2.30" + let supportedVersion = firmwareVersion == "0.0.0" || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedSame + NavigationView { VStack { if bleManager.isSwitchedOn { List { + + if supportedVersion == false { + + Section(header: Text("Upgrade your Firmware").font(.title)) { + + Text("🚨 Your firmware version is unsupported, the minimum firmware version is \(minimumVersion).").font(.subheadline).foregroundColor(.red) + } + .textCase(nil) + } + if bleManager.lastConnectionError.count > 0 { Section(header: Text("Connection Error").font(.title)) { @@ -204,9 +219,9 @@ struct Connect: View { ZStack { ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : + bluetoothOn: self.bleManager.isSwitchedOn, + deviceConnected: self.bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.shortName : "???") } ) @@ -216,7 +231,7 @@ struct Connect: View { self.bleManager.context = context - if bleManager.connectedPeripheral != nil && userSettings.preferredPeripheralId == bleManager.connectedPeripheral.peripheral.identifier.uuidString { + if self.bleManager.connectedPeripheral != nil && userSettings.preferredPeripheralId == self.bleManager.connectedPeripheral.peripheral.identifier.uuidString { isPreferredRadio = true } else { isPreferredRadio = false diff --git a/MeshtasticClient/Views/ContentView.swift b/MeshtasticClient/Views/ContentView.swift index d0ba3a04..db8610ae 100644 --- a/MeshtasticClient/Views/ContentView.swift +++ b/MeshtasticClient/Views/ContentView.swift @@ -19,21 +19,21 @@ struct ContentView: View { var body: some View { TabView(selection: $selection) { -// Contacts() -// .tabItem { -// Label("Messages", systemImage: "text.bubble") -// .symbolRenderingMode(.hierarchical) -// .symbolVariant(.none) -// -// } -// .tag(Tab.contacts) - Channels() - .tabItem { - Label("Messages", systemImage: "text.bubble") + Contacts() + .tabItem { + Label("Messages", systemImage: "text.bubble") .symbolRenderingMode(.hierarchical) - .symbolVariant(.none) - } - .tag(Tab.messages) + .symbolVariant(.none) + + } + .tag(Tab.contacts) +// Channels() +// .tabItem { +// Label("Messages", systemImage: "text.bubble") +// .symbolRenderingMode(.hierarchical) +// .symbolVariant(.none) +// } +// .tag(Tab.messages) Connect() .tabItem { Label("Bluetooth", systemImage: "dot.radiowaves.left.and.right") diff --git a/MeshtasticClient/Views/Helpers/CircleText.swift b/MeshtasticClient/Views/Helpers/CircleText.swift index de7c8548..814d508b 100644 --- a/MeshtasticClient/Views/Helpers/CircleText.swift +++ b/MeshtasticClient/Views/Helpers/CircleText.swift @@ -8,14 +8,19 @@ import SwiftUI struct CircleText: View { var text: String var color: Color + var circleSize: CGFloat? = 50 + var fontSize: CGFloat? = 24 var body: some View { + + let font = Font.system(size: fontSize!) + ZStack { Circle() .fill(color) - .frame(width: 36, height: 36) - Text(text).textCase(.uppercase).font(.caption2).foregroundColor(.white) - .frame(width: 36, height: 36, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).offset(x: 0, y: 0) + .frame(width: circleSize, height: circleSize) + Text(text).textCase(.uppercase).font(font).foregroundColor(.white) + .frame(width: circleSize, height: circleSize, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/).offset(x: 0, y: 0) } } } diff --git a/MeshtasticClient/Views/Messages/Channels.swift b/MeshtasticClient/Views/Messages/Channels.swift index 5055f10a..3068fa13 100644 --- a/MeshtasticClient/Views/Messages/Channels.swift +++ b/MeshtasticClient/Views/Messages/Channels.swift @@ -1,44 +1,44 @@ -import Foundation -import SwiftUI -import CoreBluetooth - -struct Channels: View { - - @State private var isShowingDetailView = true - - var body: some View { - - NavigationView { - - NavigationLink(destination: Messages(), isActive: $isShowingDetailView) { - - List { - - HStack { - - Image(systemName: "megaphone.fill") - .font(.largeTitle) - .symbolRenderingMode(.hierarchical) - .padding(.trailing) - .foregroundColor(.accentColor) - - Text("All - Broadcast") - .font(.largeTitle) - - }.padding() - } - } - .navigationTitle("Contacts") - } - .navigationViewStyle(DoubleColumnNavigationViewStyle()) - } -} - -struct MessageList_Previews: PreviewProvider { - - static var previews: some View { - Group { - Channels() - } - } -} +//import Foundation +//import SwiftUI +//import CoreBluetooth +// +//struct Channels: View { +// +// @State private var isShowingDetailView = true +// +// var body: some View { +// +// NavigationView { +// +// NavigationLink(destination: Messages(), isActive: $isShowingDetailView) { +// +// List { +// +// HStack { +// +// Image(systemName: "megaphone.fill") +// .font(.largeTitle) +// .symbolRenderingMode(.hierarchical) +// .padding(.trailing) +// .foregroundColor(.accentColor) +// +// Text("All - Broadcast") +// .font(.largeTitle) +// +// }.padding() +// } +// } +// .navigationTitle("Contacts") +// } +// .navigationViewStyle(DoubleColumnNavigationViewStyle()) +// } +//} +// +//struct MessageList_Previews: PreviewProvider { +// +// static var previews: some View { +// Group { +// Channels() +// } +// } +//} diff --git a/MeshtasticClient/Views/Messages/Contacts.swift b/MeshtasticClient/Views/Messages/Contacts.swift index 734fec19..2081819f 100644 --- a/MeshtasticClient/Views/Messages/Contacts.swift +++ b/MeshtasticClient/Views/Messages/Contacts.swift @@ -1,141 +1,137 @@ -//// -//// Contacts.swift -//// MeshtasticClient -//// -//// Created by Garth Vander Houwen on 12/21/21. -//// // -//import SwiftUI +// Contacts.swift +// MeshtasticClient // -//struct Contacts: View { +// Created by Garth Vander Houwen on 12/21/21. // -// @Environment(\.managedObjectContext) var context -// @EnvironmentObject var bleManager: BLEManager -// -// @FetchRequest( -// sortDescriptors: [NSSortDescriptor(key: "longName", ascending: true)], -// animation: .default) -// -// private var users: FetchedResults -// -// var body: some View { -// -// NavigationView { -// -// List(users) { (user: UserEntity) in -// -// if user.receivedMessages?.count ?? 0 > 0 { -// -// let currentUserNum = self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0 -// -// let mostRecentBC = user.receivedMessages?.array.last as! MessageEntity -// -// let mostRecentDM = user.receivedMessages?.array.last(where: {($0 as! MessageEntity).toUser!.num == currentUserNum }) as? MessageEntity -// -// let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64(mostRecentDM?.messageTimestamp ?? mostRecentBC.messageTimestamp))) -// let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 -// let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 -// -// if user.num == bleManager.broadcastNodeNum {//user.num != currentUserNum && (user.num == bleManager.broadcastNodeNum || mostRecentDM != nil) { -// -// NavigationLink(destination: UserMessageList(user: user)) { -// -// HStack { -// -// VStack { -// -// CircleText(text: user.shortName ?? "???", color: Color.blue) -// } -// .padding([.leading, .trailing]) -// -// VStack { -// -// HStack { -// -// VStack { -// -// Text(user.longName ?? "Unknown").font(.headline).fixedSize() -// } -// -// VStack { -// -// if lastMessageDay == currentDay { -// -// Text(lastMessageTime, style: .time ) -// .font(.caption) -// .foregroundColor(.gray) -// -// } else if lastMessageDay == (currentDay - 1) { -// -// Text("Yesterday") -// .font(.callout) -// .foregroundColor(.gray) -// -// } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { -// -// Text(lastMessageTime, style: .date) -// -// } else { -// -// Text(lastMessageTime, style: .date) -// } -// }.frame(maxWidth: .infinity, alignment: .trailing) -// } -// .listRowSeparator(.hidden).frame(height: 5) -// -// HStack(alignment: .top) { -// Text(mostRecentDM != nil ? mostRecentDM?.messagePayload as! String : (mostRecentBC.messagePayload ?? "Unknown" )) -// .frame(height: 60) -// .truncationMode(.tail) -// .foregroundColor(Color.gray) -// .frame(maxWidth: .infinity, alignment: .leading) -// } -// }.padding(.top, 15) -// } -// } -// } -// -// } else if false {// self.bleManager.connectedPeripheral == nil || ((self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0) != user.num) { -// -// NavigationLink(destination: UserMessageList(user: user)) { -// -// HStack { -// -// VStack { -// -// CircleText(text: user.shortName ?? "???", color: Color.blue) -// } -// .padding(.trailing) -// -// VStack { -// -// HStack { -// -// VStack { -// -// Text(user.longName ?? "Unknown").font(.headline).fixedSize() -// } -// -// VStack { -// Text(" ") -// } -// .frame(maxWidth: .infinity, alignment: .trailing) -// } -// .listRowSeparator(.hidden).frame(height: 5) -// } -// }.padding() -// } -// } -// } -// .navigationTitle("Contacts") -// .navigationBarTitleDisplayMode(.inline) -// } -// .listStyle(PlainListStyle()) -// } -//} -// -//struct Contacts_Previews: PreviewProvider { -// static var previews: some View { -// Contacts() -// } -//} + +import SwiftUI + +struct Contacts: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(key: "longName", ascending: true)], + animation: .default) + + private var users: FetchedResults + + var body: some View { + + NavigationView { + + List(users) { (user: UserEntity) in + + let currentUserNum = self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0 + + let allMessages = user.value(forKey: "allMessages") as! [MessageEntity] + + NavigationLink(destination: UserMessageList(user: user)) { + + if allMessages.count > 0 { + + let mostRecent = allMessages.last + let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent!.messageTimestamp )))) + let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 + let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 + + HStack { + + VStack { + + CircleText(text: user.shortName ?? "???", color: Color.blue) + } + .padding([.leading, .trailing]) + + VStack { + + HStack { + + VStack { + + Text(user.longName ?? "Unknown").font(.headline).fixedSize() + } + + VStack { + + if lastMessageDay == currentDay { + + Text(lastMessageTime, style: .time ) + .font(.caption) + .foregroundColor(.gray) + + } else if lastMessageDay == (currentDay - 1) { + + Text("Yesterday") + .font(.callout) + .foregroundColor(.gray) + + } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { + + Text(lastMessageTime, style: .date) + + } else { + + Text(lastMessageTime, style: .date) + } + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + .listRowSeparator(.hidden).frame(height: 5) + + HStack(alignment: .top) { + + Text(mostRecent!.messagePayload ?? "Empty Message") + .frame(height: 60) + .truncationMode(.tail) + .foregroundColor(Color.gray) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .padding(.top) + } + + } else { + + HStack { + + VStack { + + CircleText(text: user.shortName ?? "???", color: Color.blue) + } + .padding(.trailing) + + VStack { + + HStack { + + VStack { + + Text(user.longName ?? "Unknown").font(.headline).fixedSize() + } + + VStack { + Text(" ") + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + .listRowSeparator(.hidden).frame(height: 5) + } + }.padding() + } + } + } + .navigationTitle("Contacts") + .navigationBarTitleDisplayMode(.inline) + } + .listStyle(PlainListStyle()) + } +} + +struct Contacts_Previews: PreviewProvider { + static var previews: some View { + Contacts() + } +} diff --git a/MeshtasticClient/Views/Messages/Messages.swift b/MeshtasticClient/Views/Messages/Messages.swift index 68cdfb72..6ffe5aae 100644 --- a/MeshtasticClient/Views/Messages/Messages.swift +++ b/MeshtasticClient/Views/Messages/Messages.swift @@ -1,204 +1,204 @@ -import SwiftUI -import MapKit -import Foundation -import CoreLocation - -struct Messages: View { - - enum Field: Hashable { - case messageText - } - - // CoreData - @Environment(\.managedObjectContext) var context - @EnvironmentObject var bleManager: BLEManager - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true)], - animation: .default) - var messages: FetchedResults - - // Keyboard State - @State var typingMessage: String = "" - @State private var totalBytes = 0 - private var maxbytes = 228 - @State private var lastTypingMessage = "" - @FocusState private var focusedField: Field? - - @State var showDeleteMessageAlert = false - @State private var deleteMessageId: Int64 = 0 - - public var broadcastNodeId: UInt32 = 4294967295 - - var body: some View { - - Text("\(messages.count) Messages").font(.caption) - GeometryReader { bounds in - - VStack { - - ScrollViewReader { scrollView in - - if self.messages.count > 0 { - - ScrollView { - - ForEach(messages) { message in - - HStack(alignment: .top) { - let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false ) - - CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) - .gesture(LongPressGesture(minimumDuration: 2) - .onEnded {_ in - print(messages) - print("I want to delete message: \(message.messageId)") - self.showDeleteMessageAlert = true - self.deleteMessageId = message.messageId - - print(deleteMessageId) - }) - - VStack(alignment: .leading) { - Text(message.messagePayload ?? "EMPTY MESSAGE") - .textSelection(.enabled) - .padding(10) - .foregroundColor(.white) - .background(currentUser ? Color.blue : Color(.darkGray)) - .cornerRadius(10) - HStack(spacing: 4) { - - let time = Int32(message.messageTimestamp) - let messageDate = Date(timeIntervalSince1970: TimeInterval(time)) - - if time != 0 { - Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray) - Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray) - } else { - Text("Unknown").font(.caption2).foregroundColor(.gray) - } - } - .padding(.bottom, 10) - } - Spacer() - } - .alert(isPresented: $showDeleteMessageAlert) { - Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), - primaryButton: .destructive(Text("Delete")) { - print("OK button tapped") - if deleteMessageId > 0 { - - let message = messages.first(where: { $0.messageId == deleteMessageId }) - - context.delete(message!) - do { - try context.save() - - deleteMessageId = 0 - - } catch { - print("Failed to delete message \(deleteMessageId)") - } - - } - }, - secondaryButton: .cancel() - ) - } - } - .onChange(of: messages.count, perform: { newValue in - - if messages.count > 0 { - - scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom) - } - } - ) - .onAppear(perform: { - - self.bleManager.context = context - if messages.count > 0 { - - scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom) - } - }) - } - .padding(.horizontal) - } - } - - HStack(alignment: .top) { - - ZStack { - - let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) - TextEditor(text: $typingMessage) - .onChange(of: typingMessage, perform: { value in - - let size = value.utf8.count - totalBytes = size - if totalBytes <= maxbytes { - // Allow the user to type - lastTypingMessage = typingMessage - } else { - // Set the message back and remove the bytes over the count - self.typingMessage = lastTypingMessage - } - }) - .keyboardType(kbType!) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - - Button("Dismiss Keyboard") { - focusedField = nil - } - .font(.subheadline) - - Spacer() - - ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .frame(width: 130) - .padding(5) - .font(.subheadline) - .accentColor(.accentColor) - } - } - .padding(.horizontal, 8) - .focused($focusedField, equals: .messageText) - .multilineTextAlignment(.leading) - .frame(minHeight: bounds.size.height / 4, maxHeight: bounds.size.height / 4) - - Text(typingMessage).opacity(0).padding(.all, 0) - - } - .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) - .padding(.bottom, 15) - - Button(action: { - if self.bleManager.sendMessage(message: typingMessage, toUserNum: Int64(self.bleManager.broadcastNodeNum)) { - typingMessage = "" - focusedField = nil - } - - }) { - Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) - } - - } - .padding(.all, 15) - } - } - .navigationTitle("All - Broadcast") - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(trailing: - - ZStack { - - ConnectedDevice( - bluetoothOn: self.bleManager.isSwitchedOn, - deviceConnected: self.bleManager.connectedPeripheral != nil, - name: (self.bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.shortName : "???") - } - ) - } -} +//import SwiftUI +//import MapKit +//import Foundation +//import CoreLocation +// +//struct Messages: View { +// +// enum Field: Hashable { +// case messageText +// } +// +// // CoreData +// @Environment(\.managedObjectContext) var context +// @EnvironmentObject var bleManager: BLEManager +// +// @FetchRequest( +// sortDescriptors: [NSSortDescriptor(keyPath: \MessageEntity.messageTimestamp, ascending: true)], +// animation: .default) +// var messages: FetchedResults +// +// // Keyboard State +// @State var typingMessage: String = "" +// @State private var totalBytes = 0 +// private var maxbytes = 228 +// @State private var lastTypingMessage = "" +// @FocusState private var focusedField: Field? +// +// @State var showDeleteMessageAlert = false +// @State private var deleteMessageId: Int64 = 0 +// +// public var broadcastNodeId: UInt32 = 4294967295 +// +// var body: some View { +// +// Text("\(messages.count) Messages").font(.caption) +// GeometryReader { bounds in +// +// VStack { +// +// ScrollViewReader { scrollView in +// +// if self.messages.count > 0 { +// +// ScrollView { +// +// ForEach(messages) { message in +// +// HStack(alignment: .top) { +// let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false ) +// +// CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) +// .gesture(LongPressGesture(minimumDuration: 2) +// .onEnded {_ in +// print(messages) +// print("I want to delete message: \(message.messageId)") +// self.showDeleteMessageAlert = true +// self.deleteMessageId = message.messageId +// +// print(deleteMessageId) +// }) +// +// VStack(alignment: .leading) { +// Text(message.messagePayload ?? "EMPTY MESSAGE") +// .textSelection(.enabled) +// .padding(10) +// .foregroundColor(.white) +// .background(currentUser ? Color.blue : Color(.darkGray)) +// .cornerRadius(10) +// HStack(spacing: 4) { +// +// let time = Int32(message.messageTimestamp) +// let messageDate = Date(timeIntervalSince1970: TimeInterval(time)) +// +// if time != 0 { +// Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray) +// Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray) +// } else { +// Text("Unknown").font(.caption2).foregroundColor(.gray) +// } +// } +// .padding(.bottom, 10) +// } +// Spacer() +// } +// .alert(isPresented: $showDeleteMessageAlert) { +// Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), +// primaryButton: .destructive(Text("Delete")) { +// print("OK button tapped") +// if deleteMessageId > 0 { +// +// let message = messages.first(where: { $0.messageId == deleteMessageId }) +// +// context.delete(message!) +// do { +// try context.save() +// +// deleteMessageId = 0 +// +// } catch { +// print("Failed to delete message \(deleteMessageId)") +// } +// +// } +// }, +// secondaryButton: .cancel() +// ) +// } +// } +// .onChange(of: messages.count, perform: { newValue in +// +// if messages.count > 0 { +// +// scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom) +// } +// } +// ) +// .onAppear(perform: { +// +// self.bleManager.context = context +// if messages.count > 0 { +// +// scrollView.scrollTo(messages[messages.count-1].id, anchor: .bottom) +// } +// }) +// } +// .padding(.horizontal) +// } +// } +// +// HStack(alignment: .top) { +// +// ZStack { +// +// let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) +// TextEditor(text: $typingMessage) +// .onChange(of: typingMessage, perform: { value in +// +// let size = value.utf8.count +// totalBytes = size +// if totalBytes <= maxbytes { +// // Allow the user to type +// lastTypingMessage = typingMessage +// } else { +// // Set the message back and remove the bytes over the count +// self.typingMessage = lastTypingMessage +// } +// }) +// .keyboardType(kbType!) +// .toolbar { +// ToolbarItemGroup(placement: .keyboard) { +// +// Button("Dismiss Keyboard") { +// focusedField = nil +// } +// .font(.subheadline) +// +// Spacer() +// +// ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) +// .frame(width: 130) +// .padding(5) +// .font(.subheadline) +// .accentColor(.accentColor) +// } +// } +// .padding(.horizontal, 8) +// .focused($focusedField, equals: .messageText) +// .multilineTextAlignment(.leading) +// .frame(minHeight: bounds.size.height / 4, maxHeight: bounds.size.height / 4) +// +// Text(typingMessage).opacity(0).padding(.all, 0) +// +// } +// .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) +// .padding(.bottom, 15) +// +// Button(action: { +// if self.bleManager.sendMessage(message: typingMessage, toUserNum: Int64(self.bleManager.broadcastNodeNum)) { +// typingMessage = "" +// focusedField = nil +// } +// +// }) { +// Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) +// } +// +// } +// .padding(.all, 15) +// } +// } +// .navigationTitle("All - Broadcast") +// .navigationBarTitleDisplayMode(.inline) +// .navigationBarItems(trailing: +// +// ZStack { +// +// ConnectedDevice( +// bluetoothOn: self.bleManager.isSwitchedOn, +// deviceConnected: self.bleManager.connectedPeripheral != nil, +// name: (self.bleManager.connectedPeripheral != nil) ? self.bleManager.connectedPeripheral.shortName : "???") +// } +// ) +// } +//} diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 3162845a..8312afce 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -1,206 +1,423 @@ -//// -//// UserMessageList.swift -//// MeshtasticClient -//// -//// Created by Garth Vander Houwen on 12/24/21. -//// // -//import SwiftUI -//import CoreData +// UserMessageList.swift +// MeshtasticClient // -//struct UserMessageList: View { +// Created by Garth Vander Houwen on 12/24/21. // -// @Environment(\.managedObjectContext) var context -// @EnvironmentObject var bleManager: BLEManager -// -// enum Field: Hashable { -// case messageText -// } -// // Keyboard State -// @State var typingMessage: String = "" -// @State private var totalBytes = 0 -// var maxbytes = 228 -// @State var lastTypingMessage = "" -// @FocusState var focusedField: Field? -// -// var user: UserEntity -// -// @State var showDeleteMessageAlert = false -// @State private var deleteMessageId: Int64 = 0 -// -// var body: some View { -// -//// HStack { -// -// VStack { -// -// // List { -// -// ScrollViewReader { _ in -// -// ScrollView { -// -// if user.receivedMessages?.count ?? 0 > 0 { -// -// ForEach( user.receivedMessages?.array as! [MessageEntity], id: \.self) { (message: MessageEntity) in -// -// // HStack { -// let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false ) -// -// -// if message.toUser!.num == Int64(bleManager.broadcastNodeNum) || ((bleManager.connectedPeripheral) != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : true { -// -// -// HStack (alignment: .top) { -// -// if currentUser { Spacer(minLength:50) } -// -// if !currentUser { -// -// CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) -// .gesture(LongPressGesture(minimumDuration: 2).onEnded {_ in -// -// print("I want to delete message: \(message.messageId)") -// self.showDeleteMessageAlert = true -// self.deleteMessageId = message.messageId -// print(deleteMessageId) -// }) -// } -// -// VStack(alignment: currentUser ? .trailing : .leading) { -// -// Text(message.messagePayload ?? "EMPTY MESSAGE") -// .textSelection(.enabled) -// .padding(10) -// .foregroundColor(.white) -// .background(currentUser ? Color.blue : Color(.darkGray)) -// .cornerRadius(15) -// -// HStack(spacing: 4) { -// -// let time = Int32(message.messageTimestamp) -// let messageDate = Date(timeIntervalSince1970: TimeInterval(time)) -// -// if time != 0 { -// Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray) -// Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray) -// } else { -// Text("Unknown").font(.caption2).foregroundColor(.gray) -// } -// } -// .padding(.bottom, 10) -// } -// if !currentUser { -// Spacer(minLength:50) -// } -// } -// .padding(.trailing) -// .frame(maxWidth: .infinity) -// } -// // } -// } -// .listRowSeparator(.hidden) -// } -// } -// } -// // } -// // .padding(.top) -// -// HStack(alignment: .top) { -// -// ZStack { -// -// let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) -// TextEditor(text: $typingMessage) -// .onChange(of: typingMessage, perform: { value in -// -// let size = value.utf8.count -// totalBytes = size -// if totalBytes <= maxbytes { -// // Allow the user to type -// lastTypingMessage = typingMessage -// } else { -// // Set the message back and remove the bytes over the count -// self.typingMessage = lastTypingMessage -// } -// }) -// .keyboardType(kbType!) -// .toolbar { -// ToolbarItemGroup(placement: .keyboard) { -// -// Button("Dismiss Keyboard") { -// focusedField = nil -// } -// .font(.subheadline) -// -// Spacer() -// -// ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) -// .frame(width: 130) -// .padding(5) -// .font(.subheadline) -// .accentColor(.accentColor) -// } -// } -// .padding(.horizontal, 8) -// .focused($focusedField, equals: .messageText) -// .multilineTextAlignment(.leading) -// .frame(minHeight: 100, maxHeight: 160) -// -// Text(typingMessage).opacity(0).padding(.all, 0) -// -// } -// .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) -// .padding(.bottom, 15) -// -// Button(action: { -// if bleManager.sendMessage(message: typingMessage, toUserNum: user.num) { -// typingMessage = "" -// focusedField = nil -// } else { -// -// _ = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { (_) in -// -// if bleManager.sendMessage(message: typingMessage, toUserNum: user.num) { -// typingMessage = "" -// } -// } -// } -// -// }) { -// Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) -// } -// -// } -// .padding(.all, 15) -// } -//// } -// .navigationViewStyle(.stack) -// .navigationBarTitleDisplayMode(.inline) -// .toolbar { -// ToolbarItem(placement: .principal) { -// -// HStack { -// -// CircleText(text: user.shortName ?? "???", color: .blue).fixedSize() -// Text(user.longName ?? "Unknown").foregroundColor(.gray).font(.caption2).fixedSize() -// } -// } -// ToolbarItem(placement: .navigationBarTrailing) { -// ZStack { -// -// ConnectedDevice( -// bluetoothOn: bleManager.isSwitchedOn, -// deviceConnected: bleManager.connectedPeripheral != nil, -// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "???") -// } -// } -// } -// .onAppear(perform: { -// -// self.bleManager.context = context -// -// }) -// } -// -//} + +import SwiftUI +import CoreData + +struct UserMessageList: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + enum Field: Hashable { + case messageText + } + // Keyboard State + @State var typingMessage: String = "" + @State private var totalBytes = 0 + var maxbytes = 228 + @State var lastTypingMessage = "" + @FocusState var focusedField: Field? + + @ObservedObject var user: UserEntity + + @State var showDeleteMessageAlert = false + @State private var deleteMessageId: Int64 = 0 + @State private var replyMessageId: Int64 = 0 + + @State var messageCount = 0 + + var body: some View { + + let firmwareVersion = bleManager.lastConnnectionVersion + let minimumVersion = "1.2.50" + let hasTapbackSupport = minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedSame + + VStack { + + let allMessages = user.value(forKey: "allMessages") as! [MessageEntity] + + ScrollViewReader { scrollView in + + ScrollView { + + if allMessages.count > 0 { + + HStack{ + // Padding at the top of the message list + }.padding(.bottom) + + ForEach( allMessages ) { (message: MessageEntity) in + + let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false ) + + if message.toUser!.num == Int64(bleManager.broadcastNodeNum) || ((bleManager.connectedPeripheral) != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : true { + + if message.replyID > 0 { + + let messageReply = allMessages.first(where: { $0.messageId == message.replyID }) + + HStack { + + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.blue, lineWidth: 0.5) + ) + Image(systemName: "arrowshape.turn.up.left.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.blue) + .padding(.trailing) + } + } + + HStack (alignment: .top) { + + if currentUser { Spacer(minLength:50) } + + if !currentUser { + CircleText(text: message.fromUser?.shortName ?? "???", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 36, fontSize: 16).padding(.all, 5) + } + + VStack(alignment: currentUser ? .trailing : .leading) { + + Text(message.messagePayload ?? "EMPTY MESSAGE") + .padding(10) + .foregroundColor(.white) + .background(currentUser ? Color.blue : Color(.darkGray)) + .cornerRadius(15) + .contextMenu { + + if hasTapbackSupport { + + Menu("Tapback response") { + + Button(action: { + + if bleManager.sendMessage(message: "â¤ī¸", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent â¤ī¸ Tapback") + self.context.refresh(user, mergeChanges: true) + + } else { print("â¤ī¸ Tapback Failed") } + + }) { + Text("Heart") + let image = "â¤ī¸".image() + Image(uiImage: image!) + } + Button(action: { + + if bleManager.sendMessage(message: "👍", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent 👍 Tapback") + self.context.refresh(user, mergeChanges: true) + + } else { print("👍 Tapback Failed")} + + }) { + Text("Thumbs Up") + let image = "👍".image() + Image(uiImage: image!) + } + Button(action: { + + if bleManager.sendMessage(message: "👎", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent 👎 Tapback") + self.context.refresh(user, mergeChanges: true) + + } else { print("👎 Tapback Failed") } + + }) { + Text("Thumbs Down") + let image = "👎".image() + Image(uiImage: image!) + } + Button(action: { + + if bleManager.sendMessage(message: "đŸ¤Ŗ", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent đŸ¤Ŗ Tapback") + self.context.refresh(user, mergeChanges: true) + + } else { print("đŸ¤Ŗ Tapback Failed") } + + }) { + Text("HaHa") + let image = "đŸ¤Ŗ".image() + Image(uiImage: image!) + } + Button(action: { + + if bleManager.sendMessage(message: "â€ŧī¸", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent â€ŧī¸ Tapback") + self.context.refresh(user, mergeChanges: true) + + } else { print("â€ŧī¸ Tapback Failed") } + + }) { + Text("Exclamation Mark") + let image = "â€ŧī¸".image() + Image(uiImage: image!) + } + Button(action: { + + if bleManager.sendMessage(message: "❓", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent ❓ Tapback") + self.context.refresh(user, mergeChanges: true) + + } else { print("❓ Tapback Failed") } + + }) { + Text("Question Mark") + let image = "❓".image() + Image(uiImage: image!) + } + Button(action: { + + if bleManager.sendMessage(message: "💩", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent 💩 Tapback") + self.context.refresh(user, mergeChanges: true) + + } else { print("💩 Tapback Failed") } + + }) { + Text("Poop") + let image = "💩".image() + Image(uiImage: image!) + } + } + Button(action: { + self.replyMessageId = message.messageId + self.focusedField = .messageText + + print("I want to reply to \(message.messageId)") + }) { + Text("Reply") + Image(systemName: "arrowshape.turn.up.left.2.fill") + } + } + Button(action: { + UIPasteboard.general.string = message.messagePayload + }) { + Text("Copy") + Image(systemName: "doc.on.doc") + } + Divider() + Button(role: .destructive, action: { + self.showDeleteMessageAlert = true + self.deleteMessageId = message.messageId + print(deleteMessageId) + }) { + Text("Delete") + Image(systemName: "trash") + } + } + + if hasTapbackSupport { + + let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] + + + if tapbacks.count > 0 { + + VStack (alignment: .trailing) { + + HStack { + + ForEach( tapbacks ) { (tapback: MessageEntity) in + + VStack { + + let image = tapback.messagePayload!.image(fontSize: 20) + Image(uiImage: image!).font(.caption) + Text("\(tapback.fromUser?.shortName ?? "???")") + .font(.caption2) + .foregroundColor(.gray) + .fixedSize() + .padding(.bottom, 1) + } + } + } + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.gray, lineWidth: 1) + ) + } + } + } + + HStack { + + let time = Int32(message.messageTimestamp) + let messageDate = Date(timeIntervalSince1970: TimeInterval(time)) + + if time != 0 { + Text(messageDate, style: .date).font(.caption2).foregroundColor(.gray) + Text(messageDate, style: .time).font(.caption2).foregroundColor(.gray) + } else { + Text("Unknown").font(.caption2).foregroundColor(.gray) + } + } + .padding(4) + } + .id(allMessages.firstIndex(of: message)) + + if !currentUser { + Spacer(minLength:50) + } + } + .padding([.leading, .trailing]) + .frame(maxWidth: .infinity) + .alert(isPresented: $showDeleteMessageAlert) { + Alert(title: Text("Are you sure you want to delete this message?"), message: Text("This action is permanent."), + primaryButton: .destructive(Text("Delete")) { + print("OK button tapped") + if deleteMessageId > 0 { + + let message = allMessages.first(where: { $0.messageId == deleteMessageId }) + + context.delete(message!) + do { + try context.save() + + deleteMessageId = 0 + + } catch { + print("Failed to delete message \(deleteMessageId)") + } + } + }, + secondaryButton: .cancel() + ) + } + } + } + .listRowSeparator(.hidden) + } + } + .onAppear(perform: { + + self.bleManager.context = context + + messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0)) + + if messageCount > 0 { + scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) + } + }) + .onChange(of: allMessages.count, perform: { count in + + self.context.refresh(user, mergeChanges: true) + + let index = count - 1 + + if index > 2 { + + scrollView.scrollTo(index, anchor: .bottom) + + } + }) + } + + + HStack(alignment: .top) { + + ZStack { + + let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) + TextEditor(text: $typingMessage) + .onChange(of: typingMessage, perform: { value in + + let size = value.utf8.count + totalBytes = size + if totalBytes <= maxbytes { + // Allow the user to type + lastTypingMessage = typingMessage + } else { + // Set the message back and remove the bytes over the count + self.typingMessage = lastTypingMessage + } + }) + .keyboardType(kbType!) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + + Button("Dismiss Keyboard") { + focusedField = nil + } + .font(.subheadline) + + Spacer() + + ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + .frame(width: 130) + .padding(5) + .font(.subheadline) + .accentColor(.accentColor) + } + } + .padding(.horizontal, 8) + .focused($focusedField, equals: .messageText) + .multilineTextAlignment(.leading) + .frame(minHeight: 100, maxHeight: 160) + + Text(typingMessage).opacity(0).padding(.all, 0) + + } + .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) + .padding(.bottom, 15) + + Button(action: { + if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, isTapback: false, replyID: replyMessageId) { + typingMessage = "" + focusedField = nil + replyMessageId = 0 + } + + }) { + Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) + } + + } + .padding(.all, 15) + } + + .navigationViewStyle(.stack) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + + HStack { + + CircleText(text: user.shortName ?? "???", color: .blue, circleSize: 42, fontSize: 20).fixedSize() + Text(user.longName ?? "Unknown").font(.headline).fixedSize() + } + } + ToolbarItem(placement: .navigationBarTrailing) { + ZStack { + + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "???") + } + } + } + .onAppear(perform: { + + self.bleManager.context = context + + }) + } +} diff --git a/MeshtasticClient/Views/Nodes/NodeDetail.swift b/MeshtasticClient/Views/Nodes/NodeDetail.swift index 90374520..8ae18b63 100644 --- a/MeshtasticClient/Views/Nodes/NodeDetail.swift +++ b/MeshtasticClient/Views/Nodes/NodeDetail.swift @@ -42,7 +42,7 @@ struct NodeDetail: View { MapAnnotation( coordinate: location.coordinate, content: { - CircleText(text: node.user!.shortName ?? "???", color: .accentColor) + CircleText(text: node.user!.shortName ?? "???", color: .accentColor, circleSize: 33, fontSize: 16) } ) } @@ -91,13 +91,16 @@ struct NodeDetail: View { VStack { - Image(node.user!.hwModel ?? "UNSET") - .resizable() - .frame(width: 50, height: 50) - .cornerRadius(5) + if node.user != nil { + + Image(node.user!.hwModel ?? "UNSET") + .resizable() + .frame(width: 50, height: 50) + .cornerRadius(5) - Text(String(node.user!.hwModel ?? "UNSET")) - .font(.callout).fixedSize() + Text(String(node.user!.hwModel ?? "UNSET")) + .font(.callout).fixedSize() + } } .padding(5) @@ -160,7 +163,7 @@ struct NodeDetail: View { .font(.title2) .foregroundColor(.accentColor) .symbolRenderingMode(.hierarchical) - Text("Unique Id:").font(.title2) + Text("User Id:").font(.title2) } Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray) } @@ -175,11 +178,18 @@ struct NodeDetail: View { } Text(String(node.num)).font(.title3).foregroundColor(.gray) } - }.padding(5) + } + .padding(5) + Divider() HStack { + Image(systemName: "globe") + .font(.headline) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) Text("MAC Address: ") Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray) } + .padding() if node.positions?.count ?? 0 > 1 { @@ -187,7 +197,7 @@ struct NodeDetail: View { HStack { - Image(systemName: "map.circle.fill") + Image(systemName: "location.circle.fill") .font(.title) .foregroundColor(.accentColor) .symbolRenderingMode(.hierarchical) @@ -210,8 +220,13 @@ struct NodeDetail: View { Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))") .foregroundColor(.gray) .font(.caption) - - Text("Altitude:") + + Image(systemName: "arrow.up.arrow.down.circle") + .font(.subheadline) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + + Text("Alt:") .font(.caption) Text("\(String(mappin.altitude))m") @@ -231,21 +246,27 @@ struct NodeDetail: View { .font(.caption) Divider() - Text("Battery").font(.caption).fixedSize() - Text(String(mappin.batteryLevel) + "%") - .font(.caption) - .foregroundColor(.gray) - .symbolRenderingMode(.hierarchical) + HStack { + + BatteryIcon(batteryLevel: mappin.batteryLevel, font: .subheadline, color: .accentColor) + + if mappin.batteryLevel > 0 { + + Text(String(mappin.batteryLevel) + "%") + .font(.caption2) + .foregroundColor(.gray) + } + } } } .padding(1) Divider() } } - .padding(.bottom, 5) // Without some padding here there is a transparent contentview bug } } - }.ignoresSafeArea(.all, edges: [.leading, .trailing]) + } + .edgesIgnoringSafeArea([.leading, .trailing]) } } .navigationTitle(node.user!.longName ?? "Unknown") diff --git a/MeshtasticClient/Views/Settings/AppSettings.swift b/MeshtasticClient/Views/Settings/AppSettings.swift index ad654ee8..1a331f6c 100644 --- a/MeshtasticClient/Views/Settings/AppSettings.swift +++ b/MeshtasticClient/Views/Settings/AppSettings.swift @@ -164,7 +164,16 @@ struct AppSettings: View { } .pickerStyle(DefaultPickerStyle()) } - Section(header: Text("MESH NETWORK OPTIONS")) { + Section(header: Text("MAP OPTIONS")) { + Picker("Map Type", selection: $userSettings.meshMapType) { + ForEach(MeshMapType.allCases) { map in + Text(map.description) + } + } + .pickerStyle(DefaultPickerStyle()) + TextField("Custom Tile Server", text: $userSettings.meshMapCustomTileServer) + } + Section(header: Text("DEBUG")) { // Toggle(isOn: $userSettings.meshActivityLog) { // Label("Log all Mesh activity", systemImage: "network") @@ -177,15 +186,6 @@ struct AppSettings: View { .listRowSeparator(.visible) } } - Section(header: Text("MAP OPTIONS")) { - Picker("Base Map (Apple)", selection: $userSettings.meshMapType) { - ForEach(MeshMapType.allCases) { map in - Text(map.description) - } - } - .pickerStyle(DefaultPickerStyle()) - TextField("Custom Tile Server", text: $userSettings.meshMapCustomTileServer) - } } } .navigationTitle("App Settings")