mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Actionable Notifs
This commit is contained in:
parent
6d97cf0aae
commit
359dbacb44
5 changed files with 132 additions and 29 deletions
|
|
@ -42,8 +42,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-protobuf.git",
|
||||
"state" : {
|
||||
"revision" : "d57a5aecf24a25b32ec4a74be2f5d0a995a47c4b",
|
||||
"version" : "1.27.0"
|
||||
"revision" : "edb6ed4919f7756157fe02f2552b7e3850a538e5",
|
||||
"version" : "1.28.1"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -3299,7 +3299,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
// MARK: - CB Central Manager implmentation
|
||||
extension BLEManager: CBCentralManagerDelegate {
|
||||
|
||||
|
||||
// MARK: Bluetooth enabled/disabled
|
||||
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
||||
if central.state == CBManagerState.poweredOn {
|
||||
|
|
@ -3309,9 +3309,9 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
} else {
|
||||
isSwitchedOn = false
|
||||
}
|
||||
|
||||
|
||||
var status = ""
|
||||
|
||||
|
||||
switch central.state {
|
||||
case .poweredOff:
|
||||
status = "BLE is powered off"
|
||||
|
|
@ -3330,10 +3330,10 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
}
|
||||
Logger.services.info("📜 [BLE] Bluetooth status: \(status)")
|
||||
}
|
||||
|
||||
|
||||
// Called each time a peripheral is discovered
|
||||
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
||||
|
||||
|
||||
if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" {
|
||||
self.connectTo(peripheral: peripheral)
|
||||
Logger.services.info("✅ [BLE] Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown", privacy: .public)")
|
||||
|
|
@ -3341,7 +3341,7 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String
|
||||
let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "?", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral)
|
||||
let index = peripherals.map { $0.peripheral }.firstIndex(of: peripheral)
|
||||
|
||||
|
||||
if let peripheralIndex = index {
|
||||
peripherals[peripheralIndex] = device
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,16 @@ import OSLog
|
|||
class LocalNotificationManager {
|
||||
|
||||
var notifications = [Notification]()
|
||||
|
||||
let thumbsUpAction = UNNotificationAction(identifier: "messageNotification.thumbsUpAction", title:
|
||||
"👍 \(Tapbacks.thumbsUp.description)", options: [])
|
||||
let thumbsDownAction = UNNotificationAction(identifier: "messageNotification.thumbsDownAction", title:
|
||||
"👎 \(Tapbacks.thumbsDown.description)", options: [])
|
||||
let replyInputAction = UNTextInputNotificationAction(
|
||||
identifier: "messageNotification.replyInputAction",
|
||||
title: "reply".localized,
|
||||
options: [])
|
||||
|
||||
|
||||
// Step 1 Request Permissions for notifications
|
||||
private func requestAuthorization() {
|
||||
|
|
@ -31,6 +41,15 @@ class LocalNotificationManager {
|
|||
|
||||
// This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future
|
||||
private func scheduleNotifications() {
|
||||
let messageNotificationCategory = UNNotificationCategory(
|
||||
identifier: "messageNotificationCategory",
|
||||
actions: [thumbsUpAction, thumbsDownAction,replyInputAction],
|
||||
intentIdentifiers: [],
|
||||
options: .customDismissAction
|
||||
)
|
||||
|
||||
UNUserNotificationCenter.current().setNotificationCategories([messageNotificationCategory])
|
||||
|
||||
for notification in notifications {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.subtitle = notification.subtitle
|
||||
|
|
@ -45,6 +64,17 @@ class LocalNotificationManager {
|
|||
if notification.path != nil {
|
||||
content.userInfo["path"] = notification.path
|
||||
}
|
||||
if notification.messageId != nil {
|
||||
content.categoryIdentifier = "messageNotificationCategory"
|
||||
content.userInfo["messageId"] = notification.messageId
|
||||
}
|
||||
if notification.channel != nil {
|
||||
content.userInfo["channel"] = notification.channel
|
||||
}
|
||||
if notification.userNum != nil {
|
||||
content.userInfo["userNum"] = notification.userNum
|
||||
}
|
||||
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger)
|
||||
|
|
@ -76,4 +106,7 @@ struct Notification {
|
|||
var content: String
|
||||
var target: String?
|
||||
var path: String?
|
||||
var messageId: Int64?
|
||||
var channel: Int32?
|
||||
var userNum: Int64?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -815,12 +815,10 @@ func textMessageAppPacket(
|
|||
context: NSManagedObjectContext,
|
||||
appState: AppState
|
||||
) {
|
||||
|
||||
var messageText = String(bytes: packet.decoded.payload, encoding: .utf8)
|
||||
let rangeRef = Reference(Int.self)
|
||||
let rangeTestRegex = Regex {
|
||||
"seq "
|
||||
|
||||
TryCapture(as: rangeRef) {
|
||||
OneOrMore(.digit)
|
||||
} transform: { match in
|
||||
|
|
@ -828,7 +826,7 @@ func textMessageAppPacket(
|
|||
}
|
||||
}
|
||||
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false
|
||||
|
||||
|
||||
if !wantRangeTestPackets && rangeTest {
|
||||
return
|
||||
}
|
||||
|
|
@ -841,15 +839,16 @@ func textMessageAppPacket(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if messageText?.count ?? 0 > 0 {
|
||||
|
||||
MeshLogger.log("💬 \("mesh.log.textmessage.received".localized)")
|
||||
|
||||
|
||||
let messageUsers = UserEntity.fetchRequest()
|
||||
messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from])
|
||||
|
||||
do {
|
||||
let fetchedUsers = try context.fetch(messageUsers)
|
||||
|
||||
let newMessage = MessageEntity(context: context)
|
||||
newMessage.messageId = Int64(packet.id)
|
||||
if packet.rxTime > 0 {
|
||||
|
|
@ -865,54 +864,60 @@ func textMessageAppPacket(
|
|||
newMessage.portNum = Int32(packet.decoded.portnum.rawValue)
|
||||
newMessage.publicKey = packet.publicKey
|
||||
newMessage.pkiEncrypted = packet.pkiEncrypted
|
||||
|
||||
if packet.decoded.portnum == PortNum.detectionSensorApp {
|
||||
if !UserDefaults.enableDetectionNotifications {
|
||||
newMessage.read = true
|
||||
}
|
||||
}
|
||||
|
||||
if packet.decoded.replyID > 0 {
|
||||
newMessage.replyID = Int64(packet.decoded.replyID)
|
||||
}
|
||||
|
||||
|
||||
if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum {
|
||||
if !storeForwardBroadcast {
|
||||
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 })
|
||||
|
||||
if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) {
|
||||
/// We have a key, check if it matches
|
||||
// We have a key, check if it matches
|
||||
if newMessage.fromUser?.publicKey != newMessage.publicKey {
|
||||
newMessage.fromUser?.keyMatch = false
|
||||
newMessage.fromUser?.newPublicKey = newMessage.publicKey
|
||||
}
|
||||
} else {
|
||||
/// We have no key, set it
|
||||
// We have no key, set it
|
||||
newMessage.fromUser?.publicKey = packet.publicKey
|
||||
newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted
|
||||
}
|
||||
|
||||
if packet.rxTime > 0 {
|
||||
newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
|
||||
} else {
|
||||
newMessage.fromUser?.userNode?.lastHeard = Date()
|
||||
}
|
||||
}
|
||||
|
||||
newMessage.messagePayload = messageText
|
||||
newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!)
|
||||
|
||||
if packet.to != Constants.maximumNodeNum && newMessage.fromUser != nil {
|
||||
newMessage.fromUser?.lastMessage = Date()
|
||||
}
|
||||
|
||||
var messageSaved = false
|
||||
|
||||
|
||||
do {
|
||||
|
||||
try context.save()
|
||||
Logger.data.info("💾 Saved a new message for \(newMessage.messageId)")
|
||||
messageSaved = true
|
||||
|
||||
if messageSaved {
|
||||
|
||||
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
|
||||
return
|
||||
}
|
||||
|
|
@ -931,14 +936,16 @@ func textMessageAppPacket(
|
|||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)"
|
||||
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)",
|
||||
messageId: newMessage.messageId,
|
||||
channel: newMessage.channel,
|
||||
userNum: Int64(packet.from)
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
|
||||
}
|
||||
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
|
||||
|
||||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||||
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode))
|
||||
|
||||
|
|
@ -961,7 +968,11 @@ func textMessageAppPacket(
|
|||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)")
|
||||
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.messageId)",
|
||||
messageId: newMessage.messageId,
|
||||
channel: newMessage.channel,
|
||||
userNum: Int64(newMessage.fromUser?.userId ?? "0")
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)")
|
||||
|
|
@ -969,7 +980,7 @@ func textMessageAppPacket(
|
|||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -984,6 +995,7 @@ func textMessageAppPacket(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from))
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import SwiftUI
|
|||
import OSLog
|
||||
|
||||
class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
|
||||
|
||||
var router: Router?
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
Logger.services.info("🚀 [App] Meshtstic Apple App launched!")
|
||||
// Default User Default Values
|
||||
|
|
@ -22,7 +22,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
let locationsHandler = LocationsHandler.shared
|
||||
locationsHandler.startLocationUpdates()
|
||||
|
||||
|
||||
// If a background activity session was previously active, reinstantiate it after the background launch.
|
||||
if locationsHandler.backgroundActivity {
|
||||
locationsHandler.backgroundActivity = true
|
||||
|
|
@ -38,7 +38,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
) {
|
||||
completionHandler([.list, .banner, .sound])
|
||||
}
|
||||
|
||||
|
||||
// This method is called when a user clicks on the notification
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
|
|
@ -46,6 +46,64 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case UNNotificationDefaultActionIdentifier:
|
||||
break
|
||||
|
||||
case "messageNotification.thumbsUpAction":
|
||||
if let channel = userInfo["channel"] as? Int32,
|
||||
let replyID = userInfo["messageId"] as? Int64 {
|
||||
let tapbackResponse = !BLEManager.shared.sendMessage(
|
||||
message: Tapbacks.thumbsUp.emojiString,
|
||||
toUserNum: userInfo["userNum"] as? Int64 ?? 0,
|
||||
channel: channel,
|
||||
isEmoji: true,
|
||||
replyID: replyID
|
||||
)
|
||||
Logger.services.info("Tapback response sent")
|
||||
} else {
|
||||
Logger.services.error("Failed to retrieve channel or messageId from userInfo")
|
||||
}
|
||||
break
|
||||
|
||||
case "messageNotification.thumbsDownAction":
|
||||
if let channel = userInfo["channel"] as? Int32,
|
||||
let replyID = userInfo["messageId"] as? Int64 {
|
||||
let tapbackResponse = !BLEManager.shared.sendMessage(
|
||||
message: Tapbacks.thumbsDown.emojiString,
|
||||
toUserNum: userInfo["userNum"] as? Int64 ?? 0,
|
||||
channel: channel,
|
||||
isEmoji: true,
|
||||
replyID: replyID
|
||||
)
|
||||
Logger.services.info("Tapback response sent")
|
||||
} else {
|
||||
Logger.services.error("Failed to retrieve channel or messageId from userInfo")
|
||||
}
|
||||
break
|
||||
|
||||
case "messageNotification.replyInputAction":
|
||||
if let userInput = (response as? UNTextInputNotificationResponse)?.userText,
|
||||
let channel = userInfo["channel"] as? Int32,
|
||||
let replyID = userInfo["messageId"] as? Int64 {
|
||||
let tapbackResponse = !BLEManager.shared.sendMessage(
|
||||
message: userInput,
|
||||
toUserNum: userInfo["userNum"] as? Int64 ?? 0,
|
||||
channel: channel,
|
||||
isEmoji: false,
|
||||
replyID: replyID
|
||||
)
|
||||
Logger.services.info("Actionable notification reply sent")
|
||||
} else {
|
||||
Logger.services.error("Failed to retrieve user input, channel, or messageId from userInfo")
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if let targetValue = userInfo["target"] as? String,
|
||||
let deepLink = userInfo["path"] as? String,
|
||||
let url = URL(string: deepLink) {
|
||||
|
|
@ -54,7 +112,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
|
|||
} else {
|
||||
Logger.services.error("Failed to handle notification response: \(userInfo)")
|
||||
}
|
||||
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue