Skip to content

Commit

Permalink
Merge pull request #916 from RCGV1/Actionable-notifications
Browse files Browse the repository at this point in the history
Actionable Notifications
  • Loading branch information
garthvh authored Sep 11, 2024
2 parents 7896dce + 762c907 commit 8726f41
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 28 deletions.
4 changes: 2 additions & 2 deletions Meshtastic.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
],
Expand Down
12 changes: 6 additions & 6 deletions Meshtastic/Helpers/BLEManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -3312,9 +3312,9 @@ extension BLEManager: CBCentralManagerDelegate {
} else {
isSwitchedOn = false
}

var status = ""

switch central.state {
case .poweredOff:
status = "BLE is powered off"
Expand All @@ -3333,18 +3333,18 @@ 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)")
}
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 {
Expand Down
33 changes: 33 additions & 0 deletions Meshtastic/Helpers/LocalNotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -76,4 +106,7 @@ struct Notification {
var content: String
var target: String?
var path: String?
var messageId: Int64?
var channel: Int32?
var userNum: Int64?
}
41 changes: 26 additions & 15 deletions Meshtastic/Helpers/MeshPackets.swift
Original file line number Diff line number Diff line change
Expand Up @@ -816,20 +816,18 @@ 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
Int(match)
}
}
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false

if !wantRangeTestPackets && rangeTest {
return
}
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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))

Expand All @@ -966,15 +972,19 @@ 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)")
}
}
}
} catch {

// Handle error
}
}
}
Expand All @@ -989,6 +999,7 @@ func textMessageAppPacket(
}
}


func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {

let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from))
Expand Down
68 changes: 63 additions & 5 deletions Meshtastic/MeshtasticAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -38,14 +38,72 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
) {
completionHandler([.list, .banner, .sound])
}

// This method is called when a user clicks on the notification
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
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) {
Expand All @@ -54,7 +112,7 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
} else {
Logger.services.error("Failed to handle notification response: \(userInfo)")
}

completionHandler()
}
}

0 comments on commit 8726f41

Please sign in to comment.