commit
3feb27848a
15 changed files with 430 additions and 100 deletions
|
|
@ -50,6 +50,8 @@
|
|||
94CD196A283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; };
|
||||
94CD196B283E666900973B93 /* EmojiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94CD1969283E666900973B93 /* EmojiManager.swift */; };
|
||||
94E9196C28353E0100F30170 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9474F216283531A200CDE4DD /* Log.swift */; };
|
||||
E27008102AF0F64B006E33BA /* SubscriptionsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */; };
|
||||
E27008122AF1030A006E33BA /* NotificationsObservable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27008112AF1030A006E33BA /* NotificationsObservable.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -111,6 +113,8 @@
|
|||
94B736D6284AF9BE003D69FB /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
||||
94CD1965283E662900973B93 /* emojis.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emojis.json; sourceTree = "<group>"; };
|
||||
94CD1969283E666900973B93 /* EmojiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiManager.swift; sourceTree = "<group>"; };
|
||||
E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsObservable.swift; sourceTree = "<group>"; };
|
||||
E27008112AF1030A006E33BA /* NotificationsObservable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsObservable.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -211,7 +215,9 @@
|
|||
9474F1FE28316ACE00CDE4DD /* Subscription.swift */,
|
||||
9474F1F82830835400CDE4DD /* Store.swift */,
|
||||
9474F20B283321C300CDE4DD /* Notification.swift */,
|
||||
E27008112AF1030A006E33BA /* NotificationsObservable.swift */,
|
||||
94A3F7C7283734D900C48E79 /* SubscriptionManager.swift */,
|
||||
E270080F2AF0F64B006E33BA /* SubscriptionsObservable.swift */,
|
||||
9407EDD9284ADE1F00C1C334 /* User.swift */,
|
||||
);
|
||||
path = Persistence;
|
||||
|
|
@ -361,6 +367,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E27008102AF0F64B006E33BA /* SubscriptionsObservable.swift in Sources */,
|
||||
02024E60283D7CBB0064224A /* Extensions.swift in Sources */,
|
||||
948671472841B0B20093C7A4 /* NotificationContent.swift in Sources */,
|
||||
9474F1F92830835400CDE4DD /* Store.swift in Sources */,
|
||||
|
|
@ -373,6 +380,7 @@
|
|||
9474F1C3282F2AA700CDE4DD /* ContentView.swift in Sources */,
|
||||
9474F20C283321C300CDE4DD /* Notification.swift in Sources */,
|
||||
9474F1F22830825600CDE4DD /* SubscriptionListView.swift in Sources */,
|
||||
E27008122AF1030A006E33BA /* NotificationsObservable.swift in Sources */,
|
||||
9486714A2841D0CE0093C7A4 /* ActionExecutor.swift in Sources */,
|
||||
9474F1FD2831311A00CDE4DD /* SubscriptionAddView.swift in Sources */,
|
||||
9474F1FF28316ACE00CDE4DD /* Subscription.swift in Sources */,
|
||||
|
|
@ -542,7 +550,7 @@
|
|||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"ntfy/Assets/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = YXQ4AMS4B4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -559,7 +567,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2;
|
||||
MARKETING_VERSION = 1.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
@ -576,7 +584,7 @@
|
|||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = ntfy/ntfy.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"ntfy/Assets/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = YXQ4AMS4B4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -593,7 +601,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.2;
|
||||
MARKETING_VERSION = 1.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.heckel.ntfy;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/firebase-ios-sdk",
|
||||
"state" : {
|
||||
"revision" : "cfa854c9c1073c4d1b83b20dfcb1ef7ceb85388b",
|
||||
"version" : "9.0.0"
|
||||
"revision" : "7e80c25b51c2ffa238879b07fbfc5baa54bb3050",
|
||||
"version" : "9.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -32,8 +32,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/google/GoogleAppMeasurement.git",
|
||||
"state" : {
|
||||
"revision" : "6a3123fab90f3884167990bee9bb30097d99c98c",
|
||||
"version" : "9.0.0"
|
||||
"revision" : "c1cfde8067668027b23a42c29d11c246152fe046",
|
||||
"version" : "9.6.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -86,8 +86,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/firebase/nanopb.git",
|
||||
"state" : {
|
||||
"revision" : "7ee9ef9f627d85cbe1b8c4f49a3ed26eed216c77",
|
||||
"version" : "2.30908.0"
|
||||
"revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
|
||||
"version" : "2.30909.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
77
ntfy.xcodeproj/xcshareddata/xcschemes/ntfy.xcscheme
Normal file
77
ntfy.xcodeproj/xcshareddata/xcschemes/ntfy.xcscheme
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
|
||||
BuildableName = "ntfy.app"
|
||||
BlueprintName = "ntfy"
|
||||
ReferencedContainer = "container:ntfy.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
|
||||
BuildableName = "ntfy.app"
|
||||
BlueprintName = "ntfy"
|
||||
ReferencedContainer = "container:ntfy.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
|
||||
BuildableName = "ntfy.app"
|
||||
BlueprintName = "ntfy"
|
||||
ReferencedContainer = "container:ntfy.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
99
ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme
Normal file
99
ntfy.xcodeproj/xcshareddata/xcschemes/ntfyNSE.xcscheme
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
wasCreatedForAppExtension = "YES"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9474F1E3282F3FFD00CDE4DD"
|
||||
BuildableName = "ntfyNSE.appex"
|
||||
BlueprintName = "ntfyNSE"
|
||||
ReferencedContainer = "container:ntfy.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
|
||||
BuildableName = "ntfy.app"
|
||||
BlueprintName = "ntfy"
|
||||
ReferencedContainer = "container:ntfy.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
launchStyle = "0"
|
||||
askForAppToLaunch = "Yes"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<RemoteRunnable
|
||||
runnableDebuggingMode = "0"
|
||||
BundleIdentifier = "io.heckel.ntfy"
|
||||
RemotePath = "/var/containers/Bundle/Application/508E12DE-12D7-4A45-8D44-B351AFC4BF23/ntfy.app">
|
||||
</RemoteRunnable>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
|
||||
BuildableName = "ntfy.app"
|
||||
BlueprintName = "ntfy"
|
||||
ReferencedContainer = "container:ntfy.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
launchAutomaticallySubstyle = "2">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "9474F1BC282F2AA700CDE4DD"
|
||||
BuildableName = "ntfy.app"
|
||||
BlueprintName = "ntfy"
|
||||
ReferencedContainer = "container:ntfy.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -13,11 +13,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
|
|||
// Implements navigation from notifications, see https://stackoverflow.com/a/70731861/1440785
|
||||
@Published var selectedBaseUrl: String? = nil
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
|
||||
Log.d(tag, "Launching AppDelegate")
|
||||
|
||||
|
||||
FirebaseApp.configure()
|
||||
FirebaseConfiguration.shared.setLoggerLevel(.max)
|
||||
|
||||
// Register app permissions for push notifications
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
Messaging.messaging().delegate = self
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
|
||||
guard success else {
|
||||
Log.e(self.tag, "Failed to register for local push notifications", error)
|
||||
|
|
@ -28,13 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
|
|||
|
||||
// Register too receive remote notifications
|
||||
application.registerForRemoteNotifications()
|
||||
|
||||
// Set self as messaging delegate
|
||||
Messaging.messaging().delegate = self
|
||||
|
||||
// Register to "~poll" topic
|
||||
Messaging.messaging().subscribe(toTopic: pollTopic)
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -117,7 +116,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||
|
||||
let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
|
||||
let action = message.actions?.first { $0.id == response.actionIdentifier }
|
||||
|
||||
|
||||
// Show current topic
|
||||
if message.topic != "" {
|
||||
selectedBaseUrl = topicUrl(baseUrl: baseUrl, topic: message.topic)
|
||||
|
|
@ -138,7 +137,20 @@ extension AppDelegate: MessagingDelegate {
|
|||
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
|
||||
Log.d(tag, "Firebase token received: \(String(describing: fcmToken))")
|
||||
|
||||
// We don't actually need the FCM token, since we're just using topics.
|
||||
// We still print it so we can see if things were successful.
|
||||
// Subscribe to ~poll topic
|
||||
Messaging.messaging().subscribe(toTopic: pollTopic)
|
||||
|
||||
// Re-subscribe to Firebase for all topics
|
||||
let store = Store.shared
|
||||
store.getSubscriptions()?.forEach{ subscription in
|
||||
if let baseUrl = subscription.baseUrl, let topic = subscription.topic {
|
||||
Log.d(tag, "Re-subscribing to topic \(baseUrl)/\(topic)")
|
||||
if baseUrl == Config.appBaseUrl {
|
||||
Messaging.messaging().subscribe(toTopic: topic)
|
||||
} else {
|
||||
Messaging.messaging().subscribe(toTopic: topicHash(baseUrl: baseUrl, topic: topic))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import Firebase
|
|||
|
||||
@main
|
||||
struct AppMain: App {
|
||||
private let tag = "main"
|
||||
private let tag = "AppMain"
|
||||
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate: AppDelegate
|
||||
@StateObject private var store = Store.shared
|
||||
|
|
@ -13,11 +13,6 @@ struct AppMain: App {
|
|||
init() {
|
||||
Log.d(tag, "Launching ntfy 🥳. Welcome!")
|
||||
Log.d(tag, "Base URL is \(Config.appBaseUrl), user agent is \(ApiService.userAgent)")
|
||||
|
||||
// We must configure Firebase here, and not in the AppDelegate. For some reason
|
||||
// configuring it there did not work.
|
||||
FirebaseApp.configure()
|
||||
FirebaseConfiguration.shared.setLoggerLevel(.max)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
|
|
|
|||
|
|
@ -150,6 +150,66 @@
|
|||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "48.png",
|
||||
"idiom" : "watch",
|
||||
|
|
@ -225,6 +285,13 @@
|
|||
"size" : "51x51",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "appLauncher",
|
||||
"scale" : "2x",
|
||||
"size" : "54x54",
|
||||
"subtype" : "49mm"
|
||||
},
|
||||
{
|
||||
"filename" : "172.png",
|
||||
"idiom" : "watch",
|
||||
|
|
@ -256,71 +323,18 @@
|
|||
"size" : "117x117",
|
||||
"subtype" : "45mm"
|
||||
},
|
||||
{
|
||||
"idiom" : "watch",
|
||||
"role" : "quickLook",
|
||||
"scale" : "2x",
|
||||
"size" : "129x129",
|
||||
"subtype" : "49mm"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "watch-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "64.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
<dict>
|
||||
<key>AppBaseURL</key>
|
||||
<string>$(APP_BASE_URL)</string>
|
||||
<key>FirebaseAppDelegateProxyEnabled</key>
|
||||
<false/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
|
|
|||
44
ntfy/Persistence/NotificationsObservable.swift
Normal file
44
ntfy/Persistence/NotificationsObservable.swift
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import CoreData
|
||||
import SwiftUI
|
||||
|
||||
class NotificationsObservable: NSObject, ObservableObject {
|
||||
private let tag = "NotificationsObservable"
|
||||
private var subscriptionID: NSManagedObjectID
|
||||
|
||||
private lazy var fetchedResultsController: NSFetchedResultsController<Notification> = {
|
||||
let fetchRequest: NSFetchRequest<Notification> = Notification.fetchRequest()
|
||||
|
||||
// Filter by the desired subscription
|
||||
fetchRequest.predicate = NSPredicate(format: "subscription == %@", subscriptionID)
|
||||
|
||||
// Sort descriptors if you need them
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)] // Assuming you have a 'date' attribute on the NotificationEntity
|
||||
|
||||
let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: Store.shared.context, sectionNameKeyPath: nil, cacheName: nil)
|
||||
controller.delegate = self
|
||||
return controller
|
||||
}()
|
||||
|
||||
@Published var notifications: [Notification] = []
|
||||
|
||||
init(subscriptionID: NSManagedObjectID) {
|
||||
self.subscriptionID = subscriptionID
|
||||
super.init()
|
||||
|
||||
do {
|
||||
Log.d(tag, "Fetching notifications")
|
||||
try self.fetchedResultsController.performFetch()
|
||||
self.notifications = self.fetchedResultsController.fetchedObjects ?? []
|
||||
} catch {
|
||||
Log.w(tag, "Failed to fetch notifications \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NotificationsObservable: NSFetchedResultsControllerDelegate {
|
||||
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
DispatchQueue.main.async {
|
||||
self.notifications = self.fetchedResultsController.fetchedObjects ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,10 @@ class Store: ObservableObject {
|
|||
NotificationCenter.default
|
||||
.publisher(for: .NSPersistentStoreRemoteChange)
|
||||
.sink { value in
|
||||
Log.d(Store.tag, "Remote change detected, refreshing view", value)
|
||||
// TODO: this could probably broadcast the name of the channel
|
||||
// so that only relevant views can update.
|
||||
Log.d(Store.tag, "Remote change detected, refreshing views", value)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.hardRefresh()
|
||||
}
|
||||
|
|
@ -77,6 +80,7 @@ class Store: ObservableObject {
|
|||
subscription.baseUrl = baseUrl
|
||||
subscription.topic = topic
|
||||
DispatchQueue.main.sync {
|
||||
Log.d(Store.tag, "Storing subscription baseUrl=\(baseUrl), topic=\(topic)")
|
||||
try? context.save()
|
||||
}
|
||||
return subscription
|
||||
|
|
@ -114,8 +118,10 @@ class Store: ObservableObject {
|
|||
notification.tags = message.tags?.joined(separator: ",") ?? ""
|
||||
notification.actions = Actions.shared.encode(message.actions)
|
||||
notification.click = message.click ?? ""
|
||||
notification.subscription = subscription
|
||||
subscription.addToNotifications(notification)
|
||||
subscription.lastNotificationId = message.id
|
||||
Log.d(Store.tag, "Storing notification with ID \(notification.id ?? "<unknown>")")
|
||||
try context.save()
|
||||
} catch let error {
|
||||
Log.w(Store.tag, "Cannot store notification (fromMessage)", error)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import FirebaseMessaging
|
|||
/// Manager to combine persisting a subscription to the data store and subscribing to Firebase.
|
||||
/// This is to centralize the logic in one place.
|
||||
struct SubscriptionManager {
|
||||
private let tag = "Store"
|
||||
private let tag = "SubscriptionManager"
|
||||
var store: Store
|
||||
|
||||
func subscribe(baseUrl: String, topic: String) {
|
||||
|
|
@ -37,6 +37,13 @@ struct SubscriptionManager {
|
|||
}
|
||||
|
||||
func poll(_ subscription: Subscription, completionHandler: @escaping ([Message]) -> Void) {
|
||||
// This is a bit of a hack but it prevents us from polling dead subscriptions
|
||||
if (subscription.baseUrl == nil) {
|
||||
Log.d(tag, "Attempting to poll dead subscription failed")
|
||||
completionHandler([])
|
||||
return
|
||||
}
|
||||
|
||||
let user = store.getUser(baseUrl: subscription.baseUrl!)?.toBasicUser()
|
||||
Log.d(tag, "Polling from \(subscription.urlString()) with user \(user?.username ?? "anonymous")")
|
||||
ApiService.shared.poll(subscription: subscription, user: user) { messages, error in
|
||||
|
|
|
|||
60
ntfy/Persistence/SubscriptionsObservable.swift
Normal file
60
ntfy/Persistence/SubscriptionsObservable.swift
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import CoreData
|
||||
import SwiftUI
|
||||
|
||||
class SubscriptionsObservable: NSObject, ObservableObject {
|
||||
private let tag = "SubscriptionsObservable"
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// This will force the initialization of notificationsFetchedResultsController
|
||||
_ = self.notificationsFetchedResultsController
|
||||
}
|
||||
|
||||
private lazy var fetchedResultsController: NSFetchedResultsController<Subscription> = {
|
||||
let fetchRequest: NSFetchRequest<Subscription> = Subscription.fetchRequest()
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "topic", ascending: true)]
|
||||
|
||||
let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: Store.shared.context, sectionNameKeyPath: nil, cacheName: nil)
|
||||
controller.delegate = self
|
||||
|
||||
do {
|
||||
Log.d(tag, "Fetching subscriptions")
|
||||
try controller.performFetch()
|
||||
} catch {
|
||||
Log.w(tag, "Failed to fetch subscriptions: \(error)", error)
|
||||
}
|
||||
|
||||
return controller
|
||||
}()
|
||||
|
||||
private lazy var notificationsFetchedResultsController: NSFetchedResultsController<Notification> = {
|
||||
let fetchRequest: NSFetchRequest<Notification> = Notification.fetchRequest()
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "time", ascending: true)]
|
||||
|
||||
let controller = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: Store.shared.context, sectionNameKeyPath: nil, cacheName: nil)
|
||||
controller.delegate = self
|
||||
|
||||
do {
|
||||
Log.d(tag, "Fetching notifications")
|
||||
try controller.performFetch()
|
||||
} catch {
|
||||
Log.w(tag, "Failed to fetch notifications: \(error)", error)
|
||||
}
|
||||
|
||||
return controller
|
||||
}()
|
||||
|
||||
var subscriptions: [Subscription] {
|
||||
fetchedResultsController.fetchedObjects ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension SubscriptionsObservable: NSFetchedResultsControllerDelegate {
|
||||
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
|
||||
Log.d(tag, "Fetching notifications")
|
||||
DispatchQueue.main.async {
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import Foundation
|
||||
|
||||
struct Log {
|
||||
private static let dateFormat = "yyyy-MM-dd hh:mm:ss.SSSSSSZ"
|
||||
private static let dateFormat = "yy-MM-dd hh:mm:ss.SSS"
|
||||
private static let dateFormatter: DateFormatter = {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = dateFormat
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ struct NotificationListView: View {
|
|||
@EnvironmentObject private var store: Store
|
||||
|
||||
@ObservedObject var subscription: Subscription
|
||||
@ObservedObject var notificationsModel: NotificationsObservable
|
||||
|
||||
@State private var editMode = EditMode.inactive
|
||||
@State private var selection = Set<Notification>()
|
||||
|
|
@ -23,6 +24,11 @@ struct NotificationListView: View {
|
|||
return SubscriptionManager(store: store)
|
||||
}
|
||||
|
||||
init(subscription: Subscription) {
|
||||
self.subscription = subscription
|
||||
self.notificationsModel = NotificationsObservable(subscriptionID: subscription.objectID)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 15.0, *) {
|
||||
notificationList
|
||||
|
|
@ -36,7 +42,7 @@ struct NotificationListView: View {
|
|||
|
||||
private var notificationList: some View {
|
||||
List(selection: $selection) {
|
||||
ForEach(subscription.notificationsSorted(), id: \.self) { notification in
|
||||
ForEach(notificationsModel.notifications, id: \.self) { notification in
|
||||
NotificationRowView(notification: notification)
|
||||
}
|
||||
}
|
||||
|
|
@ -74,13 +80,13 @@ struct NotificationListView: View {
|
|||
subscriptionManager.poll(subscription)
|
||||
}
|
||||
}
|
||||
if subscription.notificationCount() > 0 {
|
||||
if notificationsModel.notifications.count > 0 {
|
||||
editButton
|
||||
}
|
||||
Button("Send test notification") {
|
||||
self.sendTestNotification()
|
||||
}
|
||||
if subscription.notificationCount() > 0 {
|
||||
if notificationsModel.notifications.count > 0 {
|
||||
Button("Clear all notifications") {
|
||||
self.showAlert = true
|
||||
self.activeAlert = .clear
|
||||
|
|
@ -140,7 +146,7 @@ struct NotificationListView: View {
|
|||
}
|
||||
}
|
||||
.overlay(Group {
|
||||
if subscription.notificationCount() == 0 {
|
||||
if notificationsModel.notifications.count == 0 {
|
||||
VStack {
|
||||
Text("You haven't received any notifications for this topic yet.")
|
||||
.font(.title2)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ struct SubscriptionListView: View {
|
|||
let tag = "SubscriptionList"
|
||||
|
||||
@EnvironmentObject private var store: Store
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \Subscription.topic, ascending: true)]) var subscriptions: FetchedResults<Subscription>
|
||||
@ObservedObject var subscriptionsModel = SubscriptionsObservable()
|
||||
@State private var showingAddDialog = false
|
||||
|
||||
private var subscriptionManager: SubscriptionManager {
|
||||
|
|
@ -19,7 +19,7 @@ struct SubscriptionListView: View {
|
|||
if #available(iOS 15.0, *) {
|
||||
subscriptionList
|
||||
.refreshable {
|
||||
subscriptions.forEach { subscription in
|
||||
subscriptionsModel.subscriptions.forEach { subscription in
|
||||
subscriptionManager.poll(subscription)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ struct SubscriptionListView: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
subscriptions.forEach { subscription in
|
||||
subscriptionsModel.subscriptions.forEach { subscription in
|
||||
subscriptionManager.poll(subscription)
|
||||
}
|
||||
} label: {
|
||||
|
|
@ -43,7 +43,7 @@ struct SubscriptionListView: View {
|
|||
|
||||
private var subscriptionList: some View {
|
||||
List {
|
||||
ForEach(subscriptions) { subscription in
|
||||
ForEach(subscriptionsModel.subscriptions) { subscription in
|
||||
SubscriptionItemNavView(subscription: subscription)
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ struct SubscriptionListView: View {
|
|||
}
|
||||
}
|
||||
.overlay(Group {
|
||||
if subscriptions.isEmpty {
|
||||
if subscriptionsModel.subscriptions.isEmpty {
|
||||
VStack {
|
||||
Text("It looks like you don't have any subscriptions yet")
|
||||
.font(.title2)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue