Works, though it only delivers the message if the phone is not dozing
This commit is contained in:
parent
a8236367c3
commit
fd87316f4e
5 changed files with 82 additions and 47 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue