diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 9c9bd8d1..9bbbd0bd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -2040,6 +2040,7 @@ } }, "A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key." : { + "extractionState" : "stale", "localizations" : { "sr" : { "stringUnit" : { @@ -2049,6 +2050,10 @@ } } }, + "A yellow open lock means the channel is not securely encrypted but it is not used for precise location data, it uses either no key at all or a 1 byte known key." : { + "comment" : "A description of a yellow open lock in the Channels Help view.", + "isCommentAutoGenerated" : true + }, "About" : { "localizations" : { "de" : { @@ -3900,6 +3905,10 @@ } } }, + "Anonymous Usage and Crash data" : { + "comment" : "A description of how the app collects and uses data about its usage and crashes. It emphasizes that this data is anonymous and non-personally identifiable.", + "isCommentAutoGenerated" : true + }, "Any missed messages will be delivered again." : { "localizations" : { "it" : { @@ -12962,24 +12971,24 @@ } } }, - "Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices." : { + "Enabling Ethernet will disable the bluetooth connection to the app." : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", - "value" : "Abilitando l'Ethernet verrà disabilita la connessione bluetooth all'applicazione. La connessione a nodi TCP non è disponibile su dispositivi Apple." + "value" : "Abilitando l'Ethernet verrà disabilita la connessione bluetooth all'applicazione." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "Ethernetを有効にすると、アプリへのBluetooth接続が無効になります。AppleデバイスではTCPノード接続は利用できません。" + "value" : "Ethernetを有効にすると、アプリへのBluetooth接続が無効になります。" } }, "sr" : { "stringUnit" : { "state" : "translated", - "value" : "Омогућавање Ethernet-а ће онемогућити Bluetooth везу са апликацијом. TCP везе са чвором нису доступне на Apple уређајима.\n" + "value" : "Омогућавање Ethernet-а ће онемогућити Bluetooth везу са апликацијом." } } } @@ -41031,6 +41040,10 @@ }, "Waypoints" : { + }, + "We anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : { + "comment" : "A description of how the app collects and uses user data. Includes a link to the app settings.", + "isCommentAutoGenerated" : true }, "Weather Conditions" : { "localizations" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 04622d29..63f334e5 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -330,7 +330,6 @@ /* Begin PBXFileReference section */ 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareContactQRDialog.swift; sourceTree = ""; }; 108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityToNodeInfo.swift; sourceTree = ""; }; - 10A0FE142E9290B900002DF6 /* MeshtasticDataModelV 55.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 55.xcdatamodel"; sourceTree = ""; }; 230BC3962E31071E0046BF2A /* AccessoryManager+Discovery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Discovery.swift"; sourceTree = ""; }; 231251372E3BC96400E6ED07 /* BLEAuthorizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEAuthorizationHelper.swift; sourceTree = ""; }; 231A53772E69ADB900216B99 /* NodeFilterParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeFilterParameters.swift; sourceTree = ""; }; @@ -2095,7 +2094,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.4; + MARKETING_VERSION = 2.7.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2130,7 +2129,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.4; + MARKETING_VERSION = 2.7.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2162,7 +2161,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.4; + MARKETING_VERSION = 2.7.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2195,7 +2194,7 @@ "@executable_path/../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 2.7.4; + MARKETING_VERSION = 2.7.5; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 1448ab67..e0478902 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -165,15 +165,8 @@ extension AccessoryManager { nodeMeshPacket.decoded = dataNodeMessage // Update local database with the new node info - upsertNodeInfoPacket(packet: nodeMeshPacket, context: context) + upsertNodeInfoPacket(packet: nodeMeshPacket, favorite: true, context: context) } - - // Refresh the config from the node, in a background task - Task { - Logger.transport.debug("[AccessoryManager] sending wantConfig for addContactFromURL") - try? await sendWantConfig() - } - } catch { Logger.data.error("Failed to decode contact data: \(error.localizedDescription, privacy: .public)") throw AccessoryError.appError("Unable to decode contact data from QR code.") diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index ee50ab10..ffb716c2 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -91,6 +91,17 @@ class LocalNotificationManager { } } + func cancelNotificationForMessageId(_ messageId: Int64) { + let center = UNUserNotificationCenter.current() + center.getPendingNotificationRequests { notifications in + for notification in notifications { + if let userInfo = notification.content.userInfo["messageId"] as? Int64, userInfo == messageId { + Logger.services.debug("Cancelling notification with id: \(notification.identifier)") + center.removePendingNotificationRequests(withIdentifiers: [notification.identifier]) + } + } + } + } } struct Notification { diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 9dbb9a5a..3f758329 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -165,7 +165,7 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes } } -func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) { +func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, packet.from.toHex()) Logger.mesh.info("📟 \(logString, privacy: .public)") @@ -183,6 +183,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) let newNode = NodeInfoEntity(context: context) newNode.id = Int64(packet.from) newNode.num = Int64(packet.from) + newNode.favorite = favorite if packet.rxTime > 0 { newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) diff --git a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift index ad8b3b06..ec78ed9e 100644 --- a/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift +++ b/Meshtastic/Views/Helpers/Help/ChannelsHelp.swift @@ -41,7 +41,7 @@ struct ChannelsHelp: View { .padding(.leading) .foregroundColor(Color.yellow) .font(.title) - Text("A yellow open lock lock means the channel is not securely encrypted but it not used for precise location data, it uses either no key at all or a 1 byte known key.") + Text("A yellow open lock means the channel is not securely encrypted but it is not used for precise location data, it uses either no key at all or a 1 byte known key.") .fixedSize(horizontal: false, vertical: true) .padding(.bottom) } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index e224b446..857028f1 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -80,8 +80,16 @@ struct ChannelMessageList: View { onInteractionComplete: handleInteractionComplete ) .onAppear { - if !message.read { - markMessagesAsRead() + // Only mark as read if the app is in the foreground + if !message.read && UIApplication.shared.applicationState == .active { + message.read = true + LocalNotificationManager().cancelNotificationForMessageId(message.messageId) + // Race condition, sometimes the app doesn't update unread count if we run this too early + // So, run it in the main queue after everything saves and stabilizes + DispatchQueue.main.async { + markMessagesAsRead() + scrollView.scrollTo("bottomAnchor", anchor: .bottom) + } } } .id(redrawTapbacksTrigger) diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index c2be4627..87afb16f 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -68,8 +68,16 @@ struct UserMessageList: View { onInteractionComplete: handleInteractionComplete ) .onAppear { - if !message.read { - markMessagesAsRead() // Use the function to mark all unread + // Only mark as read if the app is in the foreground + if !message.read && UIApplication.shared.applicationState == .active { + message.read = true + LocalNotificationManager().cancelNotificationForMessageId(message.messageId) + // Race condition, sometimes the app doesn't update unread count if we run this too early + // So, run it in the main queue after everything saves and stabilizes + DispatchQueue.main.async { + markMessagesAsRead() + scrollView.scrollTo("bottomAnchor", anchor: .bottom) + } } } .id(redrawTapbacksTrigger) diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index 71dff19b..6f7c87a3 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -56,6 +56,11 @@ struct DeviceOnboarding: View { title: String(localized: "Track and Share Locations"), subtitle: String(localized: "Share your location in real-time and keep your group coordinated with integrated GPS features.") ) + makeRow( + icon: "person.2.shield", + title: String(localized: "User Privacy"), + subtitle: String(localized: "Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings.") + ) } .padding() } diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index a65eb6e3..4e1ab892 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -3,6 +3,7 @@ import Combine import SwiftUI import SwiftProtobuf import MapKit +import DatadogCore import OSLog struct AppSettings: View { @@ -42,8 +43,6 @@ struct AppSettings: View { Text("PKI based node administration, requires firmware version 2.5+") .foregroundStyle(.secondary) .font(.caption) -#if targetEnvironment(macCatalyst) -#else Toggle(isOn: $usageDataAndCrashReporting) { Label("Usage and Crash Data", systemImage: "pencil.and.list.clipboard") } @@ -51,7 +50,6 @@ struct AppSettings: View { Text("Provide anonymous usage statistics and crash reports.") .foregroundStyle(.secondary) .font(.caption) -#endif if showAutoConnect { Toggle(isOn: autoconnectBinding) { Label("Automatically Connect", systemImage: "app.connected.to.app.below.fill") @@ -95,6 +93,11 @@ struct AppSettings: View { } #endif } + .onChange(of: usageDataAndCrashReporting) { oldUsageDataAndCrashReporting, newUsageDataAndCrashReporting in + if !newUsageDataAndCrashReporting { + Datadog.set(trackingConsent: .notGranted) + } + } .onChange(of: purgeStaleNodes) { _, newValue in purgeStaleNodeDays = purgeStaleNodeDays > 0 ? purgeStaleNodeDays : 7 purgeStaleNodeDays = newValue ? purgeStaleNodeDays : 0 diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index 0b91da52..92c5cc5e 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -75,12 +75,13 @@ struct NetworkConfig: View { } .keyboardType(.default) } - } - if node.metadata?.hasEthernet ?? false { - Section(header: Text("Ethernet Options")) { - Toggle(isOn: $ethEnabled) { - Label("Enabled", systemImage: "network") - Text("Enabling Ethernet will disable the bluetooth connection to the app. TCP node connections are not available on Apple devices.") + if node.metadata?.hasEthernet ?? false { + Section(header: Text("Ethernet Options")) { + Toggle(isOn: $ethEnabled) { + Label("Enabled", systemImage: "network") + Text("Enabling Ethernet will disable the bluetooth connection to the app.") + } + .tint(.accentColor) } .tint(.accentColor) }