From ce3aacba6374e4ac83495f7c87fb50e8ac20909e Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 15 Aug 2024 16:31:58 -0700 Subject: [PATCH] Help! --- Localizable.xcstrings | 35 ++++++++- Meshtastic.xcodeproj/project.pbxproj | 20 +++++ Meshtastic/Enums/RoutingError.swift | 4 +- Meshtastic/Tips/MessagesTips.swift | 19 ----- Meshtastic/Views/Helpers/Help/AckErrors.swift | 44 +++++++++++ .../Helpers/Help/DirectMessagesHelp.swift | 75 +++++++++++++++++++ .../Views/Helpers/Help/LockLegend.swift | 61 +++++++++++++++ Meshtastic/Views/Messages/UserList.swift | 35 ++++++--- 8 files changed, 258 insertions(+), 35 deletions(-) create mode 100644 Meshtastic/Views/Helpers/Help/AckErrors.swift create mode 100644 Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift create mode 100644 Meshtastic/Views/Helpers/Help/LockLegend.swift diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 785091d8..798611fc 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -4810,9 +4810,6 @@ } } } - }, - "Contacts" : { - }, "contacts %@" : { "extractionState" : "migrated", @@ -6315,6 +6312,15 @@ }, "Direct" : { + }, + "Direct Message Help" : { + + }, + "Direct messages are using the new public key infrastructure to encrypt the message." : { + + }, + "Direct messages are using the shared key for the channel when communicating with this node." : { + }, "direct.messages" : { "localizations" : { @@ -7204,7 +7210,7 @@ "Favorites" : { }, - "Favorites and nodes with recent messages show up at the top of the list. Contacts using the shared key display an open lock, nodes with a private key show a green lock and a red key with a slash will show up if a key has changed for a contact. Long press to favorite or mute the contact or delete a conversation." : { + "Favorites and nodes with recent messages show up at the top of the contact list." : { }, "Fifteen Minutes" : { @@ -11272,6 +11278,9 @@ }, "Long Name: %@" : { + }, + "Long press to favorite or mute the contact or delete a conversation." : { + }, "Longitude" : { @@ -14527,6 +14536,9 @@ }, "Message" : { + }, + "Message Status Options" : { + }, "message.details" : { "localizations" : { @@ -15406,6 +15418,9 @@ } } } + }, + "Node Encryption Status" : { + }, "Node History" : { @@ -16665,6 +16680,9 @@ }, "Public Key" : { + }, + "Public Key Encryption" : { + }, "Public Key Mismatch" : { @@ -19763,6 +19781,9 @@ } } } + }, + "Shared Key" : { + }, "Short Name" : { @@ -20995,6 +21016,9 @@ }, "The public key authorized to send admin messages to this node." : { + }, + "The public key does not match the key that was used previously, delete the node and let it negotatiate keys again. Usually the other user did a factory reset, but it could indicate a security issue." : { + }, "The region where you will be using your radios." : { @@ -22519,6 +22543,9 @@ }, "Website" : { + }, + "What does the lock mean?" : { + }, "What is Meshtastic?" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 2e083bd6..2c59e43b 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -94,6 +94,9 @@ DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65712C6AB8EC0053C113 /* SecureInput.swift */; }; DD6F65742C6CB80A0053C113 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65732C6CB80A0053C113 /* View.swift */; }; + DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65752C6EA5490053C113 /* AckErrors.swift */; }; + DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */; }; + DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F657A2C6EC2900053C113 /* LockLegend.swift */; }; DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; }; @@ -349,6 +352,9 @@ DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; DD6F65732C6CB80A0053C113 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + DD6F65752C6EA5490053C113 /* AckErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AckErrors.swift; sourceTree = ""; }; + DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesHelp.swift; sourceTree = ""; }; + DD6F657A2C6EC2900053C113 /* LockLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockLegend.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 = ""; }; DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = ""; }; @@ -710,6 +716,16 @@ path = Module; sourceTree = ""; }; + DD6F65772C6EAB860053C113 /* Help */ = { + isa = PBXGroup; + children = ( + DD6F65752C6EA5490053C113 /* AckErrors.swift */, + DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */, + DD6F657A2C6EC2900053C113 /* LockLegend.swift */, + ); + path = Help; + sourceTree = ""; + }; DD7709392AA1ABA1007A8BF0 /* Tips */ = { isa = PBXGroup; children = ( @@ -902,6 +918,7 @@ DDC2E18D26CE25CB0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + DD6F65772C6EAB860053C113 /* Help */, DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */, @@ -1300,6 +1317,7 @@ DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */, + DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */, 25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */, DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, 25C49D902C471AEA0024FBD1 /* Constants.swift in Sources */, @@ -1358,6 +1376,7 @@ 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */, D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */, DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, + DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */, DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */, DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */, DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */, @@ -1385,6 +1404,7 @@ DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */, 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */, DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, + DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 4bdb8388..b47a15f9 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -54,7 +54,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .notAuthorized: return "routing.notauthorized".localized case .pkiFailed: - return "routing.pkiFailed".localized + return "routing.pkifailed".localized case .pkiUnknownPubkey: return "routing.pkiunknownpubkey".localized } @@ -77,7 +77,7 @@ enum RoutingError: Int, CaseIterable, Identifiable { case .noChannel: return Color.orange case .tooLarge: - return Color.red + return Color.orange case .noResponse: return Color.orange case .dutyCycleLimit: diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift index 5bd50d54..4771e386 100644 --- a/Meshtastic/Tips/MessagesTips.swift +++ b/Meshtastic/Tips/MessagesTips.swift @@ -25,22 +25,3 @@ struct MessagesTip: Tip { Image(systemName: "bubble.left.and.bubble.right") } } - -@available(iOS 17.0, macOS 14.0, *) -struct ContactsTip: Tip { - - var id: String { - return "tip.messages.contacts" - } - var title: Text { - // Text("tip.messages.contacts.title") - Text("Contacts") - } - var message: Text? { - // Text("tip.messages.contacts.message") - Text("Favorites and nodes with recent messages show up at the top of the list. Contacts using the shared key display an open lock, nodes with a private key show a green lock and a red key with a slash will show up if a key has changed for a contact. Long press to favorite or mute the contact or delete a conversation.") - } - var image: Image? { - Image(systemName: "person.circle") - } -} diff --git a/Meshtastic/Views/Helpers/Help/AckErrors.swift b/Meshtastic/Views/Helpers/Help/AckErrors.swift new file mode 100644 index 00000000..c20a58f9 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/AckErrors.swift @@ -0,0 +1,44 @@ +// +// IAQScale.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 4/24/24. +// + +import SwiftUI + +struct AckErrors: View { + + var body: some View { + VStack(alignment: .leading) { + Text("Message Status Options") + .font(.title2) + HStack { + RoundedRectangle(cornerRadius: 5) + .fill(.orange) + .frame(width: 20, height: 12) + Text("Acknowledged by another node") + .font(.caption) + .foregroundStyle(.orange) + } + ForEach(RoutingError.allCases) { re in + HStack { + RoundedRectangle(cornerRadius: 5) + .fill(re.color) + .frame(width: 20, height: 12) + Text(re.display) + .font(.caption) + .foregroundStyle(re.color) + } + } + } + } +} + +struct AckErrorsPreviews: PreviewProvider { + static var previews: some View { + VStack { + AckErrors() + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift new file mode 100644 index 00000000..dd57f29f --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift @@ -0,0 +1,75 @@ +// +// DirectMessagesHelp.swift +// Meshtastic +// +// Copyright Garth Vander Houwen on 8/15/24. +// + +import SwiftUI + +struct DirectMessagesHelp: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Environment(\.dismiss) private var dismiss + + var body: some View { + ScrollView { + Text("Direct Message Help") + .font(.title) + .padding(.vertical) + VStack(alignment: .leading) { + HStack { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + .padding(.bottom) + Text("Favorites and nodes with recent messages show up at the top of the contact list.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "hand.tap") + .padding(.bottom) + Text("Long press to favorite or mute the contact or delete a conversation.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + } + if idiom == .phone { + VStack(alignment: .leading) { + LockLegend() + AckErrors() + } + } else { + HStack(alignment: .top) { + LockLegend() + AckErrors() + } + } +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + } + .frame(minHeight: 0, maxHeight: .infinity, alignment: .leading) + .padding() + .presentationDetents([.large]) + .presentationContentInteraction(.scrolls) + .presentationDragIndicator(.visible) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) + } +} + +struct DirectMessagesHelpPreviews: PreviewProvider { + static var previews: some View { + VStack { + AckErrors() + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/LockLegend.swift b/Meshtastic/Views/Helpers/Help/LockLegend.swift new file mode 100644 index 00000000..b47aaf20 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/LockLegend.swift @@ -0,0 +1,61 @@ +// +// LockLegend.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 8/15/24. +// + +import SwiftUI + +struct LockLegend: View { + + var body: some View { + VStack(alignment: .leading) { + Text("Node Encryption Status") + .font(.title2) + Text("What does the lock mean?") + .padding(.bottom) + VStack(alignment: .leading) { + HStack { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + Text("Shared Key") + .fontWeight(.semibold) + } + Text("Direct messages are using the shared key for the channel when communicating with this node.") + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + VStack(alignment: .leading) { + HStack { + Image(systemName: "lock.fill") + .foregroundColor(.green) + Text("Public Key Encryption") + .fontWeight(.semibold) + } + Text("Direct messages are using the new public key infrastructure to encrypt the message.") + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + VStack(alignment: .leading) { + HStack { + Image(systemName: "key.slash") + .foregroundColor(.red) + Text("Public Key Mismatch") + .fontWeight(.semibold) + } + Text("The public key does not match the key that was used previously, delete the node and let it negotatiate keys again. Usually the other user did a factory reset, but it could indicate a security issue.") + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + } + } +} + +struct LockLegendPreviews: PreviewProvider { + static var previews: some View { + VStack { + LockLegend() + } + } +} diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 5294c752..e3d07d3a 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -28,9 +28,10 @@ struct UserList: View { @State private var hopsAway: Double = -1.0 @State private var roleFilter = false @State private var deviceRoles: Set = [] - @State var isEditingFilters = false + @State private var editingFilters = false + @State private var showingHelp = false @State private var showingTrustConfirm: Bool = false - + var boolFilters: [Bool] {[ isFavorite, isOnline, @@ -59,9 +60,6 @@ struct UserList: View { let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { List(selection: $userSelection) { - if #available(iOS 17.0, macOS 14.0, *) { - TipView(ContactsTip(), arrowEdge: .bottom) - } ForEach(users) { (user: UserEntity) in let mostRecent = user.messageList.last let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) @@ -196,9 +194,12 @@ struct UserList: View { } .listStyle(.plain) .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count))) - .sheet(isPresented: $isEditingFilters) { + .sheet(isPresented: $editingFilters) { NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) } + .sheet(isPresented: $showingHelp) { + DirectMessagesHelp() + } .onChange(of: searchText) { _ in searchUserList() } @@ -229,25 +230,39 @@ struct UserList: View { .onFirstAppear { searchUserList() } - .safeAreaInset(edge: .bottom, alignment: .trailing) { + .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { Button(action: { withAnimation { - isEditingFilters = !isEditingFilters + showingHelp = !showingHelp } }) { - Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + Image(systemName: !editingFilters ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + + Spacer() + + Button(action: { + withAnimation { + editingFilters = !editingFilters + } + }) { + Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - } .controlSize(.regular) .padding(5) } .padding(.bottom, 5) + .padding(.bottom, 5) .searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately)