Harmonize Message and userInfo stuff
This commit is contained in:
parent
fd87316f4e
commit
379ed1bed1
7 changed files with 88 additions and 75 deletions
|
|
@ -3,6 +3,7 @@ import SafariServices
|
|||
import UserNotifications
|
||||
import Firebase
|
||||
import FirebaseCore
|
||||
import FirebaseMessaging
|
||||
import CoreData
|
||||
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
|
||||
|
|
@ -86,6 +87,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
|
|||
content.sound = .default
|
||||
content.userInfo = userInfo
|
||||
|
||||
// FIXME: Use logic in NotificationService here to build the same message
|
||||
|
||||
let request = UNNotificationRequest(identifier: message.id, content: content, trigger: nil /* now */)
|
||||
UNUserNotificationCenter.current().add(request) { (error) in
|
||||
if let error = error {
|
||||
|
|
@ -114,15 +117,16 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
let actionId = response.actionIdentifier
|
||||
|
||||
Log.d(tag, "Notification received via userNotificationCenter(didReceive)", userInfo)
|
||||
guard let message = Message.from(userInfo: userInfo) else {
|
||||
Log.w(tag, "Cannot convert userInfo to message", userInfo)
|
||||
completionHandler()
|
||||
return
|
||||
}
|
||||
|
||||
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
|
||||
let topic = userInfo["topic"] as? String ?? ""
|
||||
let clickUrl = URL(string: userInfo["click"] as? String ?? "")
|
||||
let actions = userInfo["actions"] as? String ?? "[]"
|
||||
let action = findAction(id: actionId, actions: Actions.shared.parse(actions))
|
||||
let action = message.actions?.first { $0.id == response.actionIdentifier }
|
||||
|
||||
// Show current topic
|
||||
if topic != "" {
|
||||
|
|
@ -132,18 +136,13 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||
// Execute user action or click action (if any)
|
||||
if let action = action {
|
||||
handle(action: action)
|
||||
} else if let clickUrl = clickUrl {
|
||||
open(url: clickUrl)
|
||||
} else if let click = message.click, let url = URL(string: click) {
|
||||
open(url: url)
|
||||
}
|
||||
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
private func findAction(id: String, actions: [Action]?) -> Action? {
|
||||
guard let actions = actions else { return nil }
|
||||
return actions.first { $0.id == id }
|
||||
}
|
||||
|
||||
private func handle(action: Action) {
|
||||
Log.d(tag, "Executing user action", action)
|
||||
switch action.action {
|
||||
|
|
|
|||
|
|
@ -70,16 +70,12 @@ struct Message: Decodable {
|
|||
var priority: Int16?
|
||||
var tags: [String]?
|
||||
var actions: [Action]?
|
||||
var click: String?
|
||||
|
||||
func toUserInfo() -> [AnyHashable: Any] {
|
||||
// This should mimic the way that the ntfy server encodes a message.
|
||||
// See server_firebase.go for more details.
|
||||
|
||||
var actionsStr: String?
|
||||
if let actionsData = try? JSONEncoder().encode(actions) {
|
||||
actionsStr = String(data: actionsData, encoding: .utf8)
|
||||
}
|
||||
|
||||
return [
|
||||
"id": id,
|
||||
"event": event,
|
||||
|
|
@ -88,9 +84,37 @@ struct Message: Decodable {
|
|||
"title": title ?? "",
|
||||
"priority": String(priority ?? 3),
|
||||
"tags": tags?.joined(separator: ",") ?? "",
|
||||
"actions": actionsStr ?? ""
|
||||
"actions": Actions.shared.encode(actions),
|
||||
"click": click ?? ""
|
||||
]
|
||||
}
|
||||
|
||||
static func from(userInfo: [AnyHashable: Any]) -> Message? {
|
||||
guard let id = userInfo["id"] as? String,
|
||||
let time = userInfo["time"] as? String,
|
||||
let event = userInfo["event"] as? String,
|
||||
let timeInt = Int64(time),
|
||||
let message = userInfo["message"] as? String else {
|
||||
Log.d(Store.tag, "Unknown or irrelevant message", userInfo)
|
||||
return nil
|
||||
}
|
||||
let title = userInfo["title"] as? String
|
||||
let priority = Int16(userInfo["priority"] as? String ?? "3") ?? 3
|
||||
let tags = (userInfo["tags"] as? String ?? "").components(separatedBy: ",")
|
||||
let actions = userInfo["actions"] as? String
|
||||
let click = userInfo["click"] as? String
|
||||
return Message(
|
||||
id: id,
|
||||
time: timeInt,
|
||||
event: event,
|
||||
message: message,
|
||||
title: title,
|
||||
priority: priority,
|
||||
tags: tags,
|
||||
actions: Actions.shared.parse(actions),
|
||||
click: click
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Action: Encodable, Decodable {
|
||||
|
|
|
|||
|
|
@ -79,37 +79,6 @@ class Store: ObservableObject {
|
|||
try? context.save()
|
||||
}
|
||||
|
||||
func save(notificationFromUserInfo userInfo: [AnyHashable: Any]) {
|
||||
guard let id = userInfo["id"] as? String,
|
||||
let time = userInfo["time"] as? String,
|
||||
let event = userInfo["event"] as? String,
|
||||
let topic = userInfo["topic"] as? String,
|
||||
let timeInt = Int64(time),
|
||||
let message = userInfo["message"] as? String else {
|
||||
Log.d(Store.tag, "Unknown or irrelevant message", userInfo)
|
||||
return
|
||||
}
|
||||
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl // Firebase messages all come from the main ntfy server
|
||||
guard let subscription = getSubscription(baseUrl: baseUrl, topic: topic) else {
|
||||
Log.d(Store.tag, "Subscription for topic \(topic) unknown")
|
||||
return
|
||||
}
|
||||
let title = userInfo["title"] as? String ?? ""
|
||||
let priority = Int16(userInfo["priority"] as? String ?? "3") ?? 3
|
||||
let tags = (userInfo["tags"] as? String ?? "").components(separatedBy: ",")
|
||||
let m = Message(
|
||||
id: id,
|
||||
time: timeInt,
|
||||
event: event,
|
||||
message: message,
|
||||
title: title,
|
||||
priority: priority,
|
||||
tags: tags,
|
||||
actions: nil // TODO: Actions
|
||||
)
|
||||
save(notificationFromMessage: m, withSubscription: subscription)
|
||||
}
|
||||
|
||||
func save(notificationFromMessage message: Message, withSubscription subscription: Subscription) {
|
||||
do {
|
||||
let notification = Notification(context: context)
|
||||
|
|
@ -119,7 +88,8 @@ class Store: ObservableObject {
|
|||
notification.title = message.title ?? ""
|
||||
notification.priority = (message.priority != nil && message.priority != 0) ? message.priority! : 3
|
||||
notification.tags = message.tags?.joined(separator: ",") ?? ""
|
||||
// TODO: actions
|
||||
notification.actions = Actions.shared.encode(message.actions)
|
||||
notification.click = message.click ?? ""
|
||||
subscription.addToNotifications(notification)
|
||||
subscription.lastNotificationId = message.id
|
||||
try context.save()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21E258" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Notification" representedClassName="Notification" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="actions" optional="YES" attributeType="String"/>
|
||||
<attribute name="click" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="message" attributeType="String"/>
|
||||
<attribute name="priority" optional="YES" attributeType="Integer 16" minValueString="1" maxValueString="5" defaultValueString="3" usesScalarValueType="YES"/>
|
||||
|
|
@ -27,7 +29,7 @@
|
|||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Notification" positionX="-54" positionY="9" width="128" height="134"/>
|
||||
<element name="Notification" positionX="-54" positionY="9" width="128" height="164"/>
|
||||
<element name="Subscription" positionX="-262.4760131835938" positionY="11.46405029296875" width="128" height="89"/>
|
||||
</elements>
|
||||
</model>
|
||||
|
|
@ -17,6 +17,14 @@ struct Actions {
|
|||
}
|
||||
}
|
||||
|
||||
func encode(_ actions: [Action]?) -> String {
|
||||
guard let actions = actions else { return "" }
|
||||
if let actionsData = try? JSONEncoder().encode(actions) {
|
||||
return String(data: actionsData, encoding: .utf8) ?? ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func http(_ action: Action) {
|
||||
guard let actionUrl = action.url, let url = URL(string: actionUrl) else {
|
||||
Log.w(tag, "Unable to execute HTTP action, no or invalid URL", action)
|
||||
|
|
|
|||
|
|
@ -12,12 +12,17 @@ func topicShortUrl(baseUrl: String, topic: String) -> String {
|
|||
|
||||
func parseAllTags(_ tags: String?) -> [String] {
|
||||
return (tags?.components(separatedBy: ",") ?? [])
|
||||
.filter { $0.trimmingCharacters(in: [" "]) != "" }
|
||||
.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
|
||||
}
|
||||
|
||||
func parseEmojiTags(_ tags: String?) -> [String] {
|
||||
return parseEmojiTags(parseAllTags(tags))
|
||||
}
|
||||
|
||||
func parseEmojiTags(_ tags: [String]?) -> [String] {
|
||||
guard let tags = tags else { return [] }
|
||||
var emojiTags: [String] = []
|
||||
for tag in parseAllTags(tags) {
|
||||
for tag in tags {
|
||||
if let emoji = EmojiManager.shared.getEmojiByAlias(alias: tag) {
|
||||
emojiTags.append(emoji.getUnicode())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,33 +20,38 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
Log.d(tag, "Notification received (in service)") // Logs from extensions are not printed in Xcode!
|
||||
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
let store = Store.shared
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
|
||||
// Get all the things
|
||||
let event = userInfo["event"] as? String ?? ""
|
||||
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
|
||||
let topic = userInfo["topic"] as? String ?? ""
|
||||
let title = userInfo["title"] as? String
|
||||
let priority = userInfo["priority"] as? String ?? "3"
|
||||
let tags = userInfo["tags"] as? String
|
||||
let actions = userInfo["actions"] as? String ?? "[]"
|
||||
|
||||
// Only handle "message" events
|
||||
if event != "message" {
|
||||
guard let message = Message.from(userInfo: userInfo) else {
|
||||
Log.w(Store.tag, "Message cannot be parsed from userInfo", userInfo)
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
if message.event != "message" {
|
||||
Log.w(tag, "Irrelevant message received", message)
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
// Only handle "message" events
|
||||
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
|
||||
let topic = userInfo["topic"] as? String ?? ""
|
||||
guard let subscription = store.getSubscription(baseUrl: baseUrl, topic: topic) else {
|
||||
Log.w(tag, "Subscription for topic \(topic) unknown")
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
// Set notification title to short URL if there is no title. The title is always set
|
||||
// by the server, but it may be empty.
|
||||
if let title = title, title == "" {
|
||||
if let title = message.title, title == "" {
|
||||
bestAttemptContent.title = topicShortUrl(baseUrl: baseUrl, topic: topic)
|
||||
}
|
||||
|
||||
// Emojify title or message
|
||||
let emojiTags = parseEmojiTags(tags)
|
||||
let emojiTags = parseEmojiTags(message.tags)
|
||||
if !emojiTags.isEmpty {
|
||||
if let title = title, title != "" {
|
||||
if let title = message.title, title != "" {
|
||||
bestAttemptContent.title = emojiTags.joined(separator: "") + " " + bestAttemptContent.title
|
||||
} else {
|
||||
bestAttemptContent.body = emojiTags.joined(separator: "") + " " + bestAttemptContent.body
|
||||
|
|
@ -61,7 +66,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
//
|
||||
// We also must set the .foreground flag, which brings the notification to the foreground and avoids an error about
|
||||
// permissions. This is described in https://stackoverflow.com/a/44580916/1440785
|
||||
if let actions = Actions.shared.parse(actions), !actions.isEmpty {
|
||||
if let actions = message.actions, !actions.isEmpty {
|
||||
bestAttemptContent.categoryIdentifier = actionsCategory
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
|
|
@ -76,17 +81,17 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
|
||||
// Map priorities to interruption level (light up screen, ...) and relevance (order)
|
||||
if #available(iOS 15.0, *) {
|
||||
switch priority {
|
||||
case "1":
|
||||
switch message.priority {
|
||||
case 1:
|
||||
bestAttemptContent.interruptionLevel = .passive
|
||||
bestAttemptContent.relevanceScore = 0
|
||||
case "2":
|
||||
case 2:
|
||||
bestAttemptContent.interruptionLevel = .passive
|
||||
bestAttemptContent.relevanceScore = 0.25
|
||||
case "4":
|
||||
case 4:
|
||||
bestAttemptContent.interruptionLevel = .timeSensitive
|
||||
bestAttemptContent.relevanceScore = 0.75
|
||||
case "5":
|
||||
case 5:
|
||||
bestAttemptContent.interruptionLevel = .critical
|
||||
bestAttemptContent.relevanceScore = 1
|
||||
default:
|
||||
|
|
@ -96,7 +101,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
}
|
||||
|
||||
// Save notification to store, and display it
|
||||
Store.shared.save(notificationFromUserInfo: userInfo)
|
||||
Store.shared.save(notificationFromMessage: message, withSubscription: subscription)
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue