From aebf9973151e4cc0ce1d00328917675a3591f845 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 31 Dec 2021 20:52:22 -0800 Subject: [PATCH 01/20] Add app only protobuf --- MeshtasticClient/Persistence/Persistence.swift | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/MeshtasticClient/Persistence/Persistence.swift b/MeshtasticClient/Persistence/Persistence.swift index 9ba9d0f8..43b77da3 100644 --- a/MeshtasticClient/Persistence/Persistence.swift +++ b/MeshtasticClient/Persistence/Persistence.swift @@ -41,20 +41,11 @@ class PersistenceController { // Merge policy that favors in memory data over data in the db self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - self.container.viewContext.automaticallyMergesChangesFromParent = true + //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() } }) From a8917c9aa0babe4ffc5041c3a2e190dc6d1d2cf2 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 31 Dec 2021 20:53:45 -0800 Subject: [PATCH 02/20] Add new app only protobuf --- MeshtasticClient/Protobufs/apponly.pb.swift | 73 +++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 MeshtasticClient/Protobufs/apponly.pb.swift 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 + } +} From 7416676c697a8a425b4932f3f5bb235a9849c29b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Jan 2022 08:03:46 -0800 Subject: [PATCH 03/20] Turn new messaging views back on --- .../CoreDataSample.xcdatamodel/contents | 5 +- .../Persistence/Persistence.swift | 4 +- MeshtasticClient/Views/ContentView.swift | 42 +- .../Views/Messages/Channels.swift | 88 +-- .../Views/Messages/Contacts.swift | 277 ++++----- .../Views/Messages/Messages.swift | 408 ++++++------ .../Views/Messages/UserMessageList.swift | 404 ++++++------ MeshtasticClient/Views/Nodes/NodeDetail.swift | 579 +++++++++--------- MeshtasticClient/Views/Nodes/NodeList.swift | 252 ++++---- 9 files changed, 1041 insertions(+), 1018 deletions(-) diff --git a/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents b/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents index 86be9a38..6860e1c6 100644 --- a/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents +++ b/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents @@ -65,12 +65,15 @@ + + + - + \ No newline at end of file diff --git a/MeshtasticClient/Persistence/Persistence.swift b/MeshtasticClient/Persistence/Persistence.swift index 43b77da3..d8b6a973 100644 --- a/MeshtasticClient/Persistence/Persistence.swift +++ b/MeshtasticClient/Persistence/Persistence.swift @@ -34,14 +34,16 @@ 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 self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy - //self.container.viewContext.automaticallyMergesChangesFromParent = true + self.container.viewContext.automaticallyMergesChangesFromParent = true if let error = error as NSError? { diff --git a/MeshtasticClient/Views/ContentView.swift b/MeshtasticClient/Views/ContentView.swift index d0ba3a04..327c55ef 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") @@ -41,13 +41,13 @@ struct ContentView: View { .symbolVariant(.none) } .tag(Tab.ble) - NodeList() - .tabItem { - Label("Nodes", systemImage: "flipphone") - .symbolRenderingMode(.hierarchical) - .symbolVariant(.none) - } - .tag(Tab.nodes) +// NodeList() +// .tabItem { +// Label("Nodes", systemImage: "flipphone") +// .symbolRenderingMode(.hierarchical) +// .symbolVariant(.none) +// } +// .tag(Tab.nodes) NodeMap() .tabItem { Label("Mesh Map", systemImage: "map") 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..84e627a7 100644 --- a/MeshtasticClient/Views/Messages/Contacts.swift +++ b/MeshtasticClient/Views/Messages/Contacts.swift @@ -1,141 +1,142 @@ -//// -//// 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 + + 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) + .environment(\.managedObjectContext, self.context)) { + + 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() + } +} 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..b8393215 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -1,206 +1,206 @@ -//// -//// 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 : "???") -// } -// } + +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? + + 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) + } // } -// .onAppear(perform: { -// -// self.bleManager.context = context -// -// }) -// } -// -//} + .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 + + }) + } + +} diff --git a/MeshtasticClient/Views/Nodes/NodeDetail.swift b/MeshtasticClient/Views/Nodes/NodeDetail.swift index 90374520..e5bb21db 100644 --- a/MeshtasticClient/Views/Nodes/NodeDetail.swift +++ b/MeshtasticClient/Views/Nodes/NodeDetail.swift @@ -1,281 +1,298 @@ -/* -Abstract: -A view showing the details for a node. -*/ - -import SwiftUI -import MapKit -import CoreLocation - -struct NodeDetail: View { - - @Environment(\.managedObjectContext) var context - @EnvironmentObject var bleManager: BLEManager - - var node: NodeInfoEntity - - var body: some View { - - HStack { - - GeometryReader { bounds in - - VStack { - - if node.positions?.count ?? 0 > 0 { - - let mostRecent = node.positions?.lastObject as! PositionEntity - - if mostRecent.coordinate != nil { - - let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!) - - let regionBinding = Binding( - get: { - MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) - }, - set: { _ in } - ) - let annotations = [MapLocation(name: node.user!.shortName ?? "???", coordinate: mostRecent.coordinate!)] - - Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in - MapAnnotation( - coordinate: location.coordinate, - content: { - CircleText(text: node.user!.shortName ?? "???", color: .accentColor) - } - ) - } - .frame(idealWidth: bounds.size.width, maxHeight: bounds.size.height / 3) - } else { - - Image(node.user?.hwModel ?? "UNSET") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: bounds.size.width, height: bounds.size.height / 2) - } - } else { - - Image(node.user?.hwModel ?? "UNSET") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: bounds.size.width, height: bounds.size.height / 2) - } - - ScrollView { - - if node.lastHeard != nil { - - HStack { - - Image(systemName: "clock.badge.checkmark.fill") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.title3) - } - .padding() - Divider() - } - - HStack { - - VStack(alignment: .center) { - Text("AKA").font(.title2).fixedSize() - CircleText(text: node.user?.shortName ?? "???", color: .accentColor) - .offset(y: 10) - } - .padding(5) - - Divider() - - VStack { - - Image(node.user!.hwModel ?? "UNSET") - .resizable() - .frame(width: 50, height: 50) - .cornerRadius(5) - - Text(String(node.user!.hwModel ?? "UNSET")) - .font(.callout).fixedSize() - } - .padding(5) - - if node.snr > 0 { - Divider() - VStack(alignment: .center) { - - Image(systemName: "waveform.path") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("SNR").font(.title2).fixedSize() - Text(String(node.snr)) - .font(.title2) - .foregroundColor(.gray) - .fixedSize() - } - .padding(5) - } - - if node.positions!.count > 0 { - - let mostRecent = node.positions?.lastObject as! PositionEntity - - Divider() - - VStack(alignment: .center) { - - BatteryIcon(batteryLevel: mostRecent.batteryLevel, font: .title, color: .accentColor) - .padding(.bottom) - if mostRecent.batteryLevel > 0 { - - Text("Battery") - .font(.title2) - .fixedSize() - .textCase(.uppercase) - Text(String(mostRecent.batteryLevel) + "%") - .font(.title2) - .foregroundColor(.gray) - .symbolRenderingMode(.hierarchical) - } else { - - Text("Powered") - .font(.callout) - .fixedSize() - .textCase(.uppercase) - } - } - .padding(5) - } - } - .padding(4) - - Divider() - - HStack(alignment: .center) { - VStack { - HStack { - Image(systemName: "person") - .font(.title2) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Unique Id:").font(.title2) - } - Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray) - } - Divider() - VStack { - HStack { - Image(systemName: "number") - .font(.title2) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Node Number:").font(.title2) - } - Text(String(node.num)).font(.title3).foregroundColor(.gray) - } - }.padding(5) - HStack { - Text("MAC Address: ") - Text(String(node.user?.macaddr?.macAddressString ?? "not a valid mac address")).foregroundColor(.gray) - } - - if node.positions?.count ?? 0 > 1 { - - Divider() - - HStack { - - Image(systemName: "map.circle.fill") - .font(.title) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Location History").font(.title2) - } - .padding() - - Divider() - - ForEach(node.positions!.array as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in - - if mappin.coordinate != nil { - - VStack { - - HStack { - - Image(systemName: "mappin.and.ellipse").foregroundColor(.accentColor) // .font(.subheadline) - Text("Lat/Long:").font(.caption) - Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))") - .foregroundColor(.gray) - .font(.caption) - - Text("Altitude:") - .font(.caption) - - Text("\(String(mappin.altitude))m") - .foregroundColor(.gray) - .font(.caption) - } - HStack { - - Image(systemName: "clock.badge.checkmark.fill") - .font(.subheadline) - .foregroundColor(.accentColor) - .symbolRenderingMode(.hierarchical) - Text("Time:") - .font(.caption) - Text("\(mappin.time!, style: .date) \(mappin.time!, style: .time)") - .foregroundColor(.gray) - .font(.caption) - Divider() - - Text("Battery").font(.caption).fixedSize() - Text(String(mappin.batteryLevel) + "%") - .font(.caption) - .foregroundColor(.gray) - .symbolRenderingMode(.hierarchical) - } - } - .padding(1) - Divider() - } - } - .padding(.bottom, 5) // Without some padding here there is a transparent contentview bug - } - } - }.ignoresSafeArea(.all, edges: [.leading, .trailing]) - } - } - .navigationTitle(node.user!.longName ?? "Unknown") - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(trailing: - - ZStack { - - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "???") - } - ) - .onAppear(perform: { - - self.bleManager.context = context - - }) - } -} - -struct NodeInfoEntityDetail_Previews: PreviewProvider { - - static let bleManager = BLEManager() - - static var previews: some View { - Group { - - // NodeDetail(node: node) - } - } -} +///* +//Abstract: +//A view showing the details for a node. +//*/ +// +//import SwiftUI +//import MapKit +//import CoreLocation +// +//struct NodeDetail: View { +// +// @Environment(\.managedObjectContext) var context +// @EnvironmentObject var bleManager: BLEManager +// +// var node: NodeInfoEntity +// +// var body: some View { +// +// HStack { +// +// GeometryReader { bounds in +// +// VStack { +// +// if node.positions?.count ?? 0 > 0 { +// +// let mostRecent = node.positions?.lastObject as! PositionEntity +// +// if mostRecent.coordinate != nil { +// +// let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!) +// +// let regionBinding = Binding( +// get: { +// MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) +// }, +// set: { _ in } +// ) +// let annotations = [MapLocation(name: node.user!.shortName ?? "???", coordinate: mostRecent.coordinate!)] +// +// Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in +// MapAnnotation( +// coordinate: location.coordinate, +// content: { +// CircleText(text: node.user!.shortName ?? "???", color: .accentColor) +// } +// ) +// } +// .frame(idealWidth: bounds.size.width, maxHeight: bounds.size.height / 3) +// } else { +// +// Image(node.user?.hwModel ?? "UNSET") +// .resizable() +// .aspectRatio(contentMode: .fit) +// .frame(width: bounds.size.width, height: bounds.size.height / 2) +// } +// } else { +// +// Image(node.user?.hwModel ?? "UNSET") +// .resizable() +// .aspectRatio(contentMode: .fit) +// .frame(width: bounds.size.width, height: bounds.size.height / 2) +// } +// +// ScrollView { +// +// if node.lastHeard != nil { +// +// HStack { +// +// Image(systemName: "clock.badge.checkmark.fill") +// .font(.title) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.title3) +// } +// .padding() +// Divider() +// } +// +// HStack { +// +// VStack(alignment: .center) { +// Text("AKA").font(.title2).fixedSize() +// CircleText(text: node.user?.shortName ?? "???", color: .accentColor) +// .offset(y: 10) +// } +// .padding(5) +// +// Divider() +// +// VStack { +// +// Image(node.user!.hwModel ?? "UNSET") +// .resizable() +// .frame(width: 50, height: 50) +// .cornerRadius(5) +// +// Text(String(node.user!.hwModel ?? "UNSET")) +// .font(.callout).fixedSize() +// } +// .padding(5) +// +// if node.snr > 0 { +// Divider() +// VStack(alignment: .center) { +// +// Image(systemName: "waveform.path") +// .font(.title) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("SNR").font(.title2).fixedSize() +// Text(String(node.snr)) +// .font(.title2) +// .foregroundColor(.gray) +// .fixedSize() +// } +// .padding(5) +// } +// +// if node.positions!.count > 0 { +// +// let mostRecent = node.positions?.lastObject as! PositionEntity +// +// Divider() +// +// VStack(alignment: .center) { +// +// BatteryIcon(batteryLevel: mostRecent.batteryLevel, font: .title, color: .accentColor) +// .padding(.bottom) +// if mostRecent.batteryLevel > 0 { +// +// Text("Battery") +// .font(.title2) +// .fixedSize() +// .textCase(.uppercase) +// Text(String(mostRecent.batteryLevel) + "%") +// .font(.title2) +// .foregroundColor(.gray) +// .symbolRenderingMode(.hierarchical) +// } else { +// +// Text("Powered") +// .font(.callout) +// .fixedSize() +// .textCase(.uppercase) +// } +// } +// .padding(5) +// } +// } +// .padding(4) +// +// Divider() +// +// HStack(alignment: .center) { +// VStack { +// HStack { +// Image(systemName: "person") +// .font(.title2) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("User Id:").font(.title2) +// } +// Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray) +// } +// Divider() +// VStack { +// HStack { +// Image(systemName: "number") +// .font(.title2) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("Node Number:").font(.title2) +// } +// Text(String(node.num)).font(.title3).foregroundColor(.gray) +// } +// } +// .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 { +// +// Divider() +// +// HStack { +// +// Image(systemName: "location.circle.fill") +// .font(.title) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("Location History").font(.title2) +// } +// .padding() +// +// Divider() +// +// ForEach(node.positions!.array as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in +// +// if mappin.coordinate != nil { +// +// VStack { +// +// HStack { +// +// Image(systemName: "mappin.and.ellipse").foregroundColor(.accentColor) // .font(.subheadline) +// Text("Lat/Long:").font(.caption) +// Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))") +// .foregroundColor(.gray) +// .font(.caption) +// +// Image(systemName: "arrow.up.arrow.down.circle") +// .font(.subheadline) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// +// Text("Alt:") +// .font(.caption) +// +// Text("\(String(mappin.altitude))m") +// .foregroundColor(.gray) +// .font(.caption) +// } +// HStack { +// +// Image(systemName: "clock.badge.checkmark.fill") +// .font(.subheadline) +// .foregroundColor(.accentColor) +// .symbolRenderingMode(.hierarchical) +// Text("Time:") +// .font(.caption) +// Text("\(mappin.time!, style: .date) \(mappin.time!, style: .time)") +// .foregroundColor(.gray) +// .font(.caption) +// Divider() +// +// HStack { +// +// BatteryIcon(batteryLevel: mappin.batteryLevel, font: .subheadline, color: .accentColor) +// +// if mappin.batteryLevel > 0 { +// +// Text(String(mappin.batteryLevel) + "%") +// .font(.caption2) +// .foregroundColor(.gray) +// } +// } +// } +// } +// .padding(1) +// Divider() +// } +// } +// } +// } +// }.ignoresSafeArea(.all, edges: [.leading, .trailing]) +// } +// } +// .navigationTitle(node.user!.longName ?? "Unknown") +// .navigationBarTitleDisplayMode(.inline) +// .navigationBarItems(trailing: +// +// ZStack { +// +// ConnectedDevice( +// bluetoothOn: bleManager.isSwitchedOn, +// deviceConnected: bleManager.connectedPeripheral != nil, +// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "???") +// } +// ) +// .onAppear(perform: { +// +// self.bleManager.context = context +// +// }) +// } +//} +// +//struct NodeInfoEntityDetail_Previews: PreviewProvider { +// +// static let bleManager = BLEManager() +// +// static var previews: some View { +// Group { +// +// // NodeDetail(node: node) +// } +// } +//} diff --git a/MeshtasticClient/Views/Nodes/NodeList.swift b/MeshtasticClient/Views/Nodes/NodeList.swift index f7b425f5..58a97a08 100644 --- a/MeshtasticClient/Views/Nodes/NodeList.swift +++ b/MeshtasticClient/Views/Nodes/NodeList.swift @@ -1,129 +1,129 @@ +//// +//// NodeList.swift +//// MeshtasticClient +//// +//// Created by Garth Vander Houwen on 8/7/21. +//// // -// NodeList.swift -// MeshtasticClient +//// Abstract: +//// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. // -// Created by Garth Vander Houwen on 8/7/21. +//import SwiftUI // - -// Abstract: -// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. - -import SwiftUI - -struct NodeList: View { - - @Environment(\.managedObjectContext) var context - @EnvironmentObject var bleManager: BLEManager - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], - animation: .default) - - private var nodes: FetchedResults - - @State private var selection: String? - - var body: some View { - - NavigationView { - - List { - - if nodes.count == 0 { - - Text("Scan for Radios").font(.largeTitle) - Text("No LoRa Mesh Nodes Found").font(.title2) - Text("Go to the bluetooth section in the bottom right menu and click the Start Scanning button to scan for nearby radios and find your Meshtastic device. Make sure your device is powered on and near your phone or tablet.") - .font(.body) - Text("Once the device shows under Available Devices touch the device you want to connect to and it will pull node information over BLE and populate the node list and mesh map in the Meshtastic app.") - Text("Views with bluetooth functionality will show an indicator in the upper right hand corner show if bluetooth is on, and if a device is connected.") - .listRowSeparator(.visible) - - } else { - ForEach( nodes ) { node in - - let index = nodes.firstIndex(where: { $0.id == node.id }) - - NavigationLink(destination: NodeDetail(node: node), tag: String(index!), selection: $selection) { - - let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.name == node.bleName) - - VStack(alignment: .leading) { - - HStack { - - CircleText(text: node.user?.shortName ?? "???", color: Color.accentColor).offset(y: 1).padding(.trailing, 5) - .offset(x: -15) - - if UIDevice.current.userInterfaceIdiom == .pad { Text(node.user?.longName ?? "Unknown").font(.headline) - .offset(x: -15) - } else { - Text(node.user?.longName ?? "Unknown").font(.title).offset(x: -15) - } - } - .padding(.bottom, 10) - - if connected { - HStack(alignment: .bottom) { - - Image(systemName: "repeat.circle.fill").font(.title3) - .foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) - Text("Currently Connected").font(.title3).foregroundColor(Color.accentColor) - } - Spacer() - } - - HStack(alignment: .bottom) { - - Image(systemName: "clock.badge.checkmark.fill").font(.title3).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) - - if node.lastHeard != nil { - Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.subheadline).foregroundColor(.gray) - } else { - Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray) - } - } - } - .padding([.leading, .top, .bottom]) - } - .swipeActions { - - Button { - - context.delete(node) - - do { - - try context.save() - print("Successfully Deleted NodeInfoEntiy: \(node.num)") - - } catch { - - print("Failed to save context after deleting NodeInfoEntity Num: \(node.num)") - } - - } label: { - - Label("Delete from app", systemImage: "trash") - } - .tint(.red) - } - } - } - } - .navigationTitle("All Nodes") - .onAppear { - // self.nodes.returnsObjectsAsFaults = false - self.bleManager.context = context - - if UIDevice.current.userInterfaceIdiom == .pad { - if nodes.count > 0 { - selection = "0" - } - } - } - } - .ignoresSafeArea(.all, edges: [.leading, .trailing]) - .navigationViewStyle(DoubleColumnNavigationViewStyle()) - } -} +//struct NodeList: View { +// +// @Environment(\.managedObjectContext) var context +// @EnvironmentObject var bleManager: BLEManager +// +// @FetchRequest( +// sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], +// animation: .default) +// +// private var nodes: FetchedResults +// +// @State private var selection: String? +// +// var body: some View { +// +// NavigationView { +// +// List { +// +// if nodes.count == 0 { +// +// Text("Scan for Radios").font(.largeTitle) +// Text("No LoRa Mesh Nodes Found").font(.title2) +// Text("Go to the bluetooth section in the bottom right menu and click the Start Scanning button to scan for nearby radios and find your Meshtastic device. Make sure your device is powered on and near your phone or tablet.") +// .font(.body) +// Text("Once the device shows under Available Devices touch the device you want to connect to and it will pull node information over BLE and populate the node list and mesh map in the Meshtastic app.") +// Text("Views with bluetooth functionality will show an indicator in the upper right hand corner show if bluetooth is on, and if a device is connected.") +// .listRowSeparator(.visible) +// +// } else { +// ForEach( nodes ) { node in +// +// let index = nodes.firstIndex(where: { $0.id == node.id }) +// +// NavigationLink(destination: NodeDetail(node: node), tag: String(index!), selection: $selection) { +// +// let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.name == node.bleName) +// +// VStack(alignment: .leading) { +// +// HStack { +// +// CircleText(text: node.user?.shortName ?? "???", color: Color.accentColor).offset(y: 1).padding(.trailing, 5) +// .offset(x: -15) +// +// if UIDevice.current.userInterfaceIdiom == .pad { Text(node.user?.longName ?? "Unknown").font(.headline) +// .offset(x: -15) +// } else { +// Text(node.user?.longName ?? "Unknown").font(.title).offset(x: -15) +// } +// } +// .padding(.bottom, 10) +// +// if connected { +// HStack(alignment: .bottom) { +// +// Image(systemName: "repeat.circle.fill").font(.title3) +// .foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) +// Text("Currently Connected").font(.title3).foregroundColor(Color.accentColor) +// } +// Spacer() +// } +// +// HStack(alignment: .bottom) { +// +// Image(systemName: "clock.badge.checkmark.fill").font(.title3).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) +// +// if node.lastHeard != nil { +// Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.subheadline).foregroundColor(.gray) +// } else { +// Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray) +// } +// } +// } +// .padding([.leading, .top, .bottom]) +// } +// .swipeActions { +// +// Button { +// +// context.delete(node) +// +// do { +// +// try context.save() +// print("Successfully Deleted NodeInfoEntiy: \(node.num)") +// +// } catch { +// +// print("Failed to save context after deleting NodeInfoEntity Num: \(node.num)") +// } +// +// } label: { +// +// Label("Delete from app", systemImage: "trash") +// } +// .tint(.red) +// } +// } +// } +// } +// .navigationTitle("All Nodes") +// .onAppear { +// // self.nodes.returnsObjectsAsFaults = false +// self.bleManager.context = context +// +// if UIDevice.current.userInterfaceIdiom == .pad { +// if nodes.count > 0 { +// selection = "0" +// } +// } +// } +// } +// .ignoresSafeArea(.all, edges: [.leading, .trailing]) +// .navigationViewStyle(DoubleColumnNavigationViewStyle()) +// } +//} From 461db4275d842cc9868b934b2b3da931d37d87ab Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Jan 2022 08:18:36 -0800 Subject: [PATCH 04/20] Turn contacts list back on --- .../Views/Bluetooth/Connect.swift | 8 +- MeshtasticClient/Views/ContentView.swift | 14 +- .../Views/Messages/Contacts.swift | 4 +- MeshtasticClient/Views/Nodes/NodeDetail.swift | 596 +++++++++--------- MeshtasticClient/Views/Nodes/NodeList.swift | 252 ++++---- 5 files changed, 437 insertions(+), 437 deletions(-) diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index 897f8f21..d490fcdc 100644 --- a/MeshtasticClient/Views/Bluetooth/Connect.swift +++ b/MeshtasticClient/Views/Bluetooth/Connect.swift @@ -204,9 +204,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 +216,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 327c55ef..db8610ae 100644 --- a/MeshtasticClient/Views/ContentView.swift +++ b/MeshtasticClient/Views/ContentView.swift @@ -41,13 +41,13 @@ struct ContentView: View { .symbolVariant(.none) } .tag(Tab.ble) -// NodeList() -// .tabItem { -// Label("Nodes", systemImage: "flipphone") -// .symbolRenderingMode(.hierarchical) -// .symbolVariant(.none) -// } -// .tag(Tab.nodes) + NodeList() + .tabItem { + Label("Nodes", systemImage: "flipphone") + .symbolRenderingMode(.hierarchical) + .symbolVariant(.none) + } + .tag(Tab.nodes) NodeMap() .tabItem { Label("Mesh Map", systemImage: "map") diff --git a/MeshtasticClient/Views/Messages/Contacts.swift b/MeshtasticClient/Views/Messages/Contacts.swift index 84e627a7..7b9bc067 100644 --- a/MeshtasticClient/Views/Messages/Contacts.swift +++ b/MeshtasticClient/Views/Messages/Contacts.swift @@ -36,7 +36,7 @@ struct Contacts: View { 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) { + if user.num != currentUserNum && (user.num == bleManager.broadcastNodeNum || mostRecentDM != nil) { NavigationLink(destination: UserMessageList(user: user) .environment(\.managedObjectContext, self.context)) { @@ -96,7 +96,7 @@ struct Contacts: View { } } - } else if false {// self.bleManager.connectedPeripheral == nil || ((self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0) != user.num) { + } else if self.bleManager.connectedPeripheral == nil || ((self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0) != user.num) { NavigationLink(destination: UserMessageList(user: user)) { diff --git a/MeshtasticClient/Views/Nodes/NodeDetail.swift b/MeshtasticClient/Views/Nodes/NodeDetail.swift index e5bb21db..9cf57eb3 100644 --- a/MeshtasticClient/Views/Nodes/NodeDetail.swift +++ b/MeshtasticClient/Views/Nodes/NodeDetail.swift @@ -1,298 +1,298 @@ -///* -//Abstract: -//A view showing the details for a node. -//*/ -// -//import SwiftUI -//import MapKit -//import CoreLocation -// -//struct NodeDetail: View { -// -// @Environment(\.managedObjectContext) var context -// @EnvironmentObject var bleManager: BLEManager -// -// var node: NodeInfoEntity -// -// var body: some View { -// -// HStack { -// -// GeometryReader { bounds in -// -// VStack { -// -// if node.positions?.count ?? 0 > 0 { -// -// let mostRecent = node.positions?.lastObject as! PositionEntity -// -// if mostRecent.coordinate != nil { -// -// let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!) -// -// let regionBinding = Binding( -// get: { -// MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) -// }, -// set: { _ in } -// ) -// let annotations = [MapLocation(name: node.user!.shortName ?? "???", coordinate: mostRecent.coordinate!)] -// -// Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in -// MapAnnotation( -// coordinate: location.coordinate, -// content: { -// CircleText(text: node.user!.shortName ?? "???", color: .accentColor) -// } -// ) -// } -// .frame(idealWidth: bounds.size.width, maxHeight: bounds.size.height / 3) -// } else { -// -// Image(node.user?.hwModel ?? "UNSET") -// .resizable() -// .aspectRatio(contentMode: .fit) -// .frame(width: bounds.size.width, height: bounds.size.height / 2) -// } -// } else { -// -// Image(node.user?.hwModel ?? "UNSET") -// .resizable() -// .aspectRatio(contentMode: .fit) -// .frame(width: bounds.size.width, height: bounds.size.height / 2) -// } -// -// ScrollView { -// -// if node.lastHeard != nil { -// -// HStack { -// -// Image(systemName: "clock.badge.checkmark.fill") -// .font(.title) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.title3) -// } -// .padding() -// Divider() -// } -// -// HStack { -// -// VStack(alignment: .center) { -// Text("AKA").font(.title2).fixedSize() -// CircleText(text: node.user?.shortName ?? "???", color: .accentColor) -// .offset(y: 10) -// } -// .padding(5) -// -// Divider() -// -// VStack { -// -// Image(node.user!.hwModel ?? "UNSET") -// .resizable() -// .frame(width: 50, height: 50) -// .cornerRadius(5) -// -// Text(String(node.user!.hwModel ?? "UNSET")) -// .font(.callout).fixedSize() -// } -// .padding(5) -// -// if node.snr > 0 { -// Divider() -// VStack(alignment: .center) { -// -// Image(systemName: "waveform.path") -// .font(.title) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("SNR").font(.title2).fixedSize() -// Text(String(node.snr)) -// .font(.title2) -// .foregroundColor(.gray) -// .fixedSize() -// } -// .padding(5) -// } -// -// if node.positions!.count > 0 { -// -// let mostRecent = node.positions?.lastObject as! PositionEntity -// -// Divider() -// -// VStack(alignment: .center) { -// -// BatteryIcon(batteryLevel: mostRecent.batteryLevel, font: .title, color: .accentColor) -// .padding(.bottom) -// if mostRecent.batteryLevel > 0 { -// -// Text("Battery") -// .font(.title2) -// .fixedSize() -// .textCase(.uppercase) -// Text(String(mostRecent.batteryLevel) + "%") -// .font(.title2) -// .foregroundColor(.gray) -// .symbolRenderingMode(.hierarchical) -// } else { -// -// Text("Powered") -// .font(.callout) -// .fixedSize() -// .textCase(.uppercase) -// } -// } -// .padding(5) -// } -// } -// .padding(4) -// -// Divider() -// -// HStack(alignment: .center) { -// VStack { -// HStack { -// Image(systemName: "person") -// .font(.title2) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("User Id:").font(.title2) -// } -// Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray) -// } -// Divider() -// VStack { -// HStack { -// Image(systemName: "number") -// .font(.title2) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("Node Number:").font(.title2) -// } -// Text(String(node.num)).font(.title3).foregroundColor(.gray) -// } -// } -// .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 { -// -// Divider() -// -// HStack { -// -// Image(systemName: "location.circle.fill") -// .font(.title) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("Location History").font(.title2) -// } -// .padding() -// -// Divider() -// -// ForEach(node.positions!.array as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in -// -// if mappin.coordinate != nil { -// -// VStack { -// -// HStack { -// -// Image(systemName: "mappin.and.ellipse").foregroundColor(.accentColor) // .font(.subheadline) -// Text("Lat/Long:").font(.caption) -// Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))") -// .foregroundColor(.gray) -// .font(.caption) -// -// Image(systemName: "arrow.up.arrow.down.circle") -// .font(.subheadline) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// -// Text("Alt:") -// .font(.caption) -// -// Text("\(String(mappin.altitude))m") -// .foregroundColor(.gray) -// .font(.caption) -// } -// HStack { -// -// Image(systemName: "clock.badge.checkmark.fill") -// .font(.subheadline) -// .foregroundColor(.accentColor) -// .symbolRenderingMode(.hierarchical) -// Text("Time:") -// .font(.caption) -// Text("\(mappin.time!, style: .date) \(mappin.time!, style: .time)") -// .foregroundColor(.gray) -// .font(.caption) -// Divider() -// -// HStack { -// -// BatteryIcon(batteryLevel: mappin.batteryLevel, font: .subheadline, color: .accentColor) -// -// if mappin.batteryLevel > 0 { -// -// Text(String(mappin.batteryLevel) + "%") -// .font(.caption2) -// .foregroundColor(.gray) -// } -// } -// } -// } -// .padding(1) -// Divider() -// } -// } -// } -// } -// }.ignoresSafeArea(.all, edges: [.leading, .trailing]) -// } -// } -// .navigationTitle(node.user!.longName ?? "Unknown") -// .navigationBarTitleDisplayMode(.inline) -// .navigationBarItems(trailing: -// -// ZStack { -// -// ConnectedDevice( -// bluetoothOn: bleManager.isSwitchedOn, -// deviceConnected: bleManager.connectedPeripheral != nil, -// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "???") -// } -// ) -// .onAppear(perform: { -// -// self.bleManager.context = context -// -// }) -// } -//} -// -//struct NodeInfoEntityDetail_Previews: PreviewProvider { -// -// static let bleManager = BLEManager() -// -// static var previews: some View { -// Group { -// -// // NodeDetail(node: node) -// } -// } -//} +/* +Abstract: +A view showing the details for a node. +*/ + +import SwiftUI +import MapKit +import CoreLocation + +struct NodeDetail: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + var node: NodeInfoEntity + + var body: some View { + + HStack { + + GeometryReader { bounds in + + VStack { + + if node.positions?.count ?? 0 > 0 { + + let mostRecent = node.positions?.lastObject as! PositionEntity + + if mostRecent.coordinate != nil { + + let nodeCoordinatePosition = CLLocationCoordinate2D(latitude: mostRecent.latitude!, longitude: mostRecent.longitude!) + + let regionBinding = Binding( + get: { + MKCoordinateRegion(center: nodeCoordinatePosition, span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)) + }, + set: { _ in } + ) + let annotations = [MapLocation(name: node.user!.shortName ?? "???", coordinate: mostRecent.coordinate!)] + + Map(coordinateRegion: regionBinding, showsUserLocation: true, userTrackingMode: .none, annotationItems: annotations) { location in + MapAnnotation( + coordinate: location.coordinate, + content: { + CircleText(text: node.user!.shortName ?? "???", color: .accentColor) + } + ) + } + .frame(idealWidth: bounds.size.width, maxHeight: bounds.size.height / 3) + } else { + + Image(node.user?.hwModel ?? "UNSET") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: bounds.size.width, height: bounds.size.height / 2) + } + } else { + + Image(node.user?.hwModel ?? "UNSET") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: bounds.size.width, height: bounds.size.height / 2) + } + + ScrollView { + + if node.lastHeard != nil { + + HStack { + + Image(systemName: "clock.badge.checkmark.fill") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.title3) + } + .padding() + Divider() + } + + HStack { + + VStack(alignment: .center) { + Text("AKA").font(.title2).fixedSize() + CircleText(text: node.user?.shortName ?? "???", color: .accentColor) + .offset(y: 10) + } + .padding(5) + + Divider() + + VStack { + + Image(node.user!.hwModel ?? "UNSET") + .resizable() + .frame(width: 50, height: 50) + .cornerRadius(5) + + Text(String(node.user!.hwModel ?? "UNSET")) + .font(.callout).fixedSize() + } + .padding(5) + + if node.snr > 0 { + Divider() + VStack(alignment: .center) { + + Image(systemName: "waveform.path") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("SNR").font(.title2).fixedSize() + Text(String(node.snr)) + .font(.title2) + .foregroundColor(.gray) + .fixedSize() + } + .padding(5) + } + + if node.positions!.count > 0 { + + let mostRecent = node.positions?.lastObject as! PositionEntity + + Divider() + + VStack(alignment: .center) { + + BatteryIcon(batteryLevel: mostRecent.batteryLevel, font: .title, color: .accentColor) + .padding(.bottom) + if mostRecent.batteryLevel > 0 { + + Text("Battery") + .font(.title2) + .fixedSize() + .textCase(.uppercase) + Text(String(mostRecent.batteryLevel) + "%") + .font(.title2) + .foregroundColor(.gray) + .symbolRenderingMode(.hierarchical) + } else { + + Text("Powered") + .font(.callout) + .fixedSize() + .textCase(.uppercase) + } + } + .padding(5) + } + } + .padding(4) + + Divider() + + HStack(alignment: .center) { + VStack { + HStack { + Image(systemName: "person") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("User Id:").font(.title2) + } + Text(node.user?.userId ?? "??????").font(.title3).foregroundColor(.gray) + } + Divider() + VStack { + HStack { + Image(systemName: "number") + .font(.title2) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Node Number:").font(.title2) + } + Text(String(node.num)).font(.title3).foregroundColor(.gray) + } + } + .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 { + + Divider() + + HStack { + + Image(systemName: "location.circle.fill") + .font(.title) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Location History").font(.title2) + } + .padding() + + Divider() + + ForEach(node.positions!.array as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in + + if mappin.coordinate != nil { + + VStack { + + HStack { + + Image(systemName: "mappin.and.ellipse").foregroundColor(.accentColor) // .font(.subheadline) + Text("Lat/Long:").font(.caption) + Text("\(String(mappin.latitude ?? 0)) \(String(mappin.longitude ?? 0))") + .foregroundColor(.gray) + .font(.caption) + + Image(systemName: "arrow.up.arrow.down.circle") + .font(.subheadline) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + + Text("Alt:") + .font(.caption) + + Text("\(String(mappin.altitude))m") + .foregroundColor(.gray) + .font(.caption) + } + HStack { + + Image(systemName: "clock.badge.checkmark.fill") + .font(.subheadline) + .foregroundColor(.accentColor) + .symbolRenderingMode(.hierarchical) + Text("Time:") + .font(.caption) + Text("\(mappin.time!, style: .date) \(mappin.time!, style: .time)") + .foregroundColor(.gray) + .font(.caption) + Divider() + + HStack { + + BatteryIcon(batteryLevel: mappin.batteryLevel, font: .subheadline, color: .accentColor) + + if mappin.batteryLevel > 0 { + + Text(String(mappin.batteryLevel) + "%") + .font(.caption2) + .foregroundColor(.gray) + } + } + } + } + .padding(1) + Divider() + } + } + } + } + }.ignoresSafeArea(.all, edges: [.leading, .trailing]) + } + } + .navigationTitle(node.user!.longName ?? "Unknown") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: + + ZStack { + + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "???") + } + ) + .onAppear(perform: { + + self.bleManager.context = context + + }) + } +} + +struct NodeInfoEntityDetail_Previews: PreviewProvider { + + static let bleManager = BLEManager() + + static var previews: some View { + Group { + + // NodeDetail(node: node) + } + } +} diff --git a/MeshtasticClient/Views/Nodes/NodeList.swift b/MeshtasticClient/Views/Nodes/NodeList.swift index 58a97a08..f7b425f5 100644 --- a/MeshtasticClient/Views/Nodes/NodeList.swift +++ b/MeshtasticClient/Views/Nodes/NodeList.swift @@ -1,129 +1,129 @@ -//// -//// NodeList.swift -//// MeshtasticClient -//// -//// Created by Garth Vander Houwen on 8/7/21. -//// // -//// Abstract: -//// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. +// NodeList.swift +// MeshtasticClient // -//import SwiftUI +// Created by Garth Vander Houwen on 8/7/21. // -//struct NodeList: View { -// -// @Environment(\.managedObjectContext) var context -// @EnvironmentObject var bleManager: BLEManager -// -// @FetchRequest( -// sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], -// animation: .default) -// -// private var nodes: FetchedResults -// -// @State private var selection: String? -// -// var body: some View { -// -// NavigationView { -// -// List { -// -// if nodes.count == 0 { -// -// Text("Scan for Radios").font(.largeTitle) -// Text("No LoRa Mesh Nodes Found").font(.title2) -// Text("Go to the bluetooth section in the bottom right menu and click the Start Scanning button to scan for nearby radios and find your Meshtastic device. Make sure your device is powered on and near your phone or tablet.") -// .font(.body) -// Text("Once the device shows under Available Devices touch the device you want to connect to and it will pull node information over BLE and populate the node list and mesh map in the Meshtastic app.") -// Text("Views with bluetooth functionality will show an indicator in the upper right hand corner show if bluetooth is on, and if a device is connected.") -// .listRowSeparator(.visible) -// -// } else { -// ForEach( nodes ) { node in -// -// let index = nodes.firstIndex(where: { $0.id == node.id }) -// -// NavigationLink(destination: NodeDetail(node: node), tag: String(index!), selection: $selection) { -// -// let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.name == node.bleName) -// -// VStack(alignment: .leading) { -// -// HStack { -// -// CircleText(text: node.user?.shortName ?? "???", color: Color.accentColor).offset(y: 1).padding(.trailing, 5) -// .offset(x: -15) -// -// if UIDevice.current.userInterfaceIdiom == .pad { Text(node.user?.longName ?? "Unknown").font(.headline) -// .offset(x: -15) -// } else { -// Text(node.user?.longName ?? "Unknown").font(.title).offset(x: -15) -// } -// } -// .padding(.bottom, 10) -// -// if connected { -// HStack(alignment: .bottom) { -// -// Image(systemName: "repeat.circle.fill").font(.title3) -// .foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) -// Text("Currently Connected").font(.title3).foregroundColor(Color.accentColor) -// } -// Spacer() -// } -// -// HStack(alignment: .bottom) { -// -// Image(systemName: "clock.badge.checkmark.fill").font(.title3).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) -// -// if node.lastHeard != nil { -// Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.subheadline).foregroundColor(.gray) -// } else { -// Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray) -// } -// } -// } -// .padding([.leading, .top, .bottom]) -// } -// .swipeActions { -// -// Button { -// -// context.delete(node) -// -// do { -// -// try context.save() -// print("Successfully Deleted NodeInfoEntiy: \(node.num)") -// -// } catch { -// -// print("Failed to save context after deleting NodeInfoEntity Num: \(node.num)") -// } -// -// } label: { -// -// Label("Delete from app", systemImage: "trash") -// } -// .tint(.red) -// } -// } -// } -// } -// .navigationTitle("All Nodes") -// .onAppear { -// // self.nodes.returnsObjectsAsFaults = false -// self.bleManager.context = context -// -// if UIDevice.current.userInterfaceIdiom == .pad { -// if nodes.count > 0 { -// selection = "0" -// } -// } -// } -// } -// .ignoresSafeArea(.all, edges: [.leading, .trailing]) -// .navigationViewStyle(DoubleColumnNavigationViewStyle()) -// } -//} + +// Abstract: +// A view showing a list of devices that have been seen on the mesh network from the perspective of the connected device. + +import SwiftUI + +struct NodeList: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(key: "lastHeard", ascending: false)], + animation: .default) + + private var nodes: FetchedResults + + @State private var selection: String? + + var body: some View { + + NavigationView { + + List { + + if nodes.count == 0 { + + Text("Scan for Radios").font(.largeTitle) + Text("No LoRa Mesh Nodes Found").font(.title2) + Text("Go to the bluetooth section in the bottom right menu and click the Start Scanning button to scan for nearby radios and find your Meshtastic device. Make sure your device is powered on and near your phone or tablet.") + .font(.body) + Text("Once the device shows under Available Devices touch the device you want to connect to and it will pull node information over BLE and populate the node list and mesh map in the Meshtastic app.") + Text("Views with bluetooth functionality will show an indicator in the upper right hand corner show if bluetooth is on, and if a device is connected.") + .listRowSeparator(.visible) + + } else { + ForEach( nodes ) { node in + + let index = nodes.firstIndex(where: { $0.id == node.id }) + + NavigationLink(destination: NodeDetail(node: node), tag: String(index!), selection: $selection) { + + let connected: Bool = (bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.name == node.bleName) + + VStack(alignment: .leading) { + + HStack { + + CircleText(text: node.user?.shortName ?? "???", color: Color.accentColor).offset(y: 1).padding(.trailing, 5) + .offset(x: -15) + + if UIDevice.current.userInterfaceIdiom == .pad { Text(node.user?.longName ?? "Unknown").font(.headline) + .offset(x: -15) + } else { + Text(node.user?.longName ?? "Unknown").font(.title).offset(x: -15) + } + } + .padding(.bottom, 10) + + if connected { + HStack(alignment: .bottom) { + + Image(systemName: "repeat.circle.fill").font(.title3) + .foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) + Text("Currently Connected").font(.title3).foregroundColor(Color.accentColor) + } + Spacer() + } + + HStack(alignment: .bottom) { + + Image(systemName: "clock.badge.checkmark.fill").font(.title3).foregroundColor(.accentColor).symbolRenderingMode(.hierarchical) + + if node.lastHeard != nil { + Text("Last Heard: \(node.lastHeard!, style: .relative) ago").font(.subheadline).foregroundColor(.gray) + } else { + Text("Last Heard: Unknown").font(.subheadline).foregroundColor(.gray) + } + } + } + .padding([.leading, .top, .bottom]) + } + .swipeActions { + + Button { + + context.delete(node) + + do { + + try context.save() + print("Successfully Deleted NodeInfoEntiy: \(node.num)") + + } catch { + + print("Failed to save context after deleting NodeInfoEntity Num: \(node.num)") + } + + } label: { + + Label("Delete from app", systemImage: "trash") + } + .tint(.red) + } + } + } + } + .navigationTitle("All Nodes") + .onAppear { + // self.nodes.returnsObjectsAsFaults = false + self.bleManager.context = context + + if UIDevice.current.userInterfaceIdiom == .pad { + if nodes.count > 0 { + selection = "0" + } + } + } + } + .ignoresSafeArea(.all, edges: [.leading, .trailing]) + .navigationViewStyle(DoubleColumnNavigationViewStyle()) + } +} From 6258d60728ab5622770e73a3f52343b521513f8e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Jan 2022 15:33:53 -0800 Subject: [PATCH 05/20] Hook up contacts view to all messages list --- .../CoreDataSample.xcdatamodel/contents | 4 +- .../Views/Messages/Contacts.swift | 127 +++++++++--------- .../Views/Messages/UserMessageList.swift | 36 +++-- 3 files changed, 86 insertions(+), 81 deletions(-) diff --git a/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents b/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents index 6860e1c6..a93b21d7 100644 --- a/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents +++ b/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents @@ -65,8 +65,8 @@ - - + + diff --git a/MeshtasticClient/Views/Messages/Contacts.swift b/MeshtasticClient/Views/Messages/Contacts.swift index 7b9bc067..76932659 100644 --- a/MeshtasticClient/Views/Messages/Contacts.swift +++ b/MeshtasticClient/Views/Messages/Contacts.swift @@ -23,83 +23,78 @@ struct Contacts: 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 != currentUserNum && (user.num == bleManager.broadcastNodeNum || mostRecentDM != nil) { + + let currentUserNum = self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0 + + let allMessages = user.value(forKey: "allMessages") as! [MessageEntity] + + NavigationLink(destination: UserMessageList(user: user).environment(\.managedObjectContext, self.context)) { + + if allMessages.count > 0 { - NavigationLink(destination: UserMessageList(user: user) - .environment(\.managedObjectContext, self.context)) { + 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 { - HStack { + CircleText(text: user.shortName ?? "???", color: Color.blue) + } + .padding([.leading, .trailing]) + + VStack { + + HStack { + + VStack { - 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) + Text(user.longName ?? "Unknown").font(.headline).fixedSize() } - .listRowSeparator(.hidden).frame(height: 5) + + VStack { - HStack(alignment: .top) { - Text(mostRecentDM != nil ? mostRecentDM?.messagePayload as! String : (mostRecentBC.messagePayload ?? "Unknown" )) + 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, 15) + } } + .padding(.top) } - } - - } else if self.bleManager.connectedPeripheral == nil || ((self.bleManager.connectedPeripheral != nil ? self.bleManager.connectedPeripheral.num : 0) != user.num) { - - NavigationLink(destination: UserMessageList(user: user)) { - + + } else { + HStack { VStack { diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index b8393215..b5081c42 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -27,6 +27,8 @@ struct UserMessageList: View { @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 + + @State var mergedMessageList: NSMutableOrderedSet? var body: some View { @@ -36,11 +38,20 @@ struct UserMessageList: View { // List { - ScrollViewReader { _ in + ScrollViewReader { scrollView in ScrollView { - - if user.receivedMessages?.count ?? 0 > 0 { + // Use fetched property + let allMessages = user.value(forKey: "allMessages") + as! [MessageEntity] + + if allMessages.count > 0 { + + + let mergedMessageList = user.receivedMessages!.mutableCopy() as? NSMutableOrderedSet + //mergedMessageList?.union(user.sentMessages!) + + // mergedMessageList?.append(<#T##Self.Output#>).addObjects(from: user.sentMessages!.mutableCopy() as! [Any]) ForEach( user.receivedMessages?.array as! [MessageEntity], id: \.self) { (message: MessageEntity) in @@ -102,7 +113,15 @@ struct UserMessageList: View { .listRowSeparator(.hidden) } } + .onAppear(perform: { + + self.bleManager.context = context + if mergedMessageList?.count ?? 0 > 0 { + scrollView.scrollTo((mergedMessageList![mergedMessageList!.count-1] as AnyObject).id, anchor: .bottom) + } + }) } + // } // .padding(.top) @@ -157,14 +176,6 @@ struct UserMessageList: View { 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 = "" - } - } } }) { @@ -197,10 +208,9 @@ struct UserMessageList: View { } } .onAppear(perform: { - + self.bleManager.context = context }) } - } From e4e4e381a926ea9607e4f8185e30ea7860048137 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Jan 2022 15:45:00 -0800 Subject: [PATCH 06/20] Working DM's with contact list --- .../Views/Messages/UserMessageList.swift | 240 +++++++++--------- 1 file changed, 114 insertions(+), 126 deletions(-) diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index b5081c42..45fe3852 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -32,160 +32,148 @@ struct UserMessageList: View { var body: some View { -// HStack { + VStack { + + ScrollViewReader { scrollView in - VStack { - - // List { + ScrollView { + // Use fetched property + let allMessages = user.value(forKey: "allMessages") + as! [MessageEntity] + + if allMessages.count > 0 { - ScrollViewReader { scrollView in - - ScrollView { - // Use fetched property - let allMessages = user.value(forKey: "allMessages") - as! [MessageEntity] + ForEach( allMessages ) { (message: MessageEntity) in - if allMessages.count > 0 { + let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false ) - let mergedMessageList = user.receivedMessages!.mutableCopy() as? NSMutableOrderedSet - //mergedMessageList?.union(user.sentMessages!) - - // mergedMessageList?.append(<#T##Self.Output#>).addObjects(from: user.sentMessages!.mutableCopy() as! [Any]) - - ForEach( user.receivedMessages?.array as! [MessageEntity], id: \.self) { (message: MessageEntity) in + if message.toUser!.num == Int64(bleManager.broadcastNodeNum) || ((bleManager.connectedPeripheral) != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : true { - // HStack { - let currentUser: Bool = (bleManager.connectedPeripheral == nil) ? false : ((bleManager.connectedPeripheral != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : false ) + + HStack (alignment: .top) { + + if currentUser { Spacer(minLength:50) } + if !currentUser { - 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 + 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) { + print("I want to delete message: \(message.messageId)") + self.showDeleteMessageAlert = true + self.deleteMessageId = message.messageId + print(deleteMessageId) + }) + .padding(.leading) + } + + 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)) + 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) + 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(.trailing) - .frame(maxWidth: .infinity) + .padding(.bottom, 10) } - // } + if !currentUser { + Spacer(minLength:50) + } + } + .padding(.trailing) + .frame(maxWidth: .infinity) } - .listRowSeparator(.hidden) - } + // } } - .onAppear(perform: { - - self.bleManager.context = context - if mergedMessageList?.count ?? 0 > 0 { - scrollView.scrollTo((mergedMessageList![mergedMessageList!.count-1] as AnyObject).id, anchor: .bottom) + .listRowSeparator(.hidden) + } + } + .onAppear(perform: { + + self.bleManager.context = context + if mergedMessageList?.count ?? 0 > 0 { + scrollView.scrollTo((mergedMessageList![mergedMessageList!.count-1] as AnyObject).id, 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 } }) - } - - // } - // .padding(.top) - - HStack(alignment: .top) { + .keyboardType(kbType!) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { - 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 + Button("Dismiss Keyboard") { + focusedField = nil } - }) - .keyboardType(kbType!) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { + .font(.subheadline) - Button("Dismiss Keyboard") { - focusedField = nil - } + Spacer() + + ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) + .frame(width: 130) + .padding(5) .font(.subheadline) - - Spacer() - - ProgressView("Bytes: \(totalBytes) / \(maxbytes)", value: Double(totalBytes), total: Double(maxbytes)) - .frame(width: 130) - .padding(5) - .font(.subheadline) - .accentColor(.accentColor) - } + .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 } + .padding(.horizontal, 8) + .focused($focusedField, equals: .messageText) + .multilineTextAlignment(.leading) + .frame(minHeight: 100, maxHeight: 160) - }) { - Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) - } + Text(typingMessage).opacity(0).padding(.all, 0) } - .padding(.all, 15) + .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) + .padding(.bottom, 15) + + Button(action: { + if bleManager.sendMessage(message: typingMessage, toUserNum: user.num) { + typingMessage = "" + focusedField = nil + } + + }) { + Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) + } + } -// } + .padding(.all, 15) + } + .navigationViewStyle(.stack) .navigationBarTitleDisplayMode(.inline) .toolbar { From e17091ff4fa9a3c291216be0c55ea9d81300bfff Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Jan 2022 19:08:36 -0800 Subject: [PATCH 07/20] V 1.43 Build 2 - Complete DM feature. Save the broadcast users if it does not exist when saving a myInfo --- Meshtastic Client.xcodeproj/project.pbxproj | 6 +++-- MeshtasticClient/Helpers/BLEManager.swift | 24 ++++++++++++++++- MeshtasticClient/Info.plist | 2 +- .../Views/Messages/UserMessageList.swift | 26 ++++++++++++++----- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index 98ec63f9..ddfb97c7 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 = 2; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -727,7 +728,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.42; + MARKETING_VERSION = 1.43; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -745,6 +746,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -754,7 +756,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.42; + MARKETING_VERSION = 1.43; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index 85a7c0a5..19e5afb1 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -425,7 +425,29 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph 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) 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/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 45fe3852..69f73223 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -28,18 +28,17 @@ struct UserMessageList: View { @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 - @State var mergedMessageList: NSMutableOrderedSet? + @State var messageCount = 0 var body: some View { VStack { + let allMessages = user.value(forKey: "allMessages") as! [MessageEntity] + ScrollViewReader { scrollView in ScrollView { - // Use fetched property - let allMessages = user.value(forKey: "allMessages") - as! [MessageEntity] if allMessages.count > 0 { @@ -106,10 +105,25 @@ struct UserMessageList: View { .onAppear(perform: { self.bleManager.context = context - if mergedMessageList?.count ?? 0 > 0 { - scrollView.scrollTo((mergedMessageList![mergedMessageList!.count-1] as AnyObject).id, anchor: .bottom) + messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0)) + + if messageCount > 0 { + + //scrollView.scrollTo(allMessages[allMessages.count-1].id, anchor: .bottom) + //scrollView.scrollTo(allMessages[allMessages.endIndex - 1]) + //scrollView.scrollTo((allMessages[messageCount-1] as AnyObject).id, anchor: .bottom) + } }) + .onChange(of: user, perform: { newValue in + + messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0)) + if messageCount > 0 { + + //scrollView.scrollTo((allMessages[messageCount-1] as AnyObject).id, anchor: .bottom) + } + } + ) } From 550ab11b1274b9eb35494a9235f9d4d53831c684 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Jan 2022 21:21:04 -0800 Subject: [PATCH 08/20] Tapback menus --- MeshtasticClient/Helpers/Extensions.swift | 16 ++++ .../Views/Messages/UserMessageList.swift | 78 ++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) 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/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 69f73223..2ea1c596 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -35,7 +35,7 @@ struct UserMessageList: View { VStack { let allMessages = user.value(forKey: "allMessages") as! [MessageEntity] - + ScrollViewReader { scrollView in ScrollView { @@ -70,11 +70,85 @@ struct UserMessageList: View { VStack(alignment: currentUser ? .trailing : .leading) { Text(message.messagePayload ?? "EMPTY MESSAGE") - .textSelection(.enabled) + //.textSelection(.enabled) .padding(10) .foregroundColor(.white) .background(currentUser ? Color.blue : Color(.darkGray)) .cornerRadius(15) + .contextMenu { + Menu("Tapback Reaction") { + Button(action: { + // Send Heart Tapback + }) { + Text("Heart") + let image = "â¤ī¸".image() + Image(uiImage: image!) + } + Button(action: { + // Send Heart Tapback + }) { + Text("Thumbs Up") + let image = "👍".image() + Image(uiImage: image!) + } + Button(action: { + // Send Heart Tapback + }) { + Text("Thumbs Down") + let image = "👎".image() + Image(uiImage: image!) + } + Button(action: { + // Send ROFL Tapback + }) { + Text("HaHa") + let image = "đŸ¤Ŗ".image() + Image(uiImage: image!) + } + Button(action: { + // Send Heart Tapback + }) { + Text("Exclamation Mark") + let image = "â€ŧī¸".image() + Image(uiImage: image!) + } + Button(action: { + // Send Heart Tapback + }) { + Text("Question Mark") + let image = "❓".image() + Image(uiImage: image!) + } + Button(action: { + // Send Poop Tapback + }) { + Text("Poop") + let image = "💩".image() + Image(uiImage: image!) + } + + } + Button(action: { + // copy the content to the paste board + }) { + Text("Reply") + Image(systemName: "arrowshape.turn.up.left.2.fill") + } + Button(action: { + // copy the content to the paste board + }) { + Text("Copy") + Image(systemName: "doc.on.doc") + } + Divider() + Button(role: .destructive, action: { + // copy the content to the paste board + }) { + Text("Delete") + Image(systemName: "trash") + } + + } HStack(spacing: 4) { From 5baefba14a91770dac9ff23715785372e822487a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Jan 2022 21:44:47 -0800 Subject: [PATCH 09/20] Implement delete message and message copy in context menu --- .../Views/Messages/UserMessageList.swift | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 2ea1c596..6857afcc 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -46,10 +46,8 @@ struct UserMessageList: View { 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) } @@ -57,20 +55,12 @@ struct UserMessageList: View { 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) - }) - .padding(.leading) + .padding(.leading) } VStack(alignment: currentUser ? .trailing : .leading) { Text(message.messagePayload ?? "EMPTY MESSAGE") - //.textSelection(.enabled) .padding(10) .foregroundColor(.white) .background(currentUser ? Color.blue : Color(.darkGray)) @@ -135,19 +125,20 @@ struct UserMessageList: View { Image(systemName: "arrowshape.turn.up.left.2.fill") } Button(action: { - // copy the content to the paste board + UIPasteboard.general.string = message.messagePayload }) { Text("Copy") Image(systemName: "doc.on.doc") } Divider() Button(role: .destructive, action: { - // copy the content to the paste board + self.showDeleteMessageAlert = true + self.deleteMessageId = message.messageId + print(deleteMessageId) }) { Text("Delete") Image(systemName: "trash") } - } HStack(spacing: 4) { @@ -170,8 +161,29 @@ struct UserMessageList: View { } .padding(.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) } From 2925d3e22cb37c0362b440a713e5693e3ec92c1c Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 1 Jan 2022 22:55:25 -0800 Subject: [PATCH 10/20] Implement message replies --- MeshtasticClient/Helpers/BLEManager.swift | 12 +++++++++--- .../Views/Messages/UserMessageList.swift | 12 ++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index 19e5afb1..626fceef 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -493,7 +493,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 { @@ -728,7 +728,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 { @@ -846,7 +846,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } // Send Broadcast Message - public func sendMessage(message: String, toUserNum: Int64) -> Bool { + public func sendMessage(message: String, toUserNum: Int64, replyTo: Int64) -> Bool { var success = false @@ -902,6 +902,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph newMessage.receivedACK = false newMessage.direction = "IN" newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum }) + if replyTo > 0 { + newMessage.replyID = replyTo + } if newMessage.toUser == nil { let bcu: UserEntity = UserEntity(context: context!) @@ -925,6 +928,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph var meshPacket = MeshPacket() meshPacket.to = UInt32(toUserNum) meshPacket.from = UInt32(fromUserNum) + if replyTo > 0 { + meshPacket.replyID = UInt32(replyTo) + } meshPacket.decoded = dataMessage meshPacket.wantAck = true diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 6857afcc..0542d931 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -27,6 +27,7 @@ struct UserMessageList: View { @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 + @State private var replyMessageId: Int64 = 0 @State var messageCount = 0 @@ -55,7 +56,6 @@ struct UserMessageList: View { if !currentUser { CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) - .padding(.leading) } VStack(alignment: currentUser ? .trailing : .leading) { @@ -66,7 +66,7 @@ struct UserMessageList: View { .background(currentUser ? Color.blue : Color(.darkGray)) .cornerRadius(15) .contextMenu { - Menu("Tapback Reaction") { + Menu("Tapback response") { Button(action: { // Send Heart Tapback }) { @@ -119,7 +119,10 @@ struct UserMessageList: View { } Button(action: { - // copy the content to the paste board + 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") @@ -261,9 +264,10 @@ struct UserMessageList: View { .padding(.bottom, 15) Button(action: { - if bleManager.sendMessage(message: typingMessage, toUserNum: user.num) { + if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, replyTo: replyMessageId) { typingMessage = "" focusedField = nil + replyMessageId = 0 } }) { From 7741d63e0c230b46fecc64e703f1319d95ef817e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 2 Jan 2022 10:05:13 -0800 Subject: [PATCH 11/20] Updated circle text helper --- Meshtastic Client.xcodeproj/project.pbxproj | 4 +-- .../Views/Helpers/CircleText.swift | 11 ++++-- .../Views/Messages/Contacts.swift | 2 +- .../Views/Messages/UserMessageList.swift | 35 ++++++++++++------- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index ddfb97c7..bc566a69 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -718,7 +718,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -746,7 +746,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; diff --git a/MeshtasticClient/Views/Helpers/CircleText.swift b/MeshtasticClient/Views/Helpers/CircleText.swift index de7c8548..021a0f02 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? = 40 + var fontSize: CGFloat? = 16 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/Contacts.swift b/MeshtasticClient/Views/Messages/Contacts.swift index 76932659..2081819f 100644 --- a/MeshtasticClient/Views/Messages/Contacts.swift +++ b/MeshtasticClient/Views/Messages/Contacts.swift @@ -28,7 +28,7 @@ struct Contacts: View { let allMessages = user.value(forKey: "allMessages") as! [MessageEntity] - NavigationLink(destination: UserMessageList(user: user).environment(\.managedObjectContext, self.context)) { + NavigationLink(destination: UserMessageList(user: user)) { if allMessages.count > 0 { diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 0542d931..24da3a26 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -144,6 +144,17 @@ struct UserMessageList: View { } } + +// VStack (alignment: .trailing) { +// +// HStack { +// let image = "â¤ī¸".image(fontSize: 26) +// Image(uiImage: image!).font(.caption) +// CircleText(text: "AKA", color: .blue, circleSize: 28, fontSize: 11) +// } +// .padding(0) +// } + HStack(spacing: 4) { let time = Int32(message.messageTimestamp) @@ -158,11 +169,13 @@ struct UserMessageList: View { } .padding(.bottom, 10) } + .id(allMessages.firstIndex(of: message)) + if !currentUser { Spacer(minLength:50) } } - .padding(.trailing) + .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."), @@ -189,30 +202,28 @@ struct UserMessageList: View { } } .listRowSeparator(.hidden) + } } .onAppear(perform: { self.bleManager.context = context + messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0)) if messageCount > 0 { - - //scrollView.scrollTo(allMessages[allMessages.count-1].id, anchor: .bottom) - //scrollView.scrollTo(allMessages[allMessages.endIndex - 1]) - //scrollView.scrollTo((allMessages[messageCount-1] as AnyObject).id, anchor: .bottom) - + scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) } }) .onChange(of: user, perform: { newValue in - messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0)) - if messageCount > 0 { - - //scrollView.scrollTo((allMessages[messageCount-1] as AnyObject).id, anchor: .bottom) - } + messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0)) + + if messageCount > 0 { + + scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) } - ) + }) } From e7a74145ade07d6675920d364ea87654f159ad8b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 2 Jan 2022 23:28:51 -0800 Subject: [PATCH 12/20] Implement various text circle sizes --- MeshtasticClient/Helpers/BLEManager.swift | 9 ++++++++- .../Views/Helpers/CircleText.swift | 4 ++-- .../Views/Messages/UserMessageList.swift | 13 ++++++++----- MeshtasticClient/Views/Nodes/NodeDetail.swift | 2 +- .../Views/Settings/AppSettings.swift | 19 ++++++++++--------- 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index 626fceef..f791b683 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -808,7 +808,14 @@ 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())") diff --git a/MeshtasticClient/Views/Helpers/CircleText.swift b/MeshtasticClient/Views/Helpers/CircleText.swift index 021a0f02..814d508b 100644 --- a/MeshtasticClient/Views/Helpers/CircleText.swift +++ b/MeshtasticClient/Views/Helpers/CircleText.swift @@ -8,8 +8,8 @@ import SwiftUI struct CircleText: View { var text: String var color: Color - var circleSize: CGFloat? = 40 - var fontSize: CGFloat? = 16 + var circleSize: CGFloat? = 50 + var fontSize: CGFloat? = 24 var body: some View { diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 24da3a26..17c046cb 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -42,7 +42,11 @@ struct UserMessageList: View { 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 ) @@ -54,8 +58,7 @@ struct UserMessageList: View { if currentUser { Spacer(minLength:50) } if !currentUser { - - CircleText(text: (message.fromUser?.shortName ?? "???"), color: currentUser ? .accentColor : Color(.darkGray)).padding(.all, 5) + CircleText(text: message.fromUser?.shortName ?? "???", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 36, fontSize: 16).padding(.all, 5) } VStack(alignment: currentUser ? .trailing : .leading) { @@ -296,8 +299,8 @@ struct UserMessageList: View { HStack { - CircleText(text: user.shortName ?? "???", color: .blue).fixedSize() - Text(user.longName ?? "Unknown").foregroundColor(.gray).font(.caption2).fixedSize() + CircleText(text: user.shortName ?? "???", color: .blue, circleSize: 42, fontSize: 20).fixedSize() + Text(user.longName ?? "Unknown").font(.headline).fixedSize() } } ToolbarItem(placement: .navigationBarTrailing) { diff --git a/MeshtasticClient/Views/Nodes/NodeDetail.swift b/MeshtasticClient/Views/Nodes/NodeDetail.swift index 9cf57eb3..2d6f8086 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) } ) } diff --git a/MeshtasticClient/Views/Settings/AppSettings.swift b/MeshtasticClient/Views/Settings/AppSettings.swift index 98690e2a..c3ec7c64 100644 --- a/MeshtasticClient/Views/Settings/AppSettings.swift +++ b/MeshtasticClient/Views/Settings/AppSettings.swift @@ -157,7 +157,15 @@ 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()) + } + Section(header: Text("DEBUG")) { // Toggle(isOn: $userSettings.meshActivityLog) { // Label("Log all Mesh activity", systemImage: "network") @@ -170,14 +178,7 @@ struct AppSettings: View { .listRowSeparator(.visible) } } - Section(header: Text("MAP OPTIONS")) { - Picker("Map Type", selection: $userSettings.meshMapType) { - ForEach(MeshMapType.allCases) { map in - Text(map.description) - } - } - .pickerStyle(DefaultPickerStyle()) - } + } } .navigationTitle("App Settings") From f81ad24548eb6e83b55a674d04f93a515cff6638 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 4 Jan 2022 22:57:33 -0800 Subject: [PATCH 13/20] Tapbacks --- Meshtastic Client.xcodeproj/project.pbxproj | 4 +- MeshtasticClient/Helpers/BLEManager.swift | 43 +++++---- .../CoreDataSample.xcdatamodel/contents | 9 +- .../Views/Messages/UserMessageList.swift | 94 ++++++++++++++----- 4 files changed, 105 insertions(+), 45 deletions(-) diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index bc566a69..7ed59af4 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -718,7 +718,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -746,7 +746,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index f791b683..8a4c5afa 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 @@ -419,7 +420,13 @@ 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) + myInfo.messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec) myInfo.minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion) myInfo.maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels) @@ -453,7 +460,10 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph 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) fetchedMyInfo[0].messageTimeoutMsec = Int32(bitPattern: decodedInfo.myInfo.messageTimeoutMsec) fetchedMyInfo[0].minAppVersion = Int32(bitPattern: decodedInfo.myInfo.minAppVersion) fetchedMyInfo[0].maxChannels = Int32(bitPattern: decodedInfo.myInfo.maxChannels) @@ -852,8 +862,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph peripheral.readValue(for: FROMRADIO_characteristic) } - // Send Broadcast Message - public func sendMessage(message: String, toUserNum: Int64, replyTo: Int64) -> Bool { + // Send Message + public func sendMessage(message: String, toUserNum: Int64, isTapback: Bool, replyID: Int64) -> Bool { var success = false @@ -868,18 +878,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 @@ -909,8 +912,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph newMessage.receivedACK = false newMessage.direction = "IN" newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum }) - if replyTo > 0 { - newMessage.replyID = replyTo + newMessage.isTapback = isTapback + + if replyID > 0 { + + newMessage.replyID = replyID } if newMessage.toUser == nil { @@ -922,6 +928,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph bcu.userId = "BROADCASTNODE" newMessage.toUser = bcu } + newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum }) newMessage.messagePayload = message @@ -935,8 +942,8 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph var meshPacket = MeshPacket() meshPacket.to = UInt32(toUserNum) meshPacket.from = UInt32(fromUserNum) - if replyTo > 0 { - meshPacket.replyID = UInt32(replyTo) + if replyID > 0 { + meshPacket.replyID = UInt32(replyID) } meshPacket.decoded = dataMessage meshPacket.wantAck = true diff --git a/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents b/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents index a93b21d7..75fc2789 100644 --- a/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents +++ b/MeshtasticClient/Meshtastic.xcdatamodeld/CoreDataSample.xcdatamodel/contents @@ -2,7 +2,7 @@ - + @@ -10,6 +10,9 @@ + + + @@ -66,11 +69,11 @@ - + - + diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 17c046cb..38571400 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -71,55 +71,96 @@ struct UserMessageList: View { .contextMenu { Menu("Tapback response") { Button(action: { - // Send Heart Tapback + + if bleManager.sendMessage(message: "â¤ī¸", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent â¤ī¸ Tapback") + + } else { print("â¤ī¸ Tapback Failed") } + }) { Text("Heart") let image = "â¤ī¸".image() Image(uiImage: image!) } Button(action: { - // Send Heart Tapback + + if bleManager.sendMessage(message: "👍", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent 👍 Tapback") + + } else { print("👍 Tapback Failed")} + }) { Text("Thumbs Up") let image = "👍".image() Image(uiImage: image!) } Button(action: { - // Send Heart Tapback + + if bleManager.sendMessage(message: "👎", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent 👎 Tapback") + + } else { print("👎 Tapback Failed") } + }) { Text("Thumbs Down") let image = "👎".image() Image(uiImage: image!) } Button(action: { - // Send ROFL Tapback + + if bleManager.sendMessage(message: "đŸ¤Ŗ", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent đŸ¤Ŗ Tapback") + + } else { print("đŸ¤Ŗ Tapback Failed") } + }) { Text("HaHa") let image = "đŸ¤Ŗ".image() Image(uiImage: image!) } Button(action: { - // Send Heart Tapback + + if bleManager.sendMessage(message: "â€ŧī¸", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent â€ŧī¸ Tapback") + + } else { print("â€ŧī¸ Tapback Failed") } + }) { Text("Exclamation Mark") let image = "â€ŧī¸".image() Image(uiImage: image!) } Button(action: { - // Send Heart Tapback + + if bleManager.sendMessage(message: "❓", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent ❓ Tapback") + + } else { print("❓ Tapback Failed") } + }) { Text("Question Mark") let image = "❓".image() Image(uiImage: image!) } Button(action: { - // Send Poop Tapback + + if bleManager.sendMessage(message: "💩", toUserNum: user.num, isTapback: true, replyID: message.messageId) { + + print("Sent 💩 Tapback") + + } else { print("💩 Tapback Failed") } + }) { Text("Poop") let image = "💩".image() Image(uiImage: image!) } - } Button(action: { self.replyMessageId = message.messageId @@ -147,18 +188,28 @@ struct UserMessageList: View { } } - -// VStack (alignment: .trailing) { -// -// HStack { -// let image = "â¤ī¸".image(fontSize: 26) -// Image(uiImage: image!).font(.caption) -// CircleText(text: "AKA", color: .blue, circleSize: 28, fontSize: 11) -// } -// .padding(0) -// } + VStack (alignment: .trailing) { - HStack(spacing: 4) { + let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] + 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) + } + } + } + } + + HStack { let time = Int32(message.messageTimestamp) let messageDate = Date(timeIntervalSince1970: TimeInterval(time)) @@ -170,7 +221,7 @@ struct UserMessageList: View { Text("Unknown").font(.caption2).foregroundColor(.gray) } } - .padding(.bottom, 10) + .padding(4) } .id(allMessages.firstIndex(of: message)) @@ -205,7 +256,6 @@ struct UserMessageList: View { } } .listRowSeparator(.hidden) - } } .onAppear(perform: { @@ -278,7 +328,7 @@ struct UserMessageList: View { .padding(.bottom, 15) Button(action: { - if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, replyTo: replyMessageId) { + if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, isTapback: false, replyID: replyMessageId) { typingMessage = "" focusedField = nil replyMessageId = 0 From ff32894d5a45805b901444d3204c9cee27d394a7 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 5 Jan 2022 00:06:31 -0800 Subject: [PATCH 14/20] Tapbacks with firmware version checking --- Meshtastic Client.xcodeproj/project.pbxproj | 4 +- .../Views/Messages/UserMessageList.swift | 245 ++++++++++-------- 2 files changed, 137 insertions(+), 112 deletions(-) diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index 7ed59af4..47ddc10e 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -718,7 +718,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -746,7 +746,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 38571400..2e104e65 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -32,7 +32,11 @@ struct UserMessageList: View { @State var messageCount = 0 var body: some View { - + + let firmwareVersion = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.firmwareVersion : "0.0.0" + 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] @@ -69,107 +73,115 @@ struct UserMessageList: View { .background(currentUser ? Color.blue : Color(.darkGray)) .cornerRadius(15) .contextMenu { - Menu("Tapback response") { - Button(action: { + + if hasTapbackSupport { + + Menu("Tapback response") { - if bleManager.sendMessage(message: "â¤ī¸", toUserNum: user.num, isTapback: true, replyID: message.messageId) { - - print("Sent â¤ī¸ Tapback") - - } 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") + + } 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") + + } 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") + + } 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") + + + } 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") + + } 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") + + } 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") + + } else { print("💩 Tapback Failed") } + + }) { + Text("Poop") + let image = "💩".image() + Image(uiImage: image!) + } } Button(action: { - - if bleManager.sendMessage(message: "👍", toUserNum: user.num, isTapback: true, replyID: message.messageId) { - - print("Sent 👍 Tapback") - - } else { print("👍 Tapback Failed")} - + self.replyMessageId = message.messageId + self.focusedField = .messageText + + print("I want to reply to \(message.messageId)") }) { - Text("Thumbs Up") - let image = "👍".image() - Image(uiImage: image!) + Text("Reply") + Image(systemName: "arrowshape.turn.up.left.2.fill") } - Button(action: { - - if bleManager.sendMessage(message: "👎", toUserNum: user.num, isTapback: true, replyID: message.messageId) { - - print("Sent 👎 Tapback") - - } 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") - - } 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") - - } 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") - - } 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") - - } 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 @@ -188,27 +200,40 @@ struct UserMessageList: View { } } - VStack (alignment: .trailing) { - + if hasTapbackSupport { + let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] - HStack { + + + if tapbacks.count > 0 { - ForEach( tapbacks ) { (tapback: MessageEntity) in - - VStack { + VStack (alignment: .trailing) { + + HStack { - let image = tapback.messagePayload!.image(fontSize: 20) - Image(uiImage: image!).font(.caption) - Text("\(tapback.fromUser?.shortName ?? "???")") - .font(.caption2) - .foregroundColor(.gray) - .fixedSize() - .padding(.bottom, 1) + 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) From ca179221c177e3ab4a8df7ccbeec37d939f0f37a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 5 Jan 2022 06:37:29 -0800 Subject: [PATCH 15/20] Add minimum supported version functionality, save when connection happens and carry through to the next connection so the disconnected user experience is less strange. --- MeshtasticClient/Helpers/BLEManager.swift | 13 ++++++++----- MeshtasticClient/Views/Bluetooth/Connect.swift | 15 +++++++++++++++ .../Views/Messages/UserMessageList.swift | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/MeshtasticClient/Helpers/BLEManager.swift b/MeshtasticClient/Helpers/BLEManager.swift index 8a4c5afa..69bf1501 100644 --- a/MeshtasticClient/Helpers/BLEManager.swift +++ b/MeshtasticClient/Helpers/BLEManager.swift @@ -28,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 @@ -55,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) @@ -426,6 +427,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph 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) @@ -464,6 +466,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph 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) @@ -823,22 +826,22 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } 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())") + 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 { diff --git a/MeshtasticClient/Views/Bluetooth/Connect.swift b/MeshtasticClient/Views/Bluetooth/Connect.swift index d490fcdc..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)) { diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 2e104e65..94c90c44 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -33,7 +33,7 @@ struct UserMessageList: View { var body: some View { - let firmwareVersion = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.firmwareVersion : "0.0.0" + let firmwareVersion = bleManager.lastConnnectionVersion let minimumVersion = "1.2.50" let hasTapbackSupport = minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(firmwareVersion, options: .numeric) == .orderedSame From 9694637b58ba295d2db3b524773767823ed38821 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Wed, 5 Jan 2022 08:31:30 -0800 Subject: [PATCH 16/20] Display for replies --- Meshtastic Client.xcodeproj/project.pbxproj | 4 ++-- .../Views/Messages/UserMessageList.swift | 20 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index 47ddc10e..fbc29e02 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -718,7 +718,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -746,7 +746,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 94c90c44..2fd4cbb2 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -57,6 +57,24 @@ struct UserMessageList: View { if message.toUser!.num == Int64(bleManager.broadcastNodeNum) || ((bleManager.connectedPeripheral) != nil && bleManager.connectedPeripheral.num == message.fromUser?.num) ? true : true { + + if message.replyID > 0 { + + HStack { + + Text(message.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) } @@ -78,8 +96,6 @@ struct UserMessageList: View { Menu("Tapback response") { - - Button(action: { if bleManager.sendMessage(message: "â¤ī¸", toUserNum: user.num, isTapback: true, replyID: message.messageId) { From 1171ea53c8dbe1b62c64517976d69c7232290ced Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 6 Jan 2022 08:41:44 -0800 Subject: [PATCH 17/20] Refresm usermessagelist better for fetched properties --- Meshtastic Client.xcodeproj/project.pbxproj | 8 ++++---- .../Views/Messages/UserMessageList.swift | 18 +++++++++++++----- MeshtasticClient/Views/Nodes/NodeDetail.swift | 18 +++++++++++------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index fbc29e02..b786ab07 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -718,7 +718,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -732,7 +732,7 @@ 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"; }; @@ -746,7 +746,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 12; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -760,7 +760,7 @@ 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/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 2fd4cbb2..16d6f5dd 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -23,7 +23,7 @@ struct UserMessageList: View { @State var lastTypingMessage = "" @FocusState var focusedField: Field? - var user: UserEntity + @State var user: UserEntity @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 @@ -57,12 +57,13 @@ struct UserMessageList: View { 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(message.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) .padding(10) .overlay( RoundedRectangle(cornerRadius: 18) @@ -101,6 +102,7 @@ struct UserMessageList: View { 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") } @@ -114,6 +116,7 @@ struct UserMessageList: View { 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")} @@ -127,6 +130,7 @@ struct UserMessageList: View { 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") } @@ -140,7 +144,7 @@ struct UserMessageList: View { 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") } @@ -154,6 +158,7 @@ struct UserMessageList: View { 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") } @@ -167,6 +172,7 @@ struct UserMessageList: View { 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") } @@ -180,6 +186,7 @@ struct UserMessageList: View { 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") } @@ -219,7 +226,7 @@ struct UserMessageList: View { if hasTapbackSupport { let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] - + if tapbacks.count > 0 { @@ -311,6 +318,7 @@ struct UserMessageList: View { }) .onChange(of: user, perform: { newValue in + self.context.refresh(user, mergeChanges: true) messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0)) if messageCount > 0 { diff --git a/MeshtasticClient/Views/Nodes/NodeDetail.swift b/MeshtasticClient/Views/Nodes/NodeDetail.swift index 2d6f8086..8ae18b63 100644 --- a/MeshtasticClient/Views/Nodes/NodeDetail.swift +++ b/MeshtasticClient/Views/Nodes/NodeDetail.swift @@ -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) @@ -262,7 +265,8 @@ struct NodeDetail: View { } } } - }.ignoresSafeArea(.all, edges: [.leading, .trailing]) + } + .edgesIgnoringSafeArea([.leading, .trailing]) } } .navigationTitle(node.user!.longName ?? "Unknown") From 19a14496a23a200b3a058758310683e85fdf25bd Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 9 Jan 2022 08:49:22 -0800 Subject: [PATCH 18/20] Update allMessages and tapBack fetchedProperties when they update --- MeshtasticClient/Views/Messages/UserMessageList.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 16d6f5dd..6e101cd4 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -23,7 +23,7 @@ struct UserMessageList: View { @State var lastTypingMessage = "" @FocusState var focusedField: Field? - @State var user: UserEntity + @ObservedObject var user: UserEntity @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 From b5b6fead551960a3c1347cde613b03ad607c90fa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 9 Jan 2022 08:59:14 -0800 Subject: [PATCH 19/20] Fix scroll to bottom when a new message is received --- MeshtasticClient/Views/Messages/UserMessageList.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/MeshtasticClient/Views/Messages/UserMessageList.swift b/MeshtasticClient/Views/Messages/UserMessageList.swift index 6e101cd4..8312afce 100644 --- a/MeshtasticClient/Views/Messages/UserMessageList.swift +++ b/MeshtasticClient/Views/Messages/UserMessageList.swift @@ -316,14 +316,16 @@ struct UserMessageList: View { scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) } }) - .onChange(of: user, perform: { newValue in + .onChange(of: allMessages.count, perform: { count in self.context.refresh(user, mergeChanges: true) - messageCount = ((user.sentMessages?.count ?? 0) + (user.receivedMessages?.count ?? 0)) - if messageCount > 0 { + let index = count - 1 + + if index > 2 { + + scrollView.scrollTo(index, anchor: .bottom) - scrollView.scrollTo(allMessages.firstIndex(of: allMessages.last! ), anchor: .bottom) } }) } From c488129a68781f26bb89248cf2b95ec54e2d3fe5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 9 Jan 2022 08:59:48 -0800 Subject: [PATCH 20/20] V 1.43.13 --- Meshtastic Client.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic Client.xcodeproj/project.pbxproj b/Meshtastic Client.xcodeproj/project.pbxproj index b786ab07..0a4f1479 100644 --- a/Meshtastic Client.xcodeproj/project.pbxproj +++ b/Meshtastic Client.xcodeproj/project.pbxproj @@ -718,7 +718,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -746,7 +746,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = MeshtasticClient/MeshtasticClient.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_ASSET_PATHS = "\"MeshtasticClient/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES;