From 0c3dbf3b26e120824440a57cd5e8fd5a2fc53e89 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 3 Nov 2022 20:26:38 -0700 Subject: [PATCH 01/14] Messages Cleanup --- Meshtastic/Views/Messages/Contacts.swift | 30 +++++++++------------ Meshtastic/Views/Messages/MessageList.swift | 4 +-- Meshtastic/Views/Nodes/NodeDetail.swift | 3 +-- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index dd802a0f..54ad5f5e 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -36,7 +36,7 @@ struct Contacts: View { NavigationSplitView { List { - Section(header: Text("Primary Channel")) { + Section(header: Text("Direct Messages")) { ForEach(users) { (user: UserEntity) in if user.num != bleManager.userSettings?.preferredNodeNum ?? 0 { @@ -52,43 +52,41 @@ struct Contacts: View { HStack { VStack(alignment: .leading) { + HStack { CircleText(text: user.shortName ?? "???", color: Color.blue) .padding(.trailing, 5) - } - VStack { - HStack { VStack { - Text(user.longName ?? "Unknown").font(.headline).fixedSize() + Text(user.longName ?? "Unknown").font(.headline) } - VStack { - + .frame(maxWidth: .infinity, alignment: .leading) + + VStack (alignment: .trailing) { if lastMessageDay == currentDay { Text(lastMessageTime, style: .time ) - .font(.caption) + .font(.callout) .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) + Text(lastMessageTime.formattedDate(format: "MM/dd/yy")) + .font(.callout) + .foregroundColor(.gray) } else if lastMessageDay < (currentDay - 1800) { - Text(lastMessageTime, style: .date) + Text(lastMessageTime.formattedDate(format: "MM/dd/yy")) + .font(.callout) + .foregroundColor(.gray) } } - .frame(maxWidth: .infinity, alignment: .trailing) } HStack(alignment: .top) { Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") - .frame(height: 50) .truncationMode(.tail) .foregroundColor(Color.gray) .frame(maxWidth: .infinity, alignment: .leading) } } - .padding(.top) } } else { HStack { @@ -108,7 +106,6 @@ struct Contacts: View { } HStack(alignment: .top) { Text(" ") - .frame(height: 50 ) .truncationMode(.tail) .foregroundColor(Color.gray) .frame(maxWidth: .infinity, alignment: .leading) @@ -125,7 +122,6 @@ struct Contacts: View { // Display Contacts for the rest of the non admin channels } - .hidden() } .tint(Color(UIColor.systemGray)) .navigationSplitViewStyle(.automatic) diff --git a/Meshtastic/Views/Messages/MessageList.swift b/Meshtastic/Views/Messages/MessageList.swift index 310326c6..6eab8a8e 100644 --- a/Meshtastic/Views/Messages/MessageList.swift +++ b/Meshtastic/Views/Messages/MessageList.swift @@ -422,8 +422,8 @@ struct MessageList: View { .toolbar { ToolbarItem(placement: .principal) { HStack { - CircleText(text: user.shortName ?? "???", color: .blue, circleSize: 42, fontSize: 16).fixedSize() - Text(user.longName ?? "Unknown").font(.headline).fixedSize() + CircleText(text: user.shortName ?? "???", color: .blue, circleSize: 44, fontSize: 14).fixedSize() + Text(user.longName ?? "Unknown").font(.headline) } } ToolbarItem(placement: .navigationBarTrailing) { diff --git a/Meshtastic/Views/Nodes/NodeDetail.swift b/Meshtastic/Views/Nodes/NodeDetail.swift index aa2dca85..6d052431 100644 --- a/Meshtastic/Views/Nodes/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/NodeDetail.swift @@ -431,8 +431,7 @@ struct NodeDetail: View { .offset( y:-40) } .edgesIgnoringSafeArea([.leading, .trailing]) - .navigationTitle((node.user != nil) ? String(node.user!.longName ?? "Unknown") : "Unknown") - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitle((node.user != nil) ? String(node.user!.longName ?? "Unknown") : "Unknown", displayMode: .inline) .padding(.bottom, 10) .navigationBarItems(trailing: From fa4f8ec4b3888fd9b831f087b5758cd336b80cf1 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 3 Nov 2022 20:36:09 -0700 Subject: [PATCH 02/14] Shrink circles on contact list --- Meshtastic/Views/Messages/Contacts.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 54ad5f5e..b0f84f7d 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -53,7 +53,7 @@ struct Contacts: View { HStack { VStack(alignment: .leading) { HStack { - CircleText(text: user.shortName ?? "???", color: Color.blue) + CircleText(text: user.shortName ?? "???", color: Color.blue, circleSize: 52, fontSize: 16) .padding(.trailing, 5) VStack { Text(user.longName ?? "Unknown").font(.headline) @@ -91,7 +91,7 @@ struct Contacts: View { } else { HStack { VStack(alignment: .leading) { - CircleText(text: user.shortName ?? "???", color: Color.blue) + CircleText(text: user.shortName ?? "???", color: Color.blue, circleSize: 52, fontSize: 16) .padding(.trailing, 5) } VStack { From 6c0c7d026bb64eb3f20d840637f1b791552a3df8 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 3 Nov 2022 20:52:14 -0700 Subject: [PATCH 03/14] Make a single contact template, make things take less space --- Meshtastic/Views/Messages/Contacts.swift | 69 ++++++------------------ 1 file changed, 17 insertions(+), 52 deletions(-) diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index b0f84f7d..b80aa884 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -19,8 +19,6 @@ struct Contacts: View { private var users: FetchedResults - - private var prefferedNode: NodeInfoEntity? @FetchRequest( @@ -29,37 +27,32 @@ struct Contacts: View { private var nodes: FetchedResults - @State private var selection: UserEntity? = nil // Nothing selected by default. var body: some View { NavigationSplitView { + List { Section(header: Text("Direct Messages")) { ForEach(users) { (user: UserEntity) in - if user.num != bleManager.userSettings?.preferredNodeNum ?? 0 { - NavigationLink(destination: MessageList(user: user)) { - - if user.messageList.count > 0 { - - let mostRecent = user.num == bleManager.broadcastNodeNum ? user.messageList.last : user.messageList.last(where: { $0.toUser?.num ?? 0 != bleManager.broadcastNodeNum }) - let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) - let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 - let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 - - HStack { - VStack(alignment: .leading) { - HStack { - CircleText(text: user.shortName ?? "???", color: Color.blue, circleSize: 52, fontSize: 16) + let mostRecent = user.num == bleManager.broadcastNodeNum ? user.messageList.last : user.messageList.last(where: { $0.toUser?.num ?? 0 != bleManager.broadcastNodeNum }) + let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) + let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 + let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 + HStack { + VStack(alignment: .leading) { + HStack { + CircleText(text: user.shortName ?? "???", color: Color.blue, circleSize: 52, fontSize: 16) .padding(.trailing, 5) - VStack { - Text(user.longName ?? "Unknown").font(.headline) - } - .frame(maxWidth: .infinity, alignment: .leading) - + VStack { + Text(user.longName ?? "Unknown").font(.headline) + } + .frame(maxWidth: .infinity, alignment: .leading) + + if user.messageList.count > 0 { VStack (alignment: .trailing) { if lastMessageDay == currentDay { Text(lastMessageTime, style: .time ) @@ -80,6 +73,8 @@ struct Contacts: View { } } } + } + if user.messageList.count > 0 { HStack(alignment: .top) { Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") .truncationMode(.tail) @@ -88,31 +83,6 @@ struct Contacts: View { } } } - } else { - HStack { - VStack(alignment: .leading) { - CircleText(text: user.shortName ?? "???", color: Color.blue, circleSize: 52, fontSize: 16) - .padding(.trailing, 5) - } - VStack { - HStack { - VStack { - Text(user.longName ?? "Unknown").font(.headline).fixedSize() - } - VStack { - Text(" ") - } - .frame(maxWidth: .infinity, alignment: .trailing) - } - HStack(alignment: .top) { - Text(" ") - .truncationMode(.tail) - .foregroundColor(Color.gray) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - .padding(.top) - } } } } @@ -120,7 +90,6 @@ struct Contacts: View { } Section(header: Text("Private Channels")) { // Display Contacts for the rest of the non admin channels - } } .tint(Color(UIColor.systemGray)) @@ -132,16 +101,12 @@ struct Contacts: View { ) } detail: { - if let user = selection { - MessageList(user:user) } else { - Text("Select a user") } } - } } From c27d97062dcd1df9ba34d42e1d65588e1ec30dc5 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Fri, 4 Nov 2022 10:21:31 -0700 Subject: [PATCH 04/14] Put tapbacks in an enum --- Meshtastic/Enums/MessagingEnums.swift | 53 +++++++++++ Meshtastic/Views/Messages/Contacts.swift | 1 - Meshtastic/Views/Messages/MessageList.swift | 100 +++----------------- 3 files changed, 67 insertions(+), 87 deletions(-) diff --git a/Meshtastic/Enums/MessagingEnums.swift b/Meshtastic/Enums/MessagingEnums.swift index 637cfdc6..251fbb6f 100644 --- a/Meshtastic/Enums/MessagingEnums.swift +++ b/Meshtastic/Enums/MessagingEnums.swift @@ -9,3 +9,56 @@ enum BubblePosition { case left case right } + +enum Tapbacks: Int, CaseIterable, Identifiable { + + case heart = 0 + case thumbsUp = 1 + case thumbsDown = 2 + case haHa = 3 + case exclamation = 4 + case question = 5 + case poop = 6 + + var id: Int { self.rawValue } + var emojiString: String { + get { + switch self { + case .heart: + return "❤️" + case .thumbsUp: + return "👍" + case .thumbsDown: + return "👎" + case .haHa: + return "🤣" + case .exclamation: + return "‼️" + case .question: + return "❓" + case .poop: + return "💩" + } + } + } + var description: String { + get { + switch self { + case .heart: + return "Heart" + case .thumbsUp: + return "Thumbs Up" + case .thumbsDown: + return "Thumbs Down" + case .haHa: + return "HaHa" + case .exclamation: + return "Exclamation Mark" + case .question: + return "Question Mark" + case .poop: + return "Poop" + } + } + } +} diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index b80aa884..6f126c20 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -18,7 +18,6 @@ struct Contacts: View { animation: .default) private var users: FetchedResults - private var prefferedNode: NodeInfoEntity? @FetchRequest( diff --git a/Meshtastic/Views/Messages/MessageList.swift b/Meshtastic/Views/Messages/MessageList.swift index 6eab8a8e..b583aa8c 100644 --- a/Meshtastic/Views/Messages/MessageList.swift +++ b/Meshtastic/Views/Messages/MessageList.swift @@ -69,94 +69,22 @@ struct MessageList: View { .background(currentUser ? Color.blue : Color(.darkGray)) .cornerRadius(15) .contextMenu { + VStack{ + Text("Channel: \(message.channel)") + } Menu("Tapback response") { - - Button(action: { - if bleManager.sendMessage(message: "❤️", toUserNum: user.num, isEmoji: true, replyID: message.messageId) { - print("Sent ❤️ Tapback") - self.context.refresh(user, mergeChanges: true) - } else { print("❤️ Tapback Failed") } - - }) { - Text("Heart") - let image = "❤️".image() - Image(uiImage: image!) - } - Button(action: { - - if bleManager.sendMessage(message: "👍", toUserNum: user.num, isEmoji: true, replyID: message.messageId) { + ForEach(Tapbacks.allCases) { tb in + Button(action: { + if bleManager.sendMessage(message: tb.description, toUserNum: user.num, isEmoji: true, replyID: message.messageId) { + print("Sent \(tb.description) Tapback") + self.context.refresh(user, mergeChanges: true) + } else { print("\(tb.description) Tapback Failed") } - print("Sent 👍 Tapback") - self.context.refresh(user, mergeChanges: true) - - } else { print("👍 Tapback Failed")} - - }) { - Text("Thumbs Up") - let image = "👍".image() - Image(uiImage: image!) - } - Button(action: { - - if bleManager.sendMessage(message: "👎", toUserNum: user.num, isEmoji: true, replyID: message.messageId) { - - print("Sent 👎 Tapback") - self.context.refresh(user, mergeChanges: true) - - } else { print("👎 Tapback Failed") } - - }) { - Text("Thumbs Down") - let image = "👎".image() - Image(uiImage: image!) - } - Button(action: { - - if bleManager.sendMessage(message: "🤣", toUserNum: user.num, isEmoji: true, replyID: message.messageId) { - - print("Sent 🤣 Tapback") - self.context.refresh(user, mergeChanges: true) - - } else { print("🤣 Tapback Failed") } - - }) { - Text("HaHa") - let image = "🤣".image() - Image(uiImage: image!) - } - Button(action: { - - if bleManager.sendMessage(message: "‼️", toUserNum: user.num, isEmoji: true, replyID: message.messageId) { - - print("Sent ‼️ Tapback") - self.context.refresh(user, mergeChanges: true) - - } else { print("‼️ Tapback Failed") } - - }) { - Text("Exclamation Mark") - let image = "‼️".image() - Image(uiImage: image!) - } - Button(action: { - if bleManager.sendMessage(message: "❓", toUserNum: user.num, isEmoji: true, replyID: message.messageId) { - self.context.refresh(user, mergeChanges: true) - 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, isEmoji: true, replyID: message.messageId) { - self.context.refresh(user, mergeChanges: true) - print("Sent 💩 Tapback") - } else { print("💩 Tapback Failed") } - }) { - Text("Poop") - let image = "💩".image() - Image(uiImage: image!) + }) { + Text(tb.description) + let image = tb.emojiString.image() + Image(uiImage: image!) + } } } Button(action: { From 238b2a78d1a7f91c67035511c85589860721b3bb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sat, 5 Nov 2022 08:26:27 -0700 Subject: [PATCH 05/14] Messaging cleanup --- Meshtastic.xcodeproj/project.pbxproj | 12 +- Meshtastic/Helpers/BLEManager.swift | 17 +- Meshtastic/Helpers/Extensions.swift | 8 + .../Views/Messages/ChannelMessageList.swift | 342 ++++++++++++++++++ Meshtastic/Views/Messages/Contacts.swift | 64 +++- ...essageList.swift => UserMessageList.swift} | 36 +- Meshtastic/Views/Nodes/PositionLog.swift | 54 ++- Meshtastic/Views/Settings/ShareChannels.swift | 46 +-- 8 files changed, 472 insertions(+), 107 deletions(-) create mode 100644 Meshtastic/Views/Messages/ChannelMessageList.swift rename Meshtastic/Views/Messages/{MessageList.swift => UserMessageList.swift} (94%) diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 0d54f434..14919650 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ DD17E5DE277D49D400010EC2 /* storeforward.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */; }; DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */; }; DD1925B928CDA93900720036 /* SerialConfigEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1925B828CDA93900720036 /* SerialConfigEnums.swift */; }; - DD1BF2F92776FE2E008C8D2F /* MessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* MessageList.swift */; }; + DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2553562855B02500E55709 /* LoRaConfig.swift */; }; @@ -49,6 +49,7 @@ DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; + DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD798B062915928D005217CD /* ChannelMessageList.swift */; }; DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */; }; DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */; }; DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8169FE272476C700F4AB02 /* LogDocument.swift */; }; @@ -129,7 +130,7 @@ DD17E5DC277D49D400010EC2 /* storeforward.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = storeforward.pb.swift; sourceTree = ""; }; DD1925B628CDA5A400720036 /* CannedMessagesConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfigEnums.swift; sourceTree = ""; }; DD1925B828CDA93900720036 /* SerialConfigEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfigEnums.swift; sourceTree = ""; }; - DD1BF2F82776FE2E008C8D2F /* MessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageList.swift; sourceTree = ""; }; + DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; DD2553562855B02500E55709 /* LoRaConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaConfig.swift; sourceTree = ""; }; @@ -160,6 +161,7 @@ DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = ""; }; DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = ""; }; + DD798B062915928D005217CD /* ChannelMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMessageList.swift; sourceTree = ""; }; DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = ""; }; DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = ""; }; DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = ""; }; @@ -491,7 +493,8 @@ isa = PBXGroup; children = ( DD882F5C2772E4640005BF05 /* Contacts.swift */, - DD1BF2F82776FE2E008C8D2F /* MessageList.swift */, + DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */, + DD798B062915928D005217CD /* ChannelMessageList.swift */, ); path = Messages; sourceTree = ""; @@ -700,6 +703,7 @@ DD836AE726F6B38600ABCC23 /* Connect.swift in Sources */, DDAF8C6E26ED19040058C060 /* Extensions.swift in Sources */, DD3501892852FC3B000FC853 /* Settings.swift in Sources */, + DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, DDA6B2EB28420A7B003E8C16 /* NodeAnnotation.swift in Sources */, DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */, DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */, @@ -724,7 +728,7 @@ DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */, DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */, - DD1BF2F92776FE2E008C8D2F /* MessageList.swift in Sources */, + DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */, DD3CC6C228EB9D4900FA9159 /* UpdateCoreData.swift in Sources */, DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */, DDB2CC6E27F3EB47009C5FCC /* telemetry.pb.swift in Sources */, diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 0c13dd73..080b3216 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -601,13 +601,16 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph 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") +// 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") + } else { + + context!.delete(fetchedUser[0]) } } catch { diff --git a/Meshtastic/Helpers/Extensions.swift b/Meshtastic/Helpers/Extensions.swift index 3ed14763..0c36df3c 100644 --- a/Meshtastic/Helpers/Extensions.swift +++ b/Meshtastic/Helpers/Extensions.swift @@ -68,4 +68,12 @@ extension String { UIGraphicsEndImageContext() return image } + + func camelCaseToWords() -> String { + return unicodeScalars.dropFirst().reduce(String(prefix(1))) { + return CharacterSet.uppercaseLetters.contains($1) + ? $0 + " " + String($1) + : $0 + String($1) + } + } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift new file mode 100644 index 00000000..84a1abb6 --- /dev/null +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -0,0 +1,342 @@ +// +// UserMessageList.swift +// MeshtasticApple +// +// Created by Garth Vander Houwen on 12/24/21. +// + +import SwiftUI +import CoreData + +struct ChannelMessageList: View { + + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @EnvironmentObject var userSettings: UserSettings + + enum Field: Hashable { + case messageText + } + + // Keyboard State + @State var typingMessage: String = "" + @State private var totalBytes = 0 + var maxbytes = 228 + @FocusState var focusedField: Field? + + @ObservedObject var user: UserEntity + @ObservedObject var channel: ChannelEntity + @State var showDeleteMessageAlert = false + @State private var deleteMessageId: Int64 = 0 + @State private var replyMessageId: Int64 = 0 + @State private var sendPositionWithMessage: Bool = false + @State private var refreshId = UUID() + + var body: some View { + NavigationStack { + ScrollViewReader { scrollView in + ScrollView { + LazyVStack { + ForEach( user.messageList ) { (message: MessageEntity) in + if user.num != userSettings.preferredNodeNum { + let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false) + if (user.num == bleManager.broadcastNodeNum || user.num != bleManager.broadcastNodeNum && message.toUser!.num != bleManager.broadcastNodeNum) { + if message.replyID > 0 { + let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) + HStack { + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.blue, lineWidth: 0.5) + ) + Image(systemName: "arrowshape.turn.up.left.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.blue) + .padding(.trailing) + } + } + HStack (alignment: .top) { + if currentUser { Spacer(minLength:50) } + if !currentUser { + CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 44, fontSize: 14) + .padding(.all, 5) + .offset(y: -5) + } + VStack(alignment: currentUser ? .trailing : .leading) { + Text(message.messagePayload ?? "EMPTY MESSAGE") + .padding(10) + .foregroundColor(.white) + .background(currentUser ? Color.blue : Color(.darkGray)) + .cornerRadius(15) + .contextMenu { + VStack{ + Text("Channel: \(message.channel)") + } + Menu("Tapback response") { + ForEach(Tapbacks.allCases) { tb in + Button(action: { + if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, isEmoji: true, replyID: message.messageId) { + print("Sent \(tb.emojiString) Tapback") + self.context.refresh(user, mergeChanges: true) + } else { print("\(tb.emojiString) Tapback Failed") } + + }) { + Text(tb.description) + let image = tb.emojiString.image() + Image(uiImage: image!) + } + } + } + Button(action: { + self.replyMessageId = message.messageId + self.focusedField = .messageText + print("I want to reply to \(message.messageId)") + }) { + Text("Reply") + Image(systemName: "arrowshape.turn.up.left.2.fill") + } + Button(action: { + UIPasteboard.general.string = message.messagePayload + }) { + Text("Copy") + Image(systemName: "doc.on.doc") + } + Menu("Message Details") { + VStack { + let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) + Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray) + } + if currentUser && message.receivedACK { + VStack { + Text("Received Ack \(message.receivedACK ? "✔️" : "")") + } + } else if currentUser && message.ackError == 0 { + // Empty Error + Text("Waiting. . .") + } else if currentUser && message.ackError > 0 { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) + } + if currentUser { + VStack { + let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) + let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) + if ackDate >= sixMonthsAgo! { + Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray) + } else { + Text("Unknown Age").font(.caption2).foregroundColor(.gray) + } + } + } + if message.ackSNR != 0 { + VStack { + Text("Ack SNR \(String(message.ackSNR))") + .font(.caption2) + .foregroundColor(.gray) + } + } + } + Divider() + Button(role: .destructive, action: { + self.showDeleteMessageAlert = true + self.deleteMessageId = message.messageId + print(deleteMessageId) + }) { + Text("Delete") + Image(systemName: "trash") + } + } + + let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] + if tapbacks.count > 0 { + VStack (alignment: .trailing) { + HStack { + ForEach( tapbacks ) { (tapback: MessageEntity) in + VStack { + let image = tapback.messagePayload!.image(fontSize: 20) + Image(uiImage: image!).font(.caption) + Text("\(tapback.fromUser?.shortName ?? "????")") + .font(.caption2) + .foregroundColor(.gray) + .fixedSize() + .padding(.bottom, 1) + } + } + } + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.gray, lineWidth: 1) + ) + } + } + HStack { + if currentUser && message.receivedACK { + // Ack Received + Text("Acknowledged").font(.caption2).foregroundColor(.gray) + } else if currentUser && message.ackError == 0 { + // Empty Error + Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) + } else if currentUser && message.ackError > 0 { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) + .font(.caption2).foregroundColor(.red) + } + } + } + .padding(.bottom) + .id(user.messageList.firstIndex(of: message)) + if !currentUser { + Spacer(minLength:50) + } + } + .padding([.leading, .trailing]) + .frame(maxWidth: .infinity) + .id(message.messageId) + .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 = user.messageList.first(where: { $0.messageId == deleteMessageId }) + context.delete(message!) + do { + try context.save() + deleteMessageId = 0 + } catch { + print("Failed to delete message \(deleteMessageId)") + } + } + }, secondaryButton: .cancel()) + } + } + } + } + } + } + .padding([.top]) + .scrollDismissesKeyboard(.immediately) + .onAppear(perform: { + self.bleManager.context = context + refreshId = UUID() + if user.messageList.count > 0 { + scrollView.scrollTo(user.messageList.last!.messageId) + } + }) + .onChange(of: user.messageList, perform: { messages in + refreshId = UUID() + if user.messageList.count > 0 { + scrollView.scrollTo(user.messageList.last!.messageId) + } + }) + } + HStack(alignment: .top) { + + ZStack { + let kbType = UIKeyboardType(rawValue: UserDefaults.standard.object(forKey: "keyboardType") as? Int ?? 0) + TextField("Message", text: $typingMessage, axis: .vertical) + .onChange(of: typingMessage, perform: { value in + totalBytes = value.utf8.count + // Only mess with the value if it is too big + if totalBytes > maxbytes { + let firstNBytes = Data(typingMessage.utf8.prefix(maxbytes)) + if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) { + // Set the message back to the last place where it was the right size + typingMessage = maxBytesString + } else { + print("not a valid UTF-8 sequence") + } + } + }) + .keyboardType(kbType!) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Button("Dismiss Keyboard") { + focusedField = nil + } + .font(.subheadline) + Spacer() + Button { + let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" + sendPositionWithMessage = true + if user.num == bleManager.broadcastNodeNum { + + if userSettings.meshtasticUsername.count > 0 { + + typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with the mesh from node " + userLongName + } else { + + typingMessage = "📍 " + userLongName + " has shared their position with the mesh." + } + + } else { + + if userSettings.meshtasticUsername.count > 0 { + + typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName + + } else { + + typingMessage = "📍 " + userLongName + " has shared their position with you." + } + } + } label: { + Image(systemName: "mappin.and.ellipse") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.accentColor) + } + + 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: 50) + + 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, isEmoji: false, replyID: replyMessageId) { + typingMessage = "" + focusedField = nil + replyMessageId = 0 + if sendPositionWithMessage { + if bleManager.sendLocation(destNum: user.num, wantAck: true) { + print("Location Sent") + } + } + } + }) { + Image(systemName: "arrow.up.circle.fill").font(.largeTitle).foregroundColor(.blue) + } + } + .padding(.all, 15) + } + .navigationViewStyle(.stack) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + HStack { + CircleText(text: user.shortName ?? "???", color: .blue, circleSize: 44, fontSize: 14).fixedSize() + Text(user.longName ?? "Unknown").font(.headline) + } + } + ToolbarItem(placement: .navigationBarTrailing) { + ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + } + } + } + } +} diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 6f126c20..62273ebd 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -6,6 +6,7 @@ // import SwiftUI +import CoreData struct Contacts: View { @@ -18,13 +19,7 @@ struct Contacts: View { animation: .default) private var users: FetchedResults - private var prefferedNode: NodeInfoEntity? - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(key: "num", ascending: true)], - animation: .default) - - private var nodes: FetchedResults + @State var node: NodeInfoEntity? = nil @State private var selection: UserEntity? = nil // Nothing selected by default. @@ -33,10 +28,33 @@ struct Contacts: View { NavigationSplitView { List { + Section(header: Text("Channels")) { + // Display Contacts for the rest of the non admin channels + if node != nil { + ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in + if channel.name?.lowercased() ?? "" != "admin" && channel.name?.lowercased() ?? "" != "gpio" { + HStack { + VStack(alignment: .leading) { + HStack { + CircleText(text: String(channel.index), color: Color.blue, circleSize: 52, fontSize: 32) + .padding(.trailing, 5) + VStack { + Text(channel.name?.camelCaseToWords() ?? "Channel \(channel.index)").font(.headline) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + } + } + .padding(.top, 10) + .padding(.bottom, 10) + } + } Section(header: Text("Direct Messages")) { ForEach(users) { (user: UserEntity) in if user.num != bleManager.userSettings?.preferredNodeNum ?? 0 { - NavigationLink(destination: MessageList(user: user)) { + NavigationLink(destination: UserMessageList(user: user)) { let mostRecent = user.num == bleManager.broadcastNodeNum ? user.messageList.last : user.messageList.last(where: { $0.toUser?.num ?? 0 != bleManager.broadcastNodeNum }) let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 @@ -84,12 +102,11 @@ struct Contacts: View { } } } + .padding(.top, 10) + .padding(.bottom, 10) } } } - Section(header: Text("Private Channels")) { - // Display Contacts for the rest of the non admin channels - } } .tint(Color(UIColor.systemGray)) .navigationSplitViewStyle(.automatic) @@ -98,10 +115,33 @@ struct Contacts: View { .navigationBarItems(leading: MeshtasticLogo() ) + .onAppear { + self.bleManager.userSettings = userSettings + self.bleManager.context = context + + if userSettings.preferredNodeNum > 0 { + + let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(userSettings.preferredNodeNum)) + + do { + + let fetchedNode = try context.fetch(fetchNodeInfoRequest) as! [NodeInfoEntity] + // Found a node, check it for a region + if !fetchedNode.isEmpty { + node = fetchedNode[0] + + } + } catch { + + } + } + + } } detail: { if let user = selection { - MessageList(user:user) + UserMessageList(user:user) } else { Text("Select a user") diff --git a/Meshtastic/Views/Messages/MessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift similarity index 94% rename from Meshtastic/Views/Messages/MessageList.swift rename to Meshtastic/Views/Messages/UserMessageList.swift index b583aa8c..591a698c 100644 --- a/Meshtastic/Views/Messages/MessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -8,7 +8,7 @@ import SwiftUI import CoreData -struct MessageList: View { +struct UserMessageList: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -75,10 +75,10 @@ struct MessageList: View { Menu("Tapback response") { ForEach(Tapbacks.allCases) { tb in Button(action: { - if bleManager.sendMessage(message: tb.description, toUserNum: user.num, isEmoji: true, replyID: message.messageId) { - print("Sent \(tb.description) Tapback") + if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, isEmoji: true, replyID: message.messageId) { + print("Sent \(tb.emojiString) Tapback") self.context.refresh(user, mergeChanges: true) - } else { print("\(tb.description) Tapback Failed") } + } else { print("\(tb.emojiString) Tapback Failed") } }) { Text(tb.description) @@ -90,7 +90,6 @@ struct MessageList: View { Button(action: { self.replyMessageId = message.messageId self.focusedField = .messageText - print("I want to reply to \(message.messageId)") }) { Text("Reply") @@ -108,44 +107,29 @@ struct MessageList: View { Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray) } if currentUser && message.receivedACK { - VStack { - Text("Received Ack \(message.receivedACK ? "✔️" : "")") } - } else if currentUser && message.ackError == 0 { - // Empty Error Text("Waiting. . .") - } else if currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) } - if currentUser { - VStack { - let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) - let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) if ackDate >= sixMonthsAgo! { - Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray) - } else { - Text("Unknown Age").font(.caption2).foregroundColor(.gray) } } } - if message.ackSNR != 0 { VStack { - Text("Ack SNR \(String(message.ackSNR))") .font(.caption2) .foregroundColor(.gray) @@ -165,15 +149,10 @@ struct MessageList: View { let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] if tapbacks.count > 0 { - VStack (alignment: .trailing) { - HStack { - ForEach( tapbacks ) { (tapback: MessageEntity) in - VStack { - let image = tapback.messagePayload!.image(fontSize: 20) Image(uiImage: image!).font(.caption) Text("\(tapback.fromUser?.shortName ?? "????")") @@ -227,9 +206,7 @@ struct MessageList: View { print("Failed to delete message \(deleteMessageId)") } } - }, - secondaryButton: .cancel() - ) + }, secondaryButton: .cancel()) } } } @@ -273,14 +250,11 @@ struct MessageList: View { .keyboardType(kbType!) .toolbar { ToolbarItemGroup(placement: .keyboard) { - Button("Dismiss Keyboard") { focusedField = nil } .font(.subheadline) - Spacer() - Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true diff --git a/Meshtastic/Views/Nodes/PositionLog.swift b/Meshtastic/Views/Nodes/PositionLog.swift index 87cec4a2..984d22bb 100644 --- a/Meshtastic/Views/Nodes/PositionLog.swift +++ b/Meshtastic/Views/Nodes/PositionLog.swift @@ -24,36 +24,30 @@ struct PositionLog: View { if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { //Add a table for mac and ipad - VStack { - Table(node.positions!.reversed() as! [PositionEntity]) { - TableColumn("Seq No") { position in - Text(String(position.seqNo)) - } - //.width(75) - TableColumn("Latitude") { position in - Text(String(format: "%.6f", position.latitude ?? 0)) - } - TableColumn("Longitude") { position in - Text(String(format: "%.6f", position.longitude ?? 0)) - } - TableColumn("Altitude") { position in - Text(String(position.altitude)) - } - //.width(75) - TableColumn("Sats") { position in - Text(String(position.satsInView)) - } - //.width(75) - TableColumn("Speed") { position in - Text(String(position.speed)) - } - //.width(75) - TableColumn("Heading") { position in - Text(String(position.heading)) - } - TableColumn("Time Stamp") { position in - Text(position.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time") - } + Table(node.positions!.reversed() as! [PositionEntity]) { + TableColumn("SeqNo") { position in + Text(String(position.seqNo)) + } + TableColumn("Latitude") { position in + Text(String(format: "%.6f", position.latitude ?? 0)) + } + TableColumn("Longitude") { position in + Text(String(format: "%.6f", position.longitude ?? 0)) + } + TableColumn("Altitude") { position in + Text(String(position.altitude)) + } + TableColumn("Sats") { position in + Text(String(position.satsInView)) + } + TableColumn("Speed") { position in + Text(String(position.speed)) + } + TableColumn("Heading") { position in + Text(String(position.heading)) + } + TableColumn("Time Stamp") { position in + Text(position.time?.formattedDate(format: "MM/dd/yy hh:mm") ?? "Unknown time") } } diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 7f88867b..adbf792c 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -55,7 +55,7 @@ struct ShareChannels: View { ScrollView { VStack { if node != nil && node?.myInfo != nil { - Grid(alignment: .top, horizontalSpacing: 2) { + Grid(alignment: .top) { GridRow { Spacer() Text("Include") @@ -78,13 +78,13 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary") + Text((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") - .foregroundColor(.red) + .foregroundColor(.red) } else { Image(systemName: "lock.fill") - .foregroundColor(.green) + .foregroundColor(.green) } } else if channel.index == 1 && channel.role > 0 { Toggle("Channel 1 Included", isOn: $includeChannel1) @@ -94,10 +94,10 @@ struct ShareChannels: View { Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") - .foregroundColor(.red) + .foregroundColor(.red) } else { Image(systemName: "lock.fill") - .foregroundColor(.green) + .foregroundColor(.green) } } else if channel.index == 2 && channel.role > 0 { Toggle("Channel 2 Included", isOn: $includeChannel2) @@ -107,10 +107,10 @@ struct ShareChannels: View { Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") - .foregroundColor(.red) + .foregroundColor(.red) } else { Image(systemName: "lock.fill") - .foregroundColor(.green) + .foregroundColor(.green) } } else if channel.index == 3 && channel.role > 0 { Toggle("Channel 3 Included", isOn: $includeChannel3) @@ -120,10 +120,10 @@ struct ShareChannels: View { Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") - .foregroundColor(.red) + .foregroundColor(.red) } else { Image(systemName: "lock.fill") - .foregroundColor(.green) + .foregroundColor(.green) } } else if channel.index == 4 && channel.role > 0 { Toggle("Channel 4 Included", isOn: $includeChannel4) @@ -133,10 +133,10 @@ struct ShareChannels: View { Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") - .foregroundColor(.red) + .foregroundColor(.red) } else { Image(systemName: "lock.fill") - .foregroundColor(.green) + .foregroundColor(.green) } } else if channel.index == 5 && channel.role > 0 { Toggle("Channel 5 Included", isOn: $includeChannel5) @@ -146,10 +146,10 @@ struct ShareChannels: View { Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") - .foregroundColor(.red) + .foregroundColor(.red) } else { Image(systemName: "lock.fill") - .foregroundColor(.green) + .foregroundColor(.green) } } else if channel.index == 6 && channel.role > 0 { Toggle("Channel 6 Included", isOn: $includeChannel6) @@ -159,10 +159,10 @@ struct ShareChannels: View { Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") - .foregroundColor(.red) + .foregroundColor(.red) } else { Image(systemName: "lock.fill") - .foregroundColor(.green) + .foregroundColor(.green) } } else if channel.index == 7 && channel.role > 0 { Toggle("Channel 7 Included", isOn: $includeChannel7) @@ -172,10 +172,10 @@ struct ShareChannels: View { Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") - .foregroundColor(.red) + .foregroundColor(.red) } else { Image(systemName: "lock.fill") - .foregroundColor(.green) + .foregroundColor(.green) } } Spacer() @@ -183,15 +183,15 @@ struct ShareChannels: View { .padding(0) } } - + let qrImage = qrCodeImage.generateQRCode(from: channelsUrl) VStack { if node != nil { ShareLink("Share QR Code & Link", - item: Image(uiImage: qrImage), - subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"), - message: Text(channelsUrl), - preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you", + item: Image(uiImage: qrImage), + subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"), + message: Text(channelsUrl), + preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you", image: Image(uiImage: qrImage)) ) .buttonStyle(.bordered) From e6b22db1f45bc456f670ed510d023ad20d2a7854 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 6 Nov 2022 09:21:46 -0800 Subject: [PATCH 06/14] Fix saving of channels when connecting device --- Meshtastic/Helpers/MeshPackets.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 27a254ea..aa3f037b 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -771,12 +771,12 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo newChannel.role = Int32(channel.role.rawValue) newChannel.psk = channel.settings.psk let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as! NSMutableOrderedSet - if newChannel.index == 0 { - mutableChannels.removeAllObjects() + if mutableChannels.contains(newChannel) { + mutableChannels.replaceObject(at: Int(newChannel.index), with: newChannel) + } else { + mutableChannels.add(newChannel) } - mutableChannels.add(newChannel) fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet - //fetchedMyInfo[0].objectWillChange.send() do { try context.save() } catch { From 8de0336458b36522796e3bc362a05b57b11ad4cf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Sun, 6 Nov 2022 18:05:10 -0800 Subject: [PATCH 07/14] Break apart channel names for display --- Meshtastic/Views/Settings/ShareChannels.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index adbf792c..33ccb9e8 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -78,7 +78,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").fixedSize() + Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) @@ -91,7 +91,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) @@ -104,7 +104,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) @@ -117,7 +117,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) @@ -130,7 +130,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) @@ -143,7 +143,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) @@ -156,7 +156,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) @@ -169,7 +169,7 @@ struct ShareChannels: View { .toggleStyle(.switch) .labelsHidden() .disabled(channel.role == 1) - Text((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)") + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() if channel.psk?.hexDescription.count ?? 0 < 3 { Image(systemName: "lock.slash") .foregroundColor(.red) From 69d2c3d189e0de84f57e3ae8ea001f87b9bcd89a Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 7 Nov 2022 18:31:12 -0800 Subject: [PATCH 08/14] Multiple channels --- Meshtastic.xcodeproj/project.pbxproj | 4 + Meshtastic/Helpers/MeshPackets.swift | 28 +- .../MeshtasticDataModel.xcdatamodel/contents | 11 +- .../Persistence/ChannelEntityExtension.swift | 15 + .../Views/Messages/ChannelMessageList.swift | 345 +++++++++--------- Meshtastic/Views/Messages/Contacts.swift | 5 +- .../Views/Messages/UserMessageList.swift | 326 ++++++++--------- 7 files changed, 373 insertions(+), 361 deletions(-) create mode 100644 Meshtastic/Persistence/ChannelEntityExtension.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 14919650..523c56ca 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; }; DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; }; DD539502276DAA6A00AD86B1 /* MapLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD539501276DAA6A00AD86B1 /* MapLocation.swift */; }; + DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */; }; DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; }; DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; @@ -156,6 +157,7 @@ DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = ""; }; DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = ""; }; DD539501276DAA6A00AD86B1 /* MapLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLocation.swift; sourceTree = ""; }; + DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = ""; }; DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfig.swift; sourceTree = ""; }; DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; @@ -535,6 +537,7 @@ DDC4D567275499A500A4208E /* Persistence.swift */, DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */, DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, + DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */, ); path = Persistence; sourceTree = ""; @@ -777,6 +780,7 @@ DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, + DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */, DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index aa3f037b..516fd12d 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1263,8 +1263,8 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if fetchedUsers.count <= 1 && fetchedUsers.first(where: { $0.num == packet.from }) == nil { - print("Message from another mesh, unable to manage for now") - return + //print("Message from another mesh, unable to manage for now") + //return } let newMessage = MessageEntity(context: context) @@ -1277,20 +1277,24 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if packet.decoded.replyID > 0 { newMessage.replyID = Int64(packet.decoded.replyID) } - if packet.to == broadcastNodeNum && fetchedUsers.count == 1 { + //if packet.to == broadcastNodeNum && fetchedUsers.count == 1 { // 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" - newMessage.toUser = bcu +// let bcu: UserEntity = UserEntity(context: context) +// bcu.shortName = "ALL" +// bcu.longName = "All - Broadcast" +// bcu.hwModel = "UNSET" +// bcu.num = Int64(broadcastNodeNum) +// bcu.userId = "BROADCASTNODE" +// newMessage.toUser = bcu - } else { + if fetchedUsers.first(where: { $0.num == packet.to }) != nil { newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) } - newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) + if fetchedUsers.first(where: { $0.num == packet.from }) != nil { + newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) + } + + newMessage.messagePayload = messageText newMessage.fromUser?.objectWillChange.send() newMessage.toUser?.objectWillChange.send() diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents index f92b0c92..670f7ee5 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -28,6 +28,9 @@ + + + @@ -84,8 +87,8 @@ - - + + @@ -236,7 +239,7 @@ - + \ No newline at end of file diff --git a/Meshtastic/Persistence/ChannelEntityExtension.swift b/Meshtastic/Persistence/ChannelEntityExtension.swift new file mode 100644 index 00000000..4a3bde42 --- /dev/null +++ b/Meshtastic/Persistence/ChannelEntityExtension.swift @@ -0,0 +1,15 @@ +// +// ChannelEntityExtension.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 11/7/22. +// +import Foundation + +extension ChannelEntity { + + var allPrivateMessages: [MessageEntity] { + + self.value(forKey: "allPrivateMessages") as! [MessageEntity] + } +} diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 84a1abb6..5e256313 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -24,7 +24,6 @@ struct ChannelMessageList: View { var maxbytes = 228 @FocusState var focusedField: Field? - @ObservedObject var user: UserEntity @ObservedObject var channel: ChannelEntity @State var showDeleteMessageAlert = false @State private var deleteMessageId: Int64 = 0 @@ -37,179 +36,175 @@ struct ChannelMessageList: View { ScrollViewReader { scrollView in ScrollView { LazyVStack { - ForEach( user.messageList ) { (message: MessageEntity) in - if user.num != userSettings.preferredNodeNum { - let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false) - if (user.num == bleManager.broadcastNodeNum || user.num != bleManager.broadcastNodeNum && message.toUser!.num != bleManager.broadcastNodeNum) { - if message.replyID > 0 { - let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) - HStack { - Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.blue, lineWidth: 0.5) - ) - Image(systemName: "arrowshape.turn.up.left.fill") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.blue) - .padding(.trailing) - } - } - HStack (alignment: .top) { - if currentUser { Spacer(minLength:50) } - if !currentUser { - CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 44, fontSize: 14) - .padding(.all, 5) - .offset(y: -5) - } - VStack(alignment: currentUser ? .trailing : .leading) { - Text(message.messagePayload ?? "EMPTY MESSAGE") - .padding(10) - .foregroundColor(.white) - .background(currentUser ? Color.blue : Color(.darkGray)) - .cornerRadius(15) - .contextMenu { - VStack{ - Text("Channel: \(message.channel)") - } - Menu("Tapback response") { - ForEach(Tapbacks.allCases) { tb in - Button(action: { - if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, isEmoji: true, replyID: message.messageId) { - print("Sent \(tb.emojiString) Tapback") - self.context.refresh(user, mergeChanges: true) - } else { print("\(tb.emojiString) Tapback Failed") } - - }) { - Text(tb.description) - let image = tb.emojiString.image() - Image(uiImage: image!) - } - } - } + ForEach( channel.allPrivateMessages ) { (message: MessageEntity) in + let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false) + if message.replyID > 0 { + let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID }) + HStack { + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.blue, lineWidth: 0.5) + ) + Image(systemName: "arrowshape.turn.up.left.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.blue) + .padding(.trailing) + } + } + HStack (alignment: .top) { + if currentUser { Spacer(minLength:50) } + if !currentUser { + CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 44, fontSize: 14) + .padding(.all, 5) + .offset(y: -5) + } + VStack(alignment: currentUser ? .trailing : .leading) { + Text(message.messagePayload ?? "EMPTY MESSAGE") + .padding(10) + .foregroundColor(.white) + .background(currentUser ? Color.blue : Color(.darkGray)) + .cornerRadius(15) + .contextMenu { + VStack{ + Text("Channel: \(message.channel)") + } + Menu("Tapback response") { + ForEach(Tapbacks.allCases) { tb in Button(action: { - self.replyMessageId = message.messageId - self.focusedField = .messageText - print("I want to reply to \(message.messageId)") + if bleManager.sendMessage(message: tb.emojiString, toUserNum: Int64(channel.index), isEmoji: true, replyID: message.messageId) { + print("Sent \(tb.emojiString) Tapback") + self.context.refresh(channel, mergeChanges: true) + } else { print("\(tb.emojiString) Tapback Failed") } + }) { - Text("Reply") - Image(systemName: "arrowshape.turn.up.left.2.fill") + Text(tb.description) + let image = tb.emojiString.image() + Image(uiImage: image!) } - Button(action: { - UIPasteboard.general.string = message.messagePayload - }) { - Text("Copy") - Image(systemName: "doc.on.doc") - } - Menu("Message Details") { - VStack { - let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) - Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray) - } - if currentUser && message.receivedACK { - VStack { - Text("Received Ack \(message.receivedACK ? "✔️" : "")") - } - } else if currentUser && message.ackError == 0 { - // Empty Error - Text("Waiting. . .") - } else if currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) - Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) - } - if currentUser { - VStack { - let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) - let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) - if ackDate >= sixMonthsAgo! { - Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray) - } else { - Text("Unknown Age").font(.caption2).foregroundColor(.gray) - } - } - } - if message.ackSNR != 0 { - VStack { - Text("Ack SNR \(String(message.ackSNR))") - .font(.caption2) - .foregroundColor(.gray) - } - } - } - Divider() - Button(role: .destructive, action: { - self.showDeleteMessageAlert = true - self.deleteMessageId = message.messageId - print(deleteMessageId) - }) { - Text("Delete") - Image(systemName: "trash") - } - } - - let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] - if tapbacks.count > 0 { - VStack (alignment: .trailing) { - HStack { - ForEach( tapbacks ) { (tapback: MessageEntity) in - VStack { - let image = tapback.messagePayload!.image(fontSize: 20) - Image(uiImage: image!).font(.caption) - Text("\(tapback.fromUser?.shortName ?? "????")") - .font(.caption2) - .foregroundColor(.gray) - .fixedSize() - .padding(.bottom, 1) - } - } - } - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.gray, lineWidth: 1) - ) } } - HStack { + Button(action: { + self.replyMessageId = message.messageId + self.focusedField = .messageText + print("I want to reply to \(message.messageId)") + }) { + Text("Reply") + Image(systemName: "arrowshape.turn.up.left.2.fill") + } + Button(action: { + UIPasteboard.general.string = message.messagePayload + }) { + Text("Copy") + Image(systemName: "doc.on.doc") + } + Menu("Message Details") { + VStack { + let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) + Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray) + } if currentUser && message.receivedACK { - // Ack Received - Text("Acknowledged").font(.caption2).foregroundColor(.gray) + VStack { + Text("Received Ack \(message.receivedACK ? "✔️" : "")") + } } else if currentUser && message.ackError == 0 { // Empty Error - Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) + Text("Waiting. . .") } else if currentUser && message.ackError > 0 { let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) - .font(.caption2).foregroundColor(.red) + } + if currentUser { + VStack { + let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) + let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) + if ackDate >= sixMonthsAgo! { + Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray) + } else { + Text("Unknown Age").font(.caption2).foregroundColor(.gray) + } + } + } + if message.ackSNR != 0 { + VStack { + Text("Ack SNR \(String(message.ackSNR))") + .font(.caption2) + .foregroundColor(.gray) + } } } + Divider() + Button(role: .destructive, action: { + self.showDeleteMessageAlert = true + self.deleteMessageId = message.messageId + print(deleteMessageId) + }) { + Text("Delete") + Image(systemName: "trash") + } } - .padding(.bottom) - .id(user.messageList.firstIndex(of: message)) - if !currentUser { - Spacer(minLength:50) + + let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] + if tapbacks.count > 0 { + VStack (alignment: .trailing) { + HStack { + ForEach( tapbacks ) { (tapback: MessageEntity) in + VStack { + let image = tapback.messagePayload!.image(fontSize: 20) + Image(uiImage: image!).font(.caption) + Text("\(tapback.fromUser?.shortName ?? "????")") + .font(.caption2) + .foregroundColor(.gray) + .fixedSize() + .padding(.bottom, 1) + } + } + } + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.gray, lineWidth: 1) + ) } } - .padding([.leading, .trailing]) - .frame(maxWidth: .infinity) - .id(message.messageId) - .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 = user.messageList.first(where: { $0.messageId == deleteMessageId }) - context.delete(message!) - do { - try context.save() - deleteMessageId = 0 - } catch { - print("Failed to delete message \(deleteMessageId)") - } - } - }, secondaryButton: .cancel()) + HStack { + if currentUser && message.receivedACK { + // Ack Received + Text("Acknowledged").font(.caption2).foregroundColor(.gray) + } else if currentUser && message.ackError == 0 { + // Empty Error + Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) + } else if currentUser && message.ackError > 0 { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) + .font(.caption2).foregroundColor(.red) + } } } + .padding(.bottom) + .id(channel.allPrivateMessages.firstIndex(of: message)) + if !currentUser { + Spacer(minLength:50) + } + } + .padding([.leading, .trailing]) + .frame(maxWidth: .infinity) + .id(message.messageId) + .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 = channel.allPrivateMessages.first(where: { $0.messageId == deleteMessageId }) + context.delete(message!) + do { + try context.save() + deleteMessageId = 0 + } catch { + print("Failed to delete message \(deleteMessageId)") + } + } + }, secondaryButton: .cancel()) } } } @@ -219,14 +214,14 @@ struct ChannelMessageList: View { .onAppear(perform: { self.bleManager.context = context refreshId = UUID() - if user.messageList.count > 0 { - scrollView.scrollTo(user.messageList.last!.messageId) + if channel.allPrivateMessages.count > 0 { + scrollView.scrollTo(channel.allPrivateMessages.last!.messageId) } }) - .onChange(of: user.messageList, perform: { messages in + .onChange(of: channel.allPrivateMessages, perform: { messages in refreshId = UUID() - if user.messageList.count > 0 { - scrollView.scrollTo(user.messageList.last!.messageId) + if channel.allPrivateMessages.count > 0 { + scrollView.scrollTo(channel.allPrivateMessages.last!.messageId) } }) } @@ -259,27 +254,15 @@ struct ChannelMessageList: View { Button { let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown" sendPositionWithMessage = true - if user.num == bleManager.broadcastNodeNum { + if userSettings.meshtasticUsername.count > 0 { - if userSettings.meshtasticUsername.count > 0 { - - typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with the mesh from node " + userLongName - } else { - - typingMessage = "📍 " + userLongName + " has shared their position with the mesh." - } + typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName } else { - if userSettings.meshtasticUsername.count > 0 { - - typingMessage = "📍 " + userSettings.meshtasticUsername + " has shared their position with you from node " + userLongName - - } else { - - typingMessage = "📍 " + userLongName + " has shared their position with you." - } + typingMessage = "📍 " + userLongName + " has shared their position with you." } + } label: { Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) @@ -304,12 +287,12 @@ struct ChannelMessageList: View { .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) .padding(.bottom, 15) Button(action: { - if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, isEmoji: false, replyID: replyMessageId) { + if bleManager.sendMessage(message: typingMessage, toUserNum: Int64(channel.index), isEmoji: false, replyID: replyMessageId) { typingMessage = "" focusedField = nil replyMessageId = 0 if sendPositionWithMessage { - if bleManager.sendLocation(destNum: user.num, wantAck: true) { + if bleManager.sendLocation(destNum: Int64(channel.index), wantAck: true) { print("Location Sent") } } @@ -325,8 +308,8 @@ struct ChannelMessageList: View { .toolbar { ToolbarItem(placement: .principal) { HStack { - CircleText(text: user.shortName ?? "???", color: .blue, circleSize: 44, fontSize: 14).fixedSize() - Text(user.longName ?? "Unknown").font(.headline) + CircleText(text: String(channel.index), color: .blue, circleSize: 44, fontSize: 30).fixedSize() + Text(String(channel.name ?? "Unknown").camelCaseToWords()).font(.headline) } } ToolbarItem(placement: .navigationBarTrailing) { diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 62273ebd..8f33f90d 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -39,7 +39,10 @@ struct Contacts: View { CircleText(text: String(channel.index), color: Color.blue, circleSize: 52, fontSize: 32) .padding(.trailing, 5) VStack { - Text(channel.name?.camelCaseToWords() ?? "Channel \(channel.index)").font(.headline) + NavigationLink(destination: ChannelMessageList(channel: channel)) { + + Text(channel.name?.camelCaseToWords() ?? "Channel \(channel.index)").font(.headline) + } } .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 591a698c..3e13716c 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -35,180 +35,180 @@ struct UserMessageList: View { NavigationStack { ScrollViewReader { scrollView in ScrollView { - LazyVStack { + LazyVStack { ForEach( user.messageList ) { (message: MessageEntity) in if user.num != userSettings.preferredNodeNum { let currentUser: Bool = (userSettings.preferredNodeNum == message.fromUser?.num ? true : false) - if (user.num == bleManager.broadcastNodeNum || user.num != bleManager.broadcastNodeNum && message.toUser!.num != bleManager.broadcastNodeNum) { - if message.replyID > 0 { - let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) - HStack { - Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) + + if message.replyID > 0 { + let messageReply = user.messageList.first(where: { $0.messageId == message.replyID }) + HStack { + Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.blue).font(.caption2) + .padding(10) + .overlay( + RoundedRectangle(cornerRadius: 18) + .stroke(Color.blue, lineWidth: 0.5) + ) + Image(systemName: "arrowshape.turn.up.left.fill") + .symbolRenderingMode(.hierarchical) + .imageScale(.large).foregroundColor(.blue) + .padding(.trailing) + } + } + HStack (alignment: .top) { + if currentUser { Spacer(minLength:50) } + if !currentUser { + CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 44, fontSize: 14) + .padding(.all, 5) + .offset(y: -5) + } + VStack(alignment: currentUser ? .trailing : .leading) { + Text(message.messagePayload ?? "EMPTY MESSAGE") + .padding(10) + .foregroundColor(.white) + .background(currentUser ? Color.blue : Color(.darkGray)) + .cornerRadius(15) + .contextMenu { + VStack{ + Text("Channel: \(message.channel)") + } + Menu("Tapback response") { + ForEach(Tapbacks.allCases) { tb in + Button(action: { + if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, isEmoji: true, replyID: message.messageId) { + print("Sent \(tb.emojiString) Tapback") + self.context.refresh(user, mergeChanges: true) + } else { print("\(tb.emojiString) Tapback Failed") } + + }) { + Text(tb.description) + let image = tb.emojiString.image() + Image(uiImage: image!) + } + } + } + Button(action: { + self.replyMessageId = message.messageId + self.focusedField = .messageText + print("I want to reply to \(message.messageId)") + }) { + Text("Reply") + Image(systemName: "arrowshape.turn.up.left.2.fill") + } + Button(action: { + UIPasteboard.general.string = message.messagePayload + }) { + Text("Copy") + Image(systemName: "doc.on.doc") + } + Menu("Message Details") { + VStack { + let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) + Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray) + } + if currentUser && message.receivedACK { + VStack { + Text("Received Ack \(message.receivedACK ? "✔️" : "")") + } + } else if currentUser && message.ackError == 0 { + // Empty Error + Text("Waiting. . .") + } else if currentUser && message.ackError > 0 { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) + } + if currentUser { + VStack { + let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) + let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) + if ackDate >= sixMonthsAgo! { + Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray) + } else { + Text("Unknown Age").font(.caption2).foregroundColor(.gray) + } + } + } + if message.ackSNR != 0 { + VStack { + Text("Ack SNR \(String(message.ackSNR))") + .font(.caption2) + .foregroundColor(.gray) + } + } + } + Divider() + Button(role: .destructive, action: { + self.showDeleteMessageAlert = true + self.deleteMessageId = message.messageId + print(deleteMessageId) + }) { + Text("Delete") + Image(systemName: "trash") + } + } + + let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] + if tapbacks.count > 0 { + VStack (alignment: .trailing) { + HStack { + ForEach( tapbacks ) { (tapback: MessageEntity) in + VStack { + let image = tapback.messagePayload!.image(fontSize: 20) + Image(uiImage: image!).font(.caption) + Text("\(tapback.fromUser?.shortName ?? "????")") + .font(.caption2) + .foregroundColor(.gray) + .fixedSize() + .padding(.bottom, 1) + } + } + } .padding(10) .overlay( RoundedRectangle(cornerRadius: 18) - .stroke(Color.blue, lineWidth: 0.5) + .stroke(Color.gray, lineWidth: 1) ) - Image(systemName: "arrowshape.turn.up.left.fill") - .symbolRenderingMode(.hierarchical) - .imageScale(.large).foregroundColor(.blue) - .padding(.trailing) + } + } + HStack { + if currentUser && message.receivedACK { + // Ack Received + Text("Acknowledged").font(.caption2).foregroundColor(.gray) + } else if currentUser && message.ackError == 0 { + // Empty Error + Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) + } else if currentUser && message.ackError > 0 { + let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) + Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) + .font(.caption2).foregroundColor(.red) + } } } - HStack (alignment: .top) { - if currentUser { Spacer(minLength:50) } - if !currentUser { - CircleText(text: message.fromUser?.shortName ?? "????", color: currentUser ? .accentColor : Color(.darkGray), circleSize: 44, fontSize: 14) - .padding(.all, 5) - .offset(y: -5) - } - VStack(alignment: currentUser ? .trailing : .leading) { - Text(message.messagePayload ?? "EMPTY MESSAGE") - .padding(10) - .foregroundColor(.white) - .background(currentUser ? Color.blue : Color(.darkGray)) - .cornerRadius(15) - .contextMenu { - VStack{ - Text("Channel: \(message.channel)") - } - Menu("Tapback response") { - ForEach(Tapbacks.allCases) { tb in - Button(action: { - if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, isEmoji: true, replyID: message.messageId) { - print("Sent \(tb.emojiString) Tapback") - self.context.refresh(user, mergeChanges: true) - } else { print("\(tb.emojiString) Tapback Failed") } - - }) { - Text(tb.description) - let image = tb.emojiString.image() - Image(uiImage: image!) - } - } - } - Button(action: { - self.replyMessageId = message.messageId - self.focusedField = .messageText - print("I want to reply to \(message.messageId)") - }) { - Text("Reply") - Image(systemName: "arrowshape.turn.up.left.2.fill") - } - Button(action: { - UIPasteboard.general.string = message.messagePayload - }) { - Text("Copy") - Image(systemName: "doc.on.doc") - } - Menu("Message Details") { - VStack { - let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) - Text("Date \(messageDate, style: .date) \(messageDate.formattedDate(format: "h:mm:ss a"))").font(.caption2).foregroundColor(.gray) - } - if currentUser && message.receivedACK { - VStack { - Text("Received Ack \(message.receivedACK ? "✔️" : "")") - } - } else if currentUser && message.ackError == 0 { - // Empty Error - Text("Waiting. . .") - } else if currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) - Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) - } - if currentUser { - VStack { - let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp)) - let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date()) - if ackDate >= sixMonthsAgo! { - Text((ackDate.formattedDate(format: "h:mm:ss a"))).font(.caption2).foregroundColor(.gray) - } else { - Text("Unknown Age").font(.caption2).foregroundColor(.gray) - } - } - } - if message.ackSNR != 0 { - VStack { - Text("Ack SNR \(String(message.ackSNR))") - .font(.caption2) - .foregroundColor(.gray) - } - } - } - Divider() - Button(role: .destructive, action: { - self.showDeleteMessageAlert = true - self.deleteMessageId = message.messageId - print(deleteMessageId) - }) { - Text("Delete") - Image(systemName: "trash") - } - } - - let tapbacks = message.value(forKey: "tapbacks") as! [MessageEntity] - if tapbacks.count > 0 { - VStack (alignment: .trailing) { - HStack { - ForEach( tapbacks ) { (tapback: MessageEntity) in - VStack { - let image = tapback.messagePayload!.image(fontSize: 20) - Image(uiImage: image!).font(.caption) - Text("\(tapback.fromUser?.shortName ?? "????")") - .font(.caption2) - .foregroundColor(.gray) - .fixedSize() - .padding(.bottom, 1) - } - } - } - .padding(10) - .overlay( - RoundedRectangle(cornerRadius: 18) - .stroke(Color.gray, lineWidth: 1) - ) - } - } - HStack { - if currentUser && message.receivedACK { - // Ack Received - Text("Acknowledged").font(.caption2).foregroundColor(.gray) - } else if currentUser && message.ackError == 0 { - // Empty Error - Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) - } else if currentUser && message.ackError > 0 { - let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) - Text("\(ackErrorVal?.display ?? "No Error" )").fixedSize(horizontal: false, vertical: true) - .font(.caption2).foregroundColor(.red) - } - } - } - .padding(.bottom) - .id(user.messageList.firstIndex(of: message)) - if !currentUser { - Spacer(minLength:50) - } - } - .padding([.leading, .trailing]) - .frame(maxWidth: .infinity) - .id(message.messageId) - .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 = user.messageList.first(where: { $0.messageId == deleteMessageId }) - context.delete(message!) - do { - try context.save() - deleteMessageId = 0 - } catch { - print("Failed to delete message \(deleteMessageId)") - } - } - }, secondaryButton: .cancel()) + .padding(.bottom) + .id(user.messageList.firstIndex(of: message)) + if !currentUser { + Spacer(minLength:50) } } + .padding([.leading, .trailing]) + .frame(maxWidth: .infinity) + .id(message.messageId) + .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 = user.messageList.first(where: { $0.messageId == deleteMessageId }) + context.delete(message!) + do { + try context.save() + deleteMessageId = 0 + } catch { + print("Failed to delete message \(deleteMessageId)") + } + } + }, secondaryButton: .cancel()) + } + } } } From 14547a459c44da5f08cc88a1a679a5361dbd2474 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 7 Nov 2022 18:51:17 -0800 Subject: [PATCH 09/14] Fix up multi channel send --- Meshtastic/Helpers/BLEManager.swift | 18 +++--------------- .../Views/Messages/ChannelMessageList.swift | 4 ++-- .../Views/Messages/UserMessageList.swift | 4 ++-- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 080b3216..f796aeed 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -651,7 +651,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } } - public func sendMessage(message: String, toUserNum: Int64, isEmoji: Bool, replyID: Int64) -> Bool { + public func sendMessage(message: String, toUserNum: Int64, channel: Int32, isEmoji: Bool, replyID: Int64) -> Bool { var success = false @@ -698,25 +698,13 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) newMessage.receivedACK = false newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum }) + newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum }) newMessage.isEmoji = isEmoji newMessage.admin = false - + newMessage.channel = channel if replyID > 0 { - newMessage.replyID = replyID } - if newMessage.toUser == nil { - - let bcu: UserEntity = UserEntity(context: context!) - bcu.shortName = "ALL" - bcu.longName = "All - Broadcast" - bcu.hwModel = "UNSET" - bcu.num = Int64(broadcastNodeNum) - bcu.userId = "BROADCASTNODE" - newMessage.toUser = bcu - } - - newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum }) newMessage.messagePayload = message let dataType = PortNum.textMessageApp diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index 5e256313..ec47a979 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -73,7 +73,7 @@ struct ChannelMessageList: View { Menu("Tapback response") { ForEach(Tapbacks.allCases) { tb in Button(action: { - if bleManager.sendMessage(message: tb.emojiString, toUserNum: Int64(channel.index), isEmoji: true, replyID: message.messageId) { + if bleManager.sendMessage(message: tb.emojiString, toUserNum: 0, channel: channel.index, isEmoji: true, replyID: message.messageId) { print("Sent \(tb.emojiString) Tapback") self.context.refresh(channel, mergeChanges: true) } else { print("\(tb.emojiString) Tapback Failed") } @@ -287,7 +287,7 @@ struct ChannelMessageList: View { .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) .padding(.bottom, 15) Button(action: { - if bleManager.sendMessage(message: typingMessage, toUserNum: Int64(channel.index), isEmoji: false, replyID: replyMessageId) { + if bleManager.sendMessage(message: typingMessage, toUserNum: 0, channel: channel.index, isEmoji: false, replyID: replyMessageId) { typingMessage = "" focusedField = nil replyMessageId = 0 diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 3e13716c..c083b9da 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -75,7 +75,7 @@ struct UserMessageList: View { Menu("Tapback response") { ForEach(Tapbacks.allCases) { tb in Button(action: { - if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, isEmoji: true, replyID: message.messageId) { + if bleManager.sendMessage(message: tb.emojiString, toUserNum: user.num, channel: 0, isEmoji: true, replyID: message.messageId) { print("Sent \(tb.emojiString) Tapback") self.context.refresh(user, mergeChanges: true) } else { print("\(tb.emojiString) Tapback Failed") } @@ -303,7 +303,7 @@ struct UserMessageList: View { .overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1)) .padding(.bottom, 15) Button(action: { - if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, isEmoji: false, replyID: replyMessageId) { + if bleManager.sendMessage(message: typingMessage, toUserNum: user.num, channel: 0, isEmoji: false, replyID: replyMessageId) { typingMessage = "" focusedField = nil replyMessageId = 0 From 58440669311c0b42d61e8b8f068b8e625405037e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 7 Nov 2022 19:06:15 -0800 Subject: [PATCH 10/14] Add conversation preview to private channels --- Meshtastic/Views/Messages/Contacts.swift | 55 ++++++++++++++++++++---- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 8f33f90d..6e9e267b 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -26,28 +26,65 @@ struct Contacts: View { var body: some View { NavigationSplitView { - List { Section(header: Text("Channels")) { // Display Contacts for the rest of the non admin channels if node != nil { ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in if channel.name?.lowercased() ?? "" != "admin" && channel.name?.lowercased() ?? "" != "gpio" { - HStack { - VStack(alignment: .leading) { + VStack { + NavigationLink(destination: ChannelMessageList(channel: channel)) { + + let mostRecent = channel.allPrivateMessages.last + let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) + let lastMessageDay = Calendar.current.dateComponents([.day], from: lastMessageTime).day ?? 0 + let currentDay = Calendar.current.dateComponents([.day], from: Date()).day ?? 0 HStack { - CircleText(text: String(channel.index), color: Color.blue, circleSize: 52, fontSize: 32) - .padding(.trailing, 5) - VStack { - NavigationLink(destination: ChannelMessageList(channel: channel)) { + VStack(alignment: .leading) { + HStack { + CircleText(text: String(channel.index), color: Color.blue, circleSize: 52, fontSize: 40) + .padding(.trailing, 5) + VStack { + Text(String(channel.name ?? "Channel \(channel.index)").camelCaseToWords()).font(.headline) + } + .frame(maxWidth: .infinity, alignment: .leading) - Text(channel.name?.camelCaseToWords() ?? "Channel \(channel.index)").font(.headline) + if channel.allPrivateMessages.count > 0 { + VStack (alignment: .trailing) { + if lastMessageDay == currentDay { + Text(lastMessageTime, style: .time ) + .font(.callout) + .foregroundColor(.gray) + } else if lastMessageDay == (currentDay - 1) { + Text("Yesterday") + .font(.callout) + .foregroundColor(.gray) + } else if lastMessageDay < (currentDay - 1) && lastMessageDay > (currentDay - 5) { + Text(lastMessageTime.formattedDate(format: "MM/dd/yy")) + .font(.callout) + .foregroundColor(.gray) + } else if lastMessageDay < (currentDay - 1800) { + Text(lastMessageTime.formattedDate(format: "MM/dd/yy")) + .font(.callout) + .foregroundColor(.gray) + } + } + } + } + if channel.allPrivateMessages.count > 0 { + HStack(alignment: .top) { + Text("\(mostRecent != nil ? mostRecent!.messagePayload! : " ")") + .truncationMode(.tail) + .foregroundColor(Color.gray) + .frame(maxWidth: .infinity, alignment: .leading) + } } } - .frame(maxWidth: .infinity, alignment: .leading) } } } + .frame(maxWidth: .infinity, alignment: .leading) + } } .padding(.top, 10) From 45e3c08cc0d37c279d98242b8fd1ade667be71aa Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Mon, 7 Nov 2022 19:10:22 -0800 Subject: [PATCH 11/14] Delete broadcast user hacks --- Meshtastic/Helpers/BLEManager.swift | 24 ------------------------ Meshtastic/Helpers/MeshPackets.swift | 9 --------- 2 files changed, 33 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index f796aeed..dc285539 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -592,30 +592,6 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph case .max: print("MAX PORT NUM OF 511") } - - // MARK: Check for an All / Broadcast User - let fetchBCUserRequest: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") - fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(broadcastNodeNum)) - - 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") - } else { - - context!.delete(fetchedUser[0]) - } - - } catch { - MeshLogger.log("💥 Error Saving the All - Broadcast User") - } // MARK: Share Location Position Update Timer // Use context to pass the radio name with the timer diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 516fd12d..df41ad45 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1277,15 +1277,6 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM if packet.decoded.replyID > 0 { newMessage.replyID = Int64(packet.decoded.replyID) } - //if packet.to == broadcastNodeNum && fetchedUsers.count == 1 { - // 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" -// newMessage.toUser = bcu if fetchedUsers.first(where: { $0.num == packet.to }) != nil { newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) From 47271bfbdd7b91e35ce709f72dca2e5eab19bc55 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 8 Nov 2022 11:30:08 -0800 Subject: [PATCH 12/14] Fix multi channel bugs --- Meshtastic/Helpers/BLEManager.swift | 10 +++- Meshtastic/Helpers/MeshPackets.swift | 60 ++++++++----------- .../MeshtasticDataModel.xcdatamodel/contents | 2 +- Meshtastic/Views/Messages/Contacts.swift | 4 +- 4 files changed, 37 insertions(+), 39 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index dc285539..0db2595c 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -673,7 +673,9 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph newMessage.messageId = Int64(UInt32.random(in: UInt32(UInt8.max).. 0 { + newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum }) + } newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum }) newMessage.isEmoji = isEmoji newMessage.admin = false @@ -692,7 +694,11 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph var meshPacket = MeshPacket() meshPacket.id = UInt32(newMessage.messageId) - meshPacket.to = UInt32(toUserNum) + if toUserNum > 0 { + meshPacket.to = UInt32(toUserNum) + } else { + meshPacket.to = 4294967295 + } meshPacket.from = UInt32(fromUserNum) meshPacket.decoded = dataMessage meshPacket.decoded.emoji = isEmoji ? 1 : 0 diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index df41ad45..26c7c2b0 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -1260,13 +1260,6 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM do { let fetchedUsers = try context.fetch(messageUsers) as! [UserEntity] - - if fetchedUsers.count <= 1 && fetchedUsers.first(where: { $0.num == packet.from }) == nil { - - //print("Message from another mesh, unable to manage for now") - //return - } - let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) @@ -1278,46 +1271,45 @@ func textMessageAppPacket(packet: MeshPacket, connectedNode: Int64, context: NSM newMessage.replyID = Int64(packet.decoded.replyID) } - if fetchedUsers.first(where: { $0.num == packet.to }) != nil { + if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != 4294967295 { newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to }) } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) } - - + newMessage.messagePayload = messageText newMessage.fromUser?.objectWillChange.send() newMessage.toUser?.objectWillChange.send() - var messageSaved = false + var messageSaved = false - do { + do { - try context.save() - MeshLogger.log("💾 Saved a new message for \(newMessage.messageId)") - messageSaved = true - - if messageSaved { - if newMessage.fromUser != nil { - // Create an iOS Notification for the received message and schedule it immediately - let manager = LocalNotificationManager() - manager.notifications = [ - Notification( - id: ("notification.id.\(newMessage.messageId)"), - title: "\(newMessage.fromUser?.longName ?? "Unknown")", - subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", - content: messageText) - ] - manager.schedule() - MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")") - } + try context.save() + MeshLogger.log("💾 Saved a new message for \(newMessage.messageId)") + messageSaved = true + + if messageSaved { + if newMessage.fromUser != nil { + // Create an iOS Notification for the received message and schedule it immediately + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(newMessage.messageId)"), + title: "\(newMessage.fromUser?.longName ?? "Unknown")", + subtitle: "AKA \(newMessage.fromUser?.shortName ?? "???")", + content: messageText) + ] + manager.schedule() + MeshLogger.log("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown")") } - } catch { - context.rollback() - let nsError = error as NSError - MeshLogger.log("💥 Failed to save new MessageEntity \(nsError)") } + } catch { + context.rollback() + let nsError = error as NSError + MeshLogger.log("💥 Failed to save new MessageEntity \(nsError)") + } } catch { MeshLogger.log("💥 Fetch Message To and From Users Error") } diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents index 670f7ee5..31ed3498 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModel.xcdatamodel/contents @@ -29,7 +29,7 @@ - + diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index 6e9e267b..b33c4cca 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -27,7 +27,7 @@ struct Contacts: View { NavigationSplitView { List { - Section(header: Text("Channels")) { + Section(header: Text("Channels (groups)")) { // Display Contacts for the rest of the non admin channels if node != nil { ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in @@ -91,7 +91,7 @@ struct Contacts: View { .padding(.bottom, 10) } } - Section(header: Text("Direct Messages")) { + Section(header: Text("Direct Messages (Primary Channel)")) { ForEach(users) { (user: UserEntity) in if user.num != bleManager.userSettings?.preferredNodeNum ?? 0 { NavigationLink(destination: UserMessageList(user: user)) { From cdc4288278e49ea1171adb6896f2350c1406a3eb Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 8 Nov 2022 12:48:16 -0800 Subject: [PATCH 13/14] Multi channel and qr code fixes --- Meshtastic/Helpers/BLEManager.swift | 1 + Meshtastic/Views/Messages/Contacts.swift | 1 - Meshtastic/Views/Settings/ShareChannels.swift | 412 +++++++++--------- 3 files changed, 206 insertions(+), 208 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 0db2595c..672588a8 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -699,6 +699,7 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph } else { meshPacket.to = 4294967295 } + meshPacket.channel = UInt32(channel) meshPacket.from = UInt32(fromUserNum) meshPacket.decoded = dataMessage meshPacket.decoded.emoji = isEmoji ? 1 : 0 diff --git a/Meshtastic/Views/Messages/Contacts.swift b/Meshtastic/Views/Messages/Contacts.swift index b33c4cca..4238ad61 100644 --- a/Meshtastic/Views/Messages/Contacts.swift +++ b/Meshtastic/Views/Messages/Contacts.swift @@ -84,7 +84,6 @@ struct Contacts: View { } } .frame(maxWidth: .infinity, alignment: .leading) - } } .padding(.top, 10) diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 33ccb9e8..4862835c 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -49,223 +49,221 @@ struct ShareChannels: View { var body: some View { - VStack { - GeometryReader { bounds in - let smallest = min(bounds.size.width, bounds.size.height) - ScrollView { - VStack { - if node != nil && node?.myInfo != nil { - Grid(alignment: .top) { + // VStack { + GeometryReader { bounds in + let smallest = min(bounds.size.width, bounds.size.height) + ScrollView { + if node != nil && node?.myInfo != nil { + Grid() { + GridRow { + Spacer() + Text("Include") + .font(.caption) + .fontWeight(.bold) + .padding(.trailing) + Text("Channel") + .font(.caption) + .fontWeight(.bold) + .padding(.trailing) + Text("Encrypted") + .font(.caption) + .fontWeight(.bold) + } + ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in GridRow { Spacer() - Text("Include") - .font(.caption) - .fontWeight(.bold) - .padding(.trailing) - Text("Channel") - .font(.caption) - .fontWeight(.bold) - .padding(.trailing) - Text("Encrypted") - .font(.caption) - .fontWeight(.bold) - } - ForEach(node!.myInfo!.channels?.array as! [ChannelEntity], id: \.self) { (channel: ChannelEntity) in - GridRow { - Spacer() - if channel.index == 0 { - Toggle("Channel 0 Included", isOn: $includeChannel0) - .toggleStyle(.switch) - .labelsHidden() - .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()).fixedSize() - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } - } else if channel.index == 1 && channel.role > 0 { - Toggle("Channel 1 Included", isOn: $includeChannel1) - .toggleStyle(.switch) - .labelsHidden() - .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } - } else if channel.index == 2 && channel.role > 0 { - Toggle("Channel 2 Included", isOn: $includeChannel2) - .toggleStyle(.switch) - .labelsHidden() - .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } - } else if channel.index == 3 && channel.role > 0 { - Toggle("Channel 3 Included", isOn: $includeChannel3) - .toggleStyle(.switch) - .labelsHidden() - .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } - } else if channel.index == 4 && channel.role > 0 { - Toggle("Channel 4 Included", isOn: $includeChannel4) - .toggleStyle(.switch) - .labelsHidden() - .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } - } else if channel.index == 5 && channel.role > 0 { - Toggle("Channel 5 Included", isOn: $includeChannel5) - .toggleStyle(.switch) - .labelsHidden() - .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } - } else if channel.index == 6 && channel.role > 0 { - Toggle("Channel 6 Included", isOn: $includeChannel6) - .toggleStyle(.switch) - .labelsHidden() - .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } - } else if channel.index == 7 && channel.role > 0 { - Toggle("Channel 7 Included", isOn: $includeChannel7) - .toggleStyle(.switch) - .labelsHidden() - .disabled(channel.role == 1) - Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() - if channel.psk?.hexDescription.count ?? 0 < 3 { - Image(systemName: "lock.slash") - .foregroundColor(.red) - } else { - Image(systemName: "lock.fill") - .foregroundColor(.green) - } + if channel.index == 0 { + Toggle("Channel 0 Included", isOn: $includeChannel0) + .toggleStyle(.switch) + .labelsHidden() + .disabled(channel.role == 1) + Text(((channel.name!.isEmpty ? "Primary" : channel.name) ?? "Primary").camelCaseToWords()).fixedSize() + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else if channel.index == 1 && channel.role > 0 { + Toggle("Channel 1 Included", isOn: $includeChannel1) + .toggleStyle(.switch) + .labelsHidden() + .disabled(channel.role == 1) + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else if channel.index == 2 && channel.role > 0 { + Toggle("Channel 2 Included", isOn: $includeChannel2) + .toggleStyle(.switch) + .labelsHidden() + .disabled(channel.role == 1) + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else if channel.index == 3 && channel.role > 0 { + Toggle("Channel 3 Included", isOn: $includeChannel3) + .toggleStyle(.switch) + .labelsHidden() + .disabled(channel.role == 1) + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else if channel.index == 4 && channel.role > 0 { + Toggle("Channel 4 Included", isOn: $includeChannel4) + .toggleStyle(.switch) + .labelsHidden() + .disabled(channel.role == 1) + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else if channel.index == 5 && channel.role > 0 { + Toggle("Channel 5 Included", isOn: $includeChannel5) + .toggleStyle(.switch) + .labelsHidden() + .disabled(channel.role == 1) + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else if channel.index == 6 && channel.role > 0 { + Toggle("Channel 6 Included", isOn: $includeChannel6) + .toggleStyle(.switch) + .labelsHidden() + .disabled(channel.role == 1) + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else if channel.index == 7 && channel.role > 0 { + Toggle("Channel 7 Included", isOn: $includeChannel7) + .toggleStyle(.switch) + .labelsHidden() + .disabled(channel.role == 1) + Text(((channel.name!.isEmpty ? "Channel\(channel.index)" : channel.name) ?? "Channel\(channel.index)").camelCaseToWords()).fixedSize() + if channel.psk?.hexDescription.count ?? 0 < 3 { + Image(systemName: "lock.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) } - Spacer() } - .padding(0) - } - } - - let qrImage = qrCodeImage.generateQRCode(from: channelsUrl) - VStack { - if node != nil { - ShareLink("Share QR Code & Link", - item: Image(uiImage: qrImage), - subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"), - message: Text(channelsUrl), - preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you", - image: Image(uiImage: qrImage)) - ) - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) - - Image(uiImage: qrImage) - .resizable() - .scaledToFit() - .frame( - minWidth: smallest * 0.95, - maxWidth: smallest * 0.95, - minHeight: smallest * 0.95, - maxHeight: smallest * 0.95, - alignment: .top - ) - Button { - isPresentingHelp = true - } label: { - Label("Help Me!", systemImage: "lifepreserver") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.small) + Spacer() } } } - + + let qrImage = qrCodeImage.generateQRCode(from: channelsUrl) + VStack { + if node != nil { + ShareLink("Share QR Code & Link", + item: Image(uiImage: qrImage), + subject: Text("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you"), + message: Text(channelsUrl), + preview: SharePreview("Meshtastic Node \(node?.user?.shortName ?? "????") has shared channels with you", + image: Image(uiImage: qrImage)) + ) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) + + Image(uiImage: qrImage) + .resizable() + .scaledToFit() + .frame( + minWidth: smallest * 0.95, + maxWidth: smallest * 0.95, + minHeight: smallest * 0.95, + maxHeight: smallest * 0.95, + alignment: .top + ) + Button { + isPresentingHelp = true + } label: { + Label("Help Me!", systemImage: "lifepreserver") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } + } } - } - .sheet(isPresented: $isPresentingHelp) { - VStack { - Text("Meshtastic Channels").font(.title) - Text("A Meshtastic LoRa Mesh network can have up to 8 distinct channels.") - .font(.headline) - .padding(.bottom) - Text("Primary Channel").font(.title2) - Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled.") - .font(.callout) - .padding([.leading,.trailing,.bottom]) - Text("Admin Channel").font(.title2) - Text("A channel with the name 'admin' is the Admin channel and can be used to remotely administer nodes on your mesh, text messages can not be sent over the admin channel.") - .font(.callout) - .padding([.leading,.trailing,.bottom]) - Text("Private Channels").font(.title2) - Text("The other channels can be used for private group converations. Each of these groups has its own encryption key.") - .font(.callout) - .padding([.leading,.trailing,.bottom]) - Divider() - } - .padding() - .presentationDetents([.large]) - .presentationDragIndicator(.automatic) - } - .navigationTitle("Generate QR Code") - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") - }) - .onAppear { - bleManager.context = context - GenerateChannelSet() - } - .onChange(of: includeChannel1) { includeCh1 in GenerateChannelSet() } - .onChange(of: includeChannel2) { includeCh2 in GenerateChannelSet() } - .onChange(of: includeChannel3) { includeCh3 in GenerateChannelSet() } - .onChange(of: includeChannel4) { includeCh4 in GenerateChannelSet() } - .onChange(of: includeChannel5) { includeCh5 in GenerateChannelSet() } - .onChange(of: includeChannel6) { includeCh6 in GenerateChannelSet() } - .onChange(of: includeChannel7) { includeCh7 in GenerateChannelSet() } + + //} } + .sheet(isPresented: $isPresentingHelp) { + VStack { + Text("Meshtastic Channels").font(.title) + Text("A Meshtastic LoRa Mesh network can have up to 8 distinct channels.") + .font(.headline) + .padding(.bottom) + Text("Primary Channel").font(.title2) + Text("The first channel is the Primary channel and is where much of the mesh activity takes place. DM's are only available on the primary channel and it can not be disabled.") + .font(.callout) + .padding([.leading,.trailing,.bottom]) + Text("Admin Channel").font(.title2) + Text("A channel with the name 'admin' is the Admin channel and can be used to remotely administer nodes on your mesh, text messages can not be sent over the admin channel.") + .font(.callout) + .padding([.leading,.trailing,.bottom]) + Text("Private Channels").font(.title2) + Text("The other channels can be used for private group converations. Each of these groups has its own encryption key.") + .font(.callout) + .padding([.leading,.trailing,.bottom]) + Divider() + } + .padding() + .presentationDetents([.large]) + .presentationDragIndicator(.automatic) + } + .navigationTitle("Generate QR Code") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems(trailing: + ZStack { + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "????") + }) + .onAppear { + bleManager.context = context + GenerateChannelSet() + } + .onChange(of: includeChannel1) { includeCh1 in GenerateChannelSet() } + .onChange(of: includeChannel2) { includeCh2 in GenerateChannelSet() } + .onChange(of: includeChannel3) { includeCh3 in GenerateChannelSet() } + .onChange(of: includeChannel4) { includeCh4 in GenerateChannelSet() } + .onChange(of: includeChannel5) { includeCh5 in GenerateChannelSet() } + .onChange(of: includeChannel6) { includeCh6 in GenerateChannelSet() } + .onChange(of: includeChannel7) { includeCh7 in GenerateChannelSet() } } + // } } func GenerateChannelSet() { channelSet = ChannelSet() From e798cd3e7a524a5fa2dfac2619412bae53c38840 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Tue, 8 Nov 2022 13:01:43 -0800 Subject: [PATCH 14/14] Delete broadcast user --- Meshtastic/Helpers/BLEManager.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 672588a8..0b70a265 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -592,6 +592,22 @@ class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeriph case .max: print("MAX PORT NUM OF 511") } + + // MARK: Check for an All / Broadcast User and delete it as a transition to multi channel + let fetchBCUserRequest: NSFetchRequest = NSFetchRequest.init(entityName: "UserEntity") + fetchBCUserRequest.predicate = NSPredicate(format: "num == %lld", Int64(broadcastNodeNum)) + + do { + let fetchedUser = try context?.fetch(fetchBCUserRequest) as! [UserEntity] + if fetchedUser.count > 0 { + + context?.delete(fetchedUser[0]) + print("🗑️ Deleted the All - Broadcast User") + } + + } catch { + MeshLogger.log("💥 Error Deleting the All - Broadcast User") + } // MARK: Share Location Position Update Timer // Use context to pass the radio name with the timer