Add push notifications

This commit is contained in:
Christoph Hagen
2025-02-18 22:05:19 +01:00
parent de6f474aea
commit 66a40e52a0
9 changed files with 285 additions and 4 deletions

View File

@ -41,6 +41,9 @@ struct MainView: App {
@StateObject
private var upload: RemotePush = .init()
@StateObject
private var notifications: NotificationSender = .init()
@State
private var language: ContentLanguage = .english
@ -68,6 +71,9 @@ struct MainView: App {
@State
private var showUploadSheet = false
@State
private var showNotificationsSheet = false
@ViewBuilder
var sidebar: some View {
switch selection.tab {
@ -188,6 +194,11 @@ struct MainView: App {
Image(systemSymbol: .squareAndArrowUp)
}
}
ToolbarItem {
Button(action: { showNotificationsSheet = true }) {
Image(systemSymbol: .bell)
}
}
ToolbarItem {
Button(action: saveButtonPressed) {
Image(systemSymbol: content.saveState.symbol)
@ -235,6 +246,11 @@ struct MainView: App {
.environmentObject(content)
.environmentObject(upload)
}
.sheet(isPresented: $showNotificationsSheet) {
NotificationSheet()
.environmentObject(content)
.environmentObject(notifications)
}
}
}

View File

@ -1,7 +1,11 @@
import Foundation
import SwiftUI
final class GeneralSettings: ObservableObject {
@AppStorage("pushNotificationAccessToken")
var pushNotificationAccessToken: String?
@Published
var url: String
@ -20,13 +24,17 @@ final class GeneralSettings: ObservableObject {
@Published
var remotePathForUpload: String
init(url: String, linkPreviewImageWidth: Int, linkPreviewImageHeight: Int, remoteUserForUpload: String, remotePortForUpload: Int, remotePathForUpload: String) {
@Published
var urlForPushNotification: String?
init(url: String, linkPreviewImageWidth: Int, linkPreviewImageHeight: Int, remoteUserForUpload: String, remotePortForUpload: Int, remotePathForUpload: String, urlForPushNotification: String?) {
self.url = url
self.linkPreviewImageWidth = linkPreviewImageWidth
self.linkPreviewImageHeight = linkPreviewImageHeight
self.remoteUserForUpload = remoteUserForUpload
self.remotePortForUpload = remotePortForUpload
self.remotePathForUpload = remotePathForUpload
self.urlForPushNotification = urlForPushNotification
}
}
@ -39,7 +47,8 @@ extension GeneralSettings {
linkPreviewImageHeight: data.linkPreviewImageHeight,
remoteUserForUpload: data.remoteUserForUpload,
remotePortForUpload: data.remotePortForUpload,
remotePathForUpload: data.remotePathForUpload)
remotePathForUpload: data.remotePathForUpload,
urlForPushNotification: data.urlForPushNotification)
}
var data: Data {
@ -49,7 +58,8 @@ extension GeneralSettings {
linkPreviewImageHeight: linkPreviewImageHeight,
remoteUserForUpload: remoteUserForUpload,
remotePortForUpload: remotePortForUpload,
remotePathForUpload: remotePathForUpload)
remotePathForUpload: remotePathForUpload,
urlForPushNotification: urlForPushNotification)
}
struct Data: Codable, Equatable {
@ -59,5 +69,6 @@ extension GeneralSettings {
let remoteUserForUpload: String
let remotePortForUpload: Int
let remotePathForUpload: String
let urlForPushNotification: String?
}
}

View File

@ -111,7 +111,8 @@ extension GeneralSettings {
linkPreviewImageHeight: 630,
remoteUserForUpload: "user",
remotePortForUpload: 22,
remotePathForUpload: "/home/user/web")
remotePathForUpload: "/home/user/web",
urlForPushNotification: nil)
}
extension AudioPlayerSettings {

View File

@ -0,0 +1,21 @@
struct NotificationRequest {
let accessToken: String
/// The notificiations in the available languages
let notifications: [String : WebNotification]
init(accessToken: String, notifications: [String : WebNotification]) {
self.accessToken = accessToken
self.notifications = notifications
}
func notification(for language: String) -> WebNotification? {
notifications[language] ?? notifications["en"]
}
}
extension NotificationRequest: Codable {
}

View File

@ -0,0 +1,73 @@
import Foundation
final class NotificationSender: ObservableObject {
@Published
var isTransmittingToRemote = false
@Published
var lastPushWasSuccessful = true
init() {
}
func failedToCreateNotificationDueToMissingSettings() {
guard !isTransmittingToRemote else { return }
DispatchQueue.main.async {
self.lastPushWasSuccessful = false
}
}
func send(url: String, request: NotificationRequest) {
guard !isTransmittingToRemote else { return }
DispatchQueue.main.async {
self.isTransmittingToRemote = true
}
guard let url = URL(string: url) else {
print("Invalid notification url: \(url)")
didFinish(success: false)
return
}
let body: Data
do {
body = try JSONEncoder().encode(request)
} catch {
print("Failed to encode notification: \(error)")
didFinish(success: false)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = body
Task {
do {
let (_ , result) = try await URLSession.shared.data(for: request)
guard let response = result as? HTTPURLResponse else {
print("Invalid response \(result)")
didFinish(success: false)
return
}
guard response.statusCode == 200 else {
print("Failed to send notification: \(response.statusCode)")
didFinish(success: false)
return
}
didFinish(success: true)
} catch {
print("Failed to send notification request: \(error)")
didFinish(success: false)
return
}
}
}
private func didFinish(success: Bool) {
DispatchQueue.main.async {
self.isTransmittingToRemote = false
self.lastPushWasSuccessful = success
}
}
}

View File

@ -0,0 +1,105 @@
import SwiftUI
import SFSafeSymbols
struct NotificationSheet: View {
@EnvironmentObject
private var content: Content
@EnvironmentObject
private var notifications: NotificationSender
@Environment(\.dismiss)
private var dismiss
@State
private var englishTitle = ""
@State
private var englishMessage = ""
@State
private var germanTitle = ""
@State
private var germanMessage = ""
private var uploadSymbol: SFSymbol {
if notifications.isTransmittingToRemote {
return .squareAndArrowUpBadgeClock
}
if !notifications.lastPushWasSuccessful {
return .squareAndArrowUpTrianglebadgeExclamationmark
}
return .squareAndArrowUp
}
var header: String {
let user = content.settings.general.remoteUserForUpload
let port = content.settings.general.remotePortForUpload
let url = content.settings.general.remotePathForUpload.withHttpPrefixRemoved
let path = content.settings.general.remotePathForUpload
return "\(user)@\(url):\(port)/\(path)"
}
var isIncomplete: Bool {
englishTitle.isEmpty || germanTitle.isEmpty || englishMessage.isEmpty || germanMessage.isEmpty
}
var body: some View {
VStack {
HStack {
Text("Send a notification to subscribers")
.font(.headline)
Spacer()
Button("Close", action: { dismiss() })
}
StringPropertyView(
title: "English title",
text: $englishTitle,
footer: "The title for devices using the english language")
StringPropertyView(
title: "English message",
text: $englishMessage,
footer: "The message for devices using the english language")
StringPropertyView(
title: "German title",
text: $germanTitle,
footer: "The title for devices using the german language")
StringPropertyView(
title: "German message",
text: $germanMessage,
footer: "The message for devices using the german language")
Button("Send", action: send)
.disabled(notifications.isTransmittingToRemote || isIncomplete)
}
.padding()
.frame(minWidth: 300, idealWidth: 400, idealHeight: 500)
}
private func send() {
guard let accessToken = content.settings.general.pushNotificationAccessToken,
let url = content.settings.general.urlForPushNotification else {
return
}
let english = WebNotification(title: englishTitle, body: englishMessage)
let german = WebNotification(title: germanTitle, body: germanMessage)
let notifications: [String : WebNotification] = [
"en": english,
"de": german
]
let request = NotificationRequest.init(
accessToken: accessToken,
notifications: notifications)
self.notifications.send(url: url, request: request)
}
}

View File

@ -0,0 +1,19 @@
struct WebNotification {
/// The title of the notification
/// - Note: Not supported on Safari for iOS
let title: String
/// The main content/message
let body: String
init(title: String, body: String) {
self.title = title
self.body = body
}
}
extension WebNotification: Codable {
}

View File

@ -37,6 +37,16 @@ struct GeneralSettingsDetailView: View {
title: "Upload Folder",
text: $generalSettings.remotePathForUpload,
footer: "The path to the folder on the server where the files should be uploaded to")
OptionalStringPropertyView(
title: "Push Notification URL",
text: $generalSettings.urlForPushNotification,
footer: "The url to send push notifications to")
OptionalStringPropertyView(
title: "Push Notification Access Token",
text: $generalSettings.pushNotificationAccessToken,
footer: "The access token to use for sending push notifications")
}
.padding()
}