Works, though it only delivers the message if the phone is not dozing

This commit is contained in:
Philipp Heckel 2022-05-26 12:14:03 -04:00
parent a8236367c3
commit fd87316f4e
5 changed files with 82 additions and 47 deletions

View file

@ -49,25 +49,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
return
}
// Poll and display new messages
// Poll and show new messages as notifications
let store = Store.shared
let center = UNUserNotificationCenter.current()
let subscriptionManager = SubscriptionManager(store: store)
store.getSubscriptions()?.forEach { subscription in
subscriptionManager.poll(subscription) { messages in
messages.forEach { message in
let content = UNMutableNotificationContent()
content.title = message.title ?? ""
content.body = message.message ?? ""
content.sound = .default
let request = UNNotificationRequest(identifier: message.id, content: content, trigger: nil /* now */)
center.add(request) { (error) in
if let error = error {
Log.e(self.tag, "Unable to create notification", error)
}
}
self.showNotification(subscription, message)
}
}
}
@ -83,6 +71,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
Log.e(tag, "Failed to register for remote notifications", error)
}
/// Create a local notification manually (as opposed to a remote notification being generated by Firebase). We need to make the
/// local notification look exactly like the remote one (same userInfo), so that when we tap it, the userNotificationCenter(didReceive) function
/// has the same information available.
func showNotification(_ subscription: Subscription, _ message: Message) {
var userInfo = message.toUserInfo()
userInfo["base_url"] = subscription.baseUrl
userInfo["topic"] = subscription.topic
let content = UNMutableNotificationContent()
content.title = message.title ?? ""
content.body = message.message ?? "" // FIXME: This needs to be truncated!
content.sound = .default
content.userInfo = userInfo
let request = UNNotificationRequest(identifier: message.id, content: content, trigger: nil /* now */)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
Log.e(self.tag, "Unable to create notification", error)
}
}
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
@ -108,21 +118,22 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
Log.d(tag, "Notification received via userNotificationCenter(didReceive)", userInfo)
let clickUrl = URL(string: userInfo["click"] as? String ?? "")
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))
// Show current topic
if topic != "" {
selectedBaseUrl = topicUrl(baseUrl: Config.appBaseUrl, topic: topic)
selectedBaseUrl = topicUrl(baseUrl: baseUrl, topic: topic)
}
// Execute user action or click action (if any)
if let action = action {
handleAction(action)
handle(action: action)
} else if let clickUrl = clickUrl {
handleCustomClick(clickUrl)
open(url: clickUrl)
}
completionHandler()
@ -133,12 +144,12 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
return actions.first { $0.id == id }
}
private func handleAction(_ action: Action) {
private func handle(action: Action) {
Log.d(tag, "Executing user action", action)
switch action.action {
case "view":
if let url = URL(string: action.url ?? "") {
openUrl(url)
open(url: url)
} else {
Log.w(tag, "Unable to parse action URL", action)
}
@ -149,16 +160,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
}
}
private func handleCustomClick(_ url: URL) {
openUrl(url)
}
private func handleDefaultClick(topic: String) {
Log.d(tag, "Selecting topic \(topic)")
selectedBaseUrl = topicUrl(baseUrl: Config.appBaseUrl, topic: topic)
}
private func openUrl(_ url: URL) {
private func open(url: URL) {
Log.d(tag, "Opening URL \(url)")
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}

View file

@ -64,14 +64,36 @@ extension Notification {
struct Message: Decodable {
var id: String
var time: Int64
var event: String
var message: String?
var title: String?
var priority: Int16?
var tags: [String]?
var actions: [Action]?
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,
"time": String(time),
"message": message ?? "",
"title": title ?? "",
"priority": String(priority ?? 3),
"tags": tags?.joined(separator: ",") ?? "",
"actions": actionsStr ?? ""
]
}
}
struct Action: Decodable {
struct Action: Encodable, Decodable {
var id: String
var action: String
var label: String

View file

@ -81,14 +81,15 @@ class Store: ObservableObject {
func save(notificationFromUserInfo userInfo: [AnyHashable: Any]) {
guard let id = userInfo["id"] as? String,
let topic = userInfo["topic"] 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 = Config.appBaseUrl // Firebase messages all come from the main ntfy server
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
@ -99,10 +100,12 @@ class Store: ObservableObject {
let m = Message(
id: id,
time: timeInt,
event: event,
message: message,
title: title,
priority: priority,
tags: tags
tags: tags,
actions: nil // TODO: Actions
)
save(notificationFromMessage: m, withSubscription: subscription)
}
@ -116,6 +119,7 @@ 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
subscription.addToNotifications(notification)
subscription.lastNotificationId = message.id
try context.save()
@ -181,9 +185,10 @@ class Store: ObservableObject {
extension Store {
static let sampleData = [
"stats": [
Message(id: "1", time: 1653048956, message: "In the last 24 hours, hyou had 5,000 users across 13 countries visit your website", title: "Record visitor numbers", priority: 4, tags: ["smile", "server123", "de"]),
Message(id: "2", time: 1653058956, message: "201 users/h\n80 IPs", title: "This is a title", priority: 1, tags: []),
Message(id: "3", time: 1643058956, message: "This message does not have a title, but is instead super long. Like really really long. It can't be any longer I think. I mean, there is s 4,000 byte limit of the message, so I guess I have to make this 4,000 bytes long. Or do I? 😁 I don't know. It's quite tedious to come up with something so long, so I'll stop now. Bye!", title: nil, priority: 5, tags: ["facepalm"])
// TODO: Message with action
Message(id: "1", time: 1653048956, event: "message", message: "In the last 24 hours, hyou had 5,000 users across 13 countries visit your website", title: "Record visitor numbers", priority: 4, tags: ["smile", "server123", "de"], actions: nil),
Message(id: "2", time: 1653058956, event: "message", message: "201 users/h\n80 IPs", title: "This is a title", priority: 1, tags: [], actions: nil),
Message(id: "3", time: 1643058956, event: "message", message: "This message does not have a title, but is instead super long. Like really really long. It can't be any longer I think. I mean, there is s 4,000 byte limit of the message, so I guess I have to make this 4,000 bytes long. Or do I? 😁 I don't know. It's quite tedious to come up with something so long, so I'll stop now. Bye!", title: nil, priority: 5, tags: ["facepalm"], actions: nil)
],
"backups": [],
"announcements": [],

View file

@ -26,7 +26,10 @@ struct SubscriptionAddView: View {
.disableAutocapitalization()
.disableAutocorrection(true)
}
Section {
Section(
footer:
(useAnother) ? Text("Support for self-hosted servers is currently very limited. Delivery of messages is significantly delayed and not guaranteed. This is actively being developed.") : Text("")
) {
Toggle("Use another server", isOn: $useAnother)
if useAnother {
TextField("Server URL, e.g. https://ntfy.example.com", text: $baseUrl)
@ -48,32 +51,30 @@ struct SubscriptionAddView: View {
Button(action: subscribeAction) {
Text("Subscribe")
}
.disabled(!isValid(topic: topic))
.disabled(!isValid())
}
}
}
}
private func sanitize(topic: String) -> String {
return topic.trimmingCharacters(in: [" "])
private var sanitizedTopic: String {
return topic.trimmingCharacters(in: .whitespaces)
}
private func isValid(topic: String) -> Bool {
let sanitizedTopic = sanitize(topic: topic)
private func isValid() -> Bool {
if sanitizedTopic.isEmpty {
return false
} else if sanitizedTopic.range(of: "^[-_A-Za-z0-9]{1,64}$", options: .regularExpression, range: nil, locale: nil) == nil {
return false
} else if store.getSubscription(baseUrl: Config.appBaseUrl, topic: topic) != nil {
} else if store.getSubscription(baseUrl: selectedBaseUrl, topic: topic) != nil {
return false
}
return true
}
private func subscribeAction() {
let baseUrl = (useAnother) ? baseUrl : Config.appBaseUrl
DispatchQueue.global(qos: .background).async {
subscriptionManager.subscribe(baseUrl: baseUrl, topic: sanitize(topic: topic))
subscriptionManager.subscribe(baseUrl: selectedBaseUrl, topic: sanitizedTopic)
}
isShowing = false
}
@ -81,6 +82,10 @@ struct SubscriptionAddView: View {
private func cancelAction() {
isShowing = false
}
private var selectedBaseUrl: String {
return (useAnother) ? baseUrl : Config.appBaseUrl
}
}

View file

@ -24,6 +24,7 @@ class NotificationService: UNNotificationServiceExtension {
// 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"
@ -39,7 +40,7 @@ class NotificationService: UNNotificationServiceExtension {
// 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 == "" {
bestAttemptContent.title = topicShortUrl(baseUrl: Config.appBaseUrl, topic: topic)
bestAttemptContent.title = topicShortUrl(baseUrl: baseUrl, topic: topic)
}
// Emojify title or message