Merge pull request #925 from meshtastic/actionable-notifications

Actionable notifications
This commit is contained in:
Garth Vander Houwen 2024-09-11 19:16:05 -07:00 committed by GitHub
commit 568163ce69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 130 additions and 28 deletions

View file

@ -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"
}
}
],

View file

@ -3302,7 +3302,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 {
@ -3312,9 +3312,9 @@ extension BLEManager: CBCentralManagerDelegate {
} else {
isSwitchedOn = false
}
var status = ""
switch central.state {
case .poweredOff:
status = "BLE is powered off"
@ -3333,10 +3333,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)")
@ -3344,7 +3344,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 {

View file

@ -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?
}

View file

@ -816,12 +816,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
@ -829,7 +827,7 @@ func textMessageAppPacket(
}
}
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false
if !wantRangeTestPackets && rangeTest {
return
}
@ -842,15 +840,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 {
@ -873,19 +872,22 @@ func textMessageAppPacket(
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
@ -897,27 +899,29 @@ func textMessageAppPacket(
newMessage.fromUser?.publicKey = packet.publicKey
}
}
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
}
@ -936,14 +940,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))
@ -966,7 +972,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)")
@ -974,7 +984,7 @@ func textMessageAppPacket(
}
}
} catch {
// Handle error
}
}
}
@ -989,6 +999,7 @@ func textMessageAppPacket(
}
}
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from))

View file

@ -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()
}
}