From bf7b66d5c1c0d7f7bdcc1c1aa43b6881d797d1e8 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 15 May 2024 11:49:07 -0700 Subject: [PATCH] Add push notification extensions (#4005) * add wav * add sound to config * add extension to `updateExtensions.sh` * add ios source files * add a build extension * add a new module * use correct type on ios * update the build plugin * add android handler * create a patch for expo-notifications * basic android implementation * add entitlements for notifications extension * add some generic logic for ios * add age check logic * add extension to app config * remove dash * move directory * rename again * update privacy manifest * add prefs storage ios * better types * create interface for setting and getting prefs * add notifications prefs for android * add functions to module * add types to js * add prefs context * add web stub * wrap the app * fix types * more preferences for ios * add a test toggle * swap vars * update patch * fix patch error * fix typo * sigh * sigh * get stored prefs on launch * anotehr type * simplify * about finished * comment * adjust plugin * use supported file types * update NSE * futureproof ios * futureproof android * update sound file name * handle initialization * more cleanup * update js types * strict js types * set the notification channel * rm * add silent channel * add mute logic * update patch * podfile * adjust channels * fix android channel * update readme * oreo or higher * nit * don't use getValue * nit --- app.config.js | 14 +- assets/blueskydm.wav | Bin 33540 -> 0 bytes assets/dm.aiff | Bin 0 -> 184392 bytes assets/dm.mp3 | Bin 0 -> 7935 bytes modules/BlueskyNSE/BlueskyNSE.entitlements | 10 + modules/BlueskyNSE/Info.plist | 29 +++ modules/BlueskyNSE/NotificationService.swift | 51 +++++ modules/Share-with-Bluesky/Info.plist | 2 +- .../Share-with-Bluesky.entitlements | 2 +- .../android/build.gradle | 93 +++++++++ .../android/src/main/AndroidManifest.xml | 2 + .../BackgroundNotificationHandler.kt | 39 ++++ .../BackgroundNotificationHandlerInterface.kt | 7 + ...ExpoBackgroundNotificationHandlerModule.kt | 70 +++++++ .../NotificationPrefs.kt | 134 ++++++++++++ .../expo-module.config.json | 9 + .../index.ts | 2 + .../ExpoBackgroundNotificationHandler.podspec | 21 ++ ...oBackgroundNotificationHandlerModule.swift | 116 +++++++++++ .../BackgroundNotificationHandlerProvider.tsx | 70 +++++++ ...ExpoBackgroundNotificationHandler.types.ts | 40 ++++ ...ExpoBackgroundNotificationHandlerModule.ts | 8 + ...BackgroundNotificationHandlerModule.web.ts | 27 +++ patches/expo-notifications+0.27.6.patch | 197 ++++++++++++++++++ patches/expo-notifications-0.27.6.patch.md | 9 + plugins/notificationsExtension/README.md | 17 ++ .../withAppEntitlements.js | 13 ++ .../withExtensionEntitlements.js | 31 +++ .../withExtensionInfoPlist.js | 39 ++++ .../withExtensionViewController.js | 31 +++ .../withNotificationsExtension.js | 55 +++++ plugins/notificationsExtension/withSounds.js | 27 +++ .../notificationsExtension/withXcodeTarget.js | 76 +++++++ scripts/updateExtensions.sh | 8 + src/App.native.tsx | 11 +- src/App.web.tsx | 9 +- src/lib/hooks/useNotificationHandler.ts | 25 ++- src/screens/Messages/Settings.tsx | 15 ++ 38 files changed, 1297 insertions(+), 12 deletions(-) delete mode 100644 assets/blueskydm.wav create mode 100644 assets/dm.aiff create mode 100644 assets/dm.mp3 create mode 100644 modules/BlueskyNSE/BlueskyNSE.entitlements create mode 100644 modules/BlueskyNSE/Info.plist create mode 100644 modules/BlueskyNSE/NotificationService.swift create mode 100644 modules/expo-background-notification-handler/android/build.gradle create mode 100644 modules/expo-background-notification-handler/android/src/main/AndroidManifest.xml create mode 100644 modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt create mode 100644 modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt create mode 100644 modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt create mode 100644 modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt create mode 100644 modules/expo-background-notification-handler/expo-module.config.json create mode 100644 modules/expo-background-notification-handler/index.ts create mode 100644 modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandler.podspec create mode 100644 modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift create mode 100644 modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx create mode 100644 modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts create mode 100644 modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.ts create mode 100644 modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts create mode 100644 patches/expo-notifications+0.27.6.patch create mode 100644 patches/expo-notifications-0.27.6.patch.md create mode 100644 plugins/notificationsExtension/README.md create mode 100644 plugins/notificationsExtension/withAppEntitlements.js create mode 100644 plugins/notificationsExtension/withExtensionEntitlements.js create mode 100644 plugins/notificationsExtension/withExtensionInfoPlist.js create mode 100644 plugins/notificationsExtension/withExtensionViewController.js create mode 100644 plugins/notificationsExtension/withNotificationsExtension.js create mode 100644 plugins/notificationsExtension/withSounds.js create mode 100644 plugins/notificationsExtension/withXcodeTarget.js diff --git a/app.config.js b/app.config.js index 4b54157c..4ade9de3 100644 --- a/app.config.js +++ b/app.config.js @@ -110,7 +110,7 @@ module.exports = function (config) { { NSPrivacyAccessedAPIType: 'NSPrivacyAccessedAPICategoryUserDefaults', - NSPrivacyAccessedAPITypeReasons: ['CA92.1'], + NSPrivacyAccessedAPITypeReasons: ['CA92.1', '1C8F.1'], }, ], }, @@ -200,7 +200,7 @@ module.exports = function (config) { { icon: './assets/icon-android-notification.png', color: '#1185fe', - sounds: ['assets/blueskydm.wav'], + sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'], }, ], './plugins/withAndroidManifestPlugin.js', @@ -209,6 +209,7 @@ module.exports = function (config) { './plugins/withAndroidStylesAccentColorPlugin.js', './plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js', './plugins/shareExtension/withShareExtensions.js', + './plugins/notificationsExtension/withNotificationsExtension.js', ].filter(Boolean), extra: { eas: { @@ -225,6 +226,15 @@ module.exports = function (config) { ], }, }, + { + targetName: 'BlueskyNSE', + bundleIdentifier: 'xyz.blueskyweb.app.BlueskyNSE', + entitlements: { + 'com.apple.security.application-groups': [ + 'group.app.bsky', + ], + }, + }, ], }, }, diff --git a/assets/blueskydm.wav b/assets/blueskydm.wav deleted file mode 100644 index 8d35258dd76dad52c98e68788a7ca1cd70b44c01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33540 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 bkxQzQf*52A-!A)L{CBkfGXwVD)XM(>@hMbq diff --git a/assets/dm.aiff b/assets/dm.aiff new file mode 100644 index 0000000000000000000000000000000000000000..364b814b7f5127cd05ada4ae7fe7c1501b46657f GIT binary patch literal 184392 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

sB{0;?ELG`|1AcdF{O(>9 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 Tp5eSaH1OkJvxsZ|Y|j4y=J^(C literal 0 HcmV?d00001 diff --git a/modules/BlueskyNSE/BlueskyNSE.entitlements b/modules/BlueskyNSE/BlueskyNSE.entitlements new file mode 100644 index 00000000..4954bdb3 --- /dev/null +++ b/modules/BlueskyNSE/BlueskyNSE.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.app.bsky + + + \ No newline at end of file diff --git a/modules/BlueskyNSE/Info.plist b/modules/BlueskyNSE/Info.plist new file mode 100644 index 00000000..c2dd7eda --- /dev/null +++ b/modules/BlueskyNSE/Info.plist @@ -0,0 +1,29 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + MainAppScheme + bluesky + CFBundleName + $(PRODUCT_NAME) + CFBundleDisplayName + Bluesky Notifications + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + + \ No newline at end of file diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift new file mode 100644 index 00000000..c6f391e0 --- /dev/null +++ b/modules/BlueskyNSE/NotificationService.swift @@ -0,0 +1,51 @@ +import UserNotifications + +let APP_GROUP = "group.app.bsky" + +class NotificationService: UNNotificationServiceExtension { + var prefs = UserDefaults(suiteName: APP_GROUP) + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + guard var bestAttempt = createCopy(request.content), + let reason = request.content.userInfo["reason"] as? String + else { + contentHandler(request.content) + return + } + + if reason == "chat-message" { + mutateWithChatMessage(bestAttempt) + } + + // The badge should always be incremented when in the background + mutateWithBadge(bestAttempt) + + contentHandler(bestAttempt) + } + + override func serviceExtensionTimeWillExpire() { + // If for some reason the alloted time expires, we don't actually want to display a notification + } + + func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { + return content.mutableCopy() as? UNMutableNotificationContent + } + + func mutateWithBadge(_ content: UNMutableNotificationContent) { + content.badge = 1 + } + + func mutateWithChatMessage(_ content: UNMutableNotificationContent) { + if self.prefs?.bool(forKey: "playSoundChat") == true { + mutateWithDmSound(content) + } + } + + func mutateWithDefaultSound(_ content: UNMutableNotificationContent) { + content.sound = UNNotificationSound.default + } + + func mutateWithDmSound(_ content: UNMutableNotificationContent) { + content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) + } +} diff --git a/modules/Share-with-Bluesky/Info.plist b/modules/Share-with-Bluesky/Info.plist index 90fe9234..421abb3c 100644 --- a/modules/Share-with-Bluesky/Info.plist +++ b/modules/Share-with-Bluesky/Info.plist @@ -38,4 +38,4 @@ CFBundleShortVersionString $(MARKETING_VERSION) - + \ No newline at end of file diff --git a/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements b/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements index d2253d31..4954bdb3 100644 --- a/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements +++ b/modules/Share-with-Bluesky/Share-with-Bluesky.entitlements @@ -7,4 +7,4 @@ group.app.bsky - + \ No newline at end of file diff --git a/modules/expo-background-notification-handler/android/build.gradle b/modules/expo-background-notification-handler/android/build.gradle new file mode 100644 index 00000000..e18eee93 --- /dev/null +++ b/modules/expo-background-notification-handler/android/build.gradle @@ -0,0 +1,93 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'maven-publish' + +group = 'expo.modules.backgroundnotificationhandler' +version = '0.5.0' + +buildscript { + def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle") + if (expoModulesCorePlugin.exists()) { + apply from: expoModulesCorePlugin + applyKotlinExpoModulesCorePlugin() + } + + // Simple helper that allows the root project to override versions declared by this library. + ext.safeExtGet = { prop, fallback -> + rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback + } + + // Ensures backward compatibility + ext.getKotlinVersion = { + if (ext.has("kotlinVersion")) { + ext.kotlinVersion() + } else { + ext.safeExtGet("kotlinVersion", "1.8.10") + } + } + + repositories { + mavenCentral() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}") + } +} + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + from components.release + } + } + repositories { + maven { + url = mavenLocal().url + } + } + } +} + +android { + compileSdkVersion safeExtGet("compileSdkVersion", 33) + + def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION + if (agpVersion.tokenize('.')[0].toInteger() < 8) { + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.majorVersion + } + } + + namespace "expo.modules.backgroundnotificationhandler" + defaultConfig { + minSdkVersion safeExtGet("minSdkVersion", 21) + targetSdkVersion safeExtGet("targetSdkVersion", 34) + versionCode 1 + versionName "0.5.0" + } + lintOptions { + abortOnError false + } + publishing { + singleVariant("release") { + withSourcesJar() + } + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation project(':expo-modules-core') + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" + implementation 'com.google.firebase:firebase-messaging-ktx:24.0.0' +} diff --git a/modules/expo-background-notification-handler/android/src/main/AndroidManifest.xml b/modules/expo-background-notification-handler/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..bdae66c8 --- /dev/null +++ b/modules/expo-background-notification-handler/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt new file mode 100644 index 00000000..34450852 --- /dev/null +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt @@ -0,0 +1,39 @@ +package expo.modules.backgroundnotificationhandler + +import android.content.Context +import com.google.firebase.messaging.RemoteMessage + +class BackgroundNotificationHandler( + private val context: Context, + private val notifInterface: BackgroundNotificationHandlerInterface +) { + fun handleMessage(remoteMessage: RemoteMessage) { + if (ExpoBackgroundNotificationHandlerModule.isForegrounded) { + // We'll let expo-notifications handle the notification if the app is foregrounded + return + } + + if (remoteMessage.data["reason"] == "chat-message") { + mutateWithChatMessage(remoteMessage) + } + + notifInterface.showMessage(remoteMessage) + } + + private fun mutateWithChatMessage(remoteMessage: RemoteMessage) { + if (NotificationPrefs(context).getBoolean("playSoundChat")) { + // If oreo or higher + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + remoteMessage.data["channelId"] = "chat-messages" + } else { + remoteMessage.data["sound"] = "dm.mp3" + } + } else { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + remoteMessage.data["channelId"] = "chat-messages-muted" + } else { + remoteMessage.data["sound"] = null + } + } + } +} diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt new file mode 100644 index 00000000..41fb65eb --- /dev/null +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandlerInterface.kt @@ -0,0 +1,7 @@ +package expo.modules.backgroundnotificationhandler + +import com.google.firebase.messaging.RemoteMessage + +interface BackgroundNotificationHandlerInterface { + fun showMessage(remoteMessage: RemoteMessage) +} diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt new file mode 100644 index 00000000..083ff122 --- /dev/null +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt @@ -0,0 +1,70 @@ +package expo.modules.backgroundnotificationhandler + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBackgroundNotificationHandlerModule : Module() { + companion object { + var isForegrounded = false + } + + override fun definition() = ModuleDefinition { + Name("ExpoBackgroundNotificationHandler") + + OnCreate { + NotificationPrefs(appContext.reactContext).initialize() + } + + OnActivityEntersForeground { + isForegrounded = true + } + + OnActivityEntersBackground { + isForegrounded = false + } + + AsyncFunction("getAllPrefsAsync") { + return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs() + } + + AsyncFunction("getBoolAsync") { forKey: String -> + return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey) + } + + AsyncFunction("getStringAsync") { forKey: String -> + return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey) + } + + AsyncFunction("getStringArrayAsync") { forKey: String -> + return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey) + } + + AsyncFunction("setBoolAsync") { forKey: String, value: Boolean -> + NotificationPrefs(appContext.reactContext).setBoolean(forKey, value) + } + + AsyncFunction("setStringAsync") { forKey: String, value: String -> + NotificationPrefs(appContext.reactContext).setString(forKey, value) + } + + AsyncFunction("setStringArrayAsync") { forKey: String, value: Array -> + NotificationPrefs(appContext.reactContext).setStringArray(forKey, value) + } + + AsyncFunction("addToStringArrayAsync") { forKey: String, string: String -> + NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string) + } + + AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String -> + NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string) + } + + AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array -> + NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings) + } + + AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array -> + NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) + } + } +} diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt new file mode 100644 index 00000000..17ef9205 --- /dev/null +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt @@ -0,0 +1,134 @@ +package expo.modules.backgroundnotificationhandler + +import android.content.Context + +val DEFAULTS = mapOf( + "playSoundChat" to true, + "playSoundFollow" to false, + "playSoundLike" to false, + "playSoundMention" to false, + "playSoundQuote" to false, + "playSoundReply" to false, + "playSoundRepost" to false, + "mutedThreads" to mapOf>() +) + +class NotificationPrefs (private val context: Context?) { + private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) + ?: throw Error("Context is null") + + fun initialize() { + prefs + .edit() + .apply { + DEFAULTS.forEach { (key, value) -> + if (prefs.contains(key)) { + return@forEach + } + + when (value) { + is Boolean -> { + putBoolean(key, value) + } + is String -> { + putString(key, value) + } + is Array<*> -> { + putStringSet(key, value.map { it.toString() }.toSet()) + } + is Map<*, *> -> { + putStringSet(key, value.map { it.toString() }.toSet()) + } + } + } + } + .apply() + } + + fun getAllPrefs(): MutableMap { + return prefs.all + } + + fun getBoolean(key: String): Boolean { + return prefs.getBoolean(key, false) + } + + fun getString(key: String): String? { + return prefs.getString(key, null) + } + + fun getStringArray(key: String): Array? { + return prefs.getStringSet(key, null)?.toTypedArray() + } + + fun setBoolean(key: String, value: Boolean) { + prefs + .edit() + .apply { + putBoolean(key, value) + } + .apply() + } + + fun setString(key: String, value: String) { + prefs + .edit() + .apply { + putString(key, value) + } + .apply() + } + + fun setStringArray(key: String, value: Array) { + prefs + .edit() + .apply { + putStringSet(key, value.toSet()) + } + .apply() + } + + fun addToStringArray(key: String, string: String) { + prefs + .edit() + .apply { + val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() + set.add(string) + putStringSet(key, set) + } + .apply() + } + + fun removeFromStringArray(key: String, string: String) { + prefs + .edit() + .apply { + val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() + set.remove(string) + putStringSet(key, set) + } + .apply() + } + + fun addManyToStringArray(key: String, strings: Array) { + prefs + .edit() + .apply { + val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() + set.addAll(strings.toSet()) + putStringSet(key, set) + } + .apply() + } + + fun removeManyFromStringArray(key: String, strings: Array) { + prefs + .edit() + .apply { + val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() + set.removeAll(strings.toSet()) + putStringSet(key, set) + } + .apply() + } +} \ No newline at end of file diff --git a/modules/expo-background-notification-handler/expo-module.config.json b/modules/expo-background-notification-handler/expo-module.config.json new file mode 100644 index 00000000..9e5c9d55 --- /dev/null +++ b/modules/expo-background-notification-handler/expo-module.config.json @@ -0,0 +1,9 @@ +{ + "platforms": ["ios", "android"], + "ios": { + "modules": ["ExpoBackgroundNotificationHandlerModule"] + }, + "android": { + "modules": ["expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule"] + } +} diff --git a/modules/expo-background-notification-handler/index.ts b/modules/expo-background-notification-handler/index.ts new file mode 100644 index 00000000..680c6c13 --- /dev/null +++ b/modules/expo-background-notification-handler/index.ts @@ -0,0 +1,2 @@ +import {BackgroundNotificationHandler} from './src/ExpoBackgroundNotificationHandlerModule' +export default BackgroundNotificationHandler diff --git a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandler.podspec b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandler.podspec new file mode 100644 index 00000000..363c7b5e --- /dev/null +++ b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandler.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'ExpoBackgroundNotificationHandler' + s.version = '1.0.0' + s.summary = 'Interface for BlueskyNSE preferences' + s.description = 'Interface for BlueskyNSE preferenes' + s.author = '' + s.homepage = 'https://github.com/bluesky-social/social-app' + s.platforms = { :ios => '13.4', :tvos => '13.4' } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'SWIFT_COMPILATION_MODE' => 'wholemodule' + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift new file mode 100644 index 00000000..08972a04 --- /dev/null +++ b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift @@ -0,0 +1,116 @@ +import ExpoModulesCore + +let APP_GROUP = "group.app.bsky" + +let DEFAULTS: [String:Any] = [ + "playSoundChat" : true, + "playSoundFollow": false, + "playSoundLike": false, + "playSoundMention": false, + "playSoundQuote": false, + "playSoundReply": false, + "playSoundRepost": false, + "mutedThreads": [:] as! [String:[String]] +] + +/* + * The purpose of this module is to store values that are needed by the notification service + * extension. Since we would rather get and store values such as age or user mute state + * while the app is foregrounded, we should use this module liberally. We should aim to keep + * background fetches to a minimum (two or three times per hour) while the app is backgrounded + * or killed + */ +public class ExpoBackgroundNotificationHandlerModule: Module { + let userDefaults = UserDefaults(suiteName: APP_GROUP) + + public func definition() -> ModuleDefinition { + Name("ExpoBackgroundNotificationHandler") + + OnCreate { + DEFAULTS.forEach { p in + if userDefaults?.value(forKey: p.key) == nil { + userDefaults?.setValue(p.value, forKey: p.key) + } + } + } + + AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in + var keys: [String] = [] + DEFAULTS.forEach { p in + keys.append(p.key) + } + return userDefaults?.dictionaryWithValues(forKeys: keys) + } + + AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in + if let pref = userDefaults?.bool(forKey: forKey) { + return pref + } + return false + } + + AsyncFunction("getStringAsync") { (forKey: String) -> String? in + if let pref = userDefaults?.string(forKey: forKey) { + return pref + } + return nil + } + + AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in + if let pref = userDefaults?.stringArray(forKey: forKey) { + return pref + } + return nil + } + + AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in + userDefaults?.setValue(value, forKey: forKey) + } + + AsyncFunction("setStringAsync") { (forKey: String, value: String) -> Void in + userDefaults?.setValue(value, forKey: forKey) + } + + AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in + userDefaults?.setValue(value, forKey: forKey) + } + + AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in + if var curr = userDefaults?.stringArray(forKey: forKey), + !curr.contains(string) + { + curr.append(string) + userDefaults?.setValue(curr, forKey: forKey) + } + } + + AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in + if var curr = userDefaults?.stringArray(forKey: forKey) { + curr.removeAll { s in + return s == string + } + userDefaults?.setValue(curr, forKey: forKey) + } + } + + AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in + if var curr = userDefaults?.stringArray(forKey: forKey) { + strings.forEach { s in + if !curr.contains(s) { + curr.append(s) + } + } + userDefaults?.setValue(curr, forKey: forKey) + } + } + + AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in + if var curr = userDefaults?.stringArray(forKey: forKey) { + strings.forEach { s in + curr.removeAll(where: { $0 == s }) + } + userDefaults?.setValue(curr, forKey: forKey) + } + } + } +} diff --git a/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx b/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx new file mode 100644 index 00000000..6ecdd1d4 --- /dev/null +++ b/modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider.tsx @@ -0,0 +1,70 @@ +import React from 'react' + +import {BackgroundNotificationHandlerPreferences} from './ExpoBackgroundNotificationHandler.types' +import {BackgroundNotificationHandler} from './ExpoBackgroundNotificationHandlerModule' + +interface BackgroundNotificationPreferencesContext { + preferences: BackgroundNotificationHandlerPreferences + setPref: ( + key: Key, + value: BackgroundNotificationHandlerPreferences[Key], + ) => void +} + +const Context = React.createContext( + {} as BackgroundNotificationPreferencesContext, +) +export const useBackgroundNotificationPreferences = () => + React.useContext(Context) + +export function BackgroundNotificationPreferencesProvider({ + children, +}: { + children: React.ReactNode +}) { + const [preferences, setPreferences] = + React.useState({ + playSoundChat: true, + }) + + React.useEffect(() => { + ;(async () => { + const prefs = await BackgroundNotificationHandler.getAllPrefsAsync() + setPreferences(prefs) + })() + }, []) + + const value = React.useMemo( + () => ({ + preferences, + setPref: async < + Key extends keyof BackgroundNotificationHandlerPreferences, + >( + k: Key, + v: BackgroundNotificationHandlerPreferences[Key], + ) => { + switch (typeof v) { + case 'boolean': { + await BackgroundNotificationHandler.setBoolAsync(k, v) + break + } + case 'string': { + await BackgroundNotificationHandler.setStringAsync(k, v) + break + } + default: { + throw new Error(`Invalid type for value: ${typeof v}`) + } + } + + setPreferences(prev => ({ + ...prev, + [k]: v, + })) + }, + }), + [preferences], + ) + + return {children} +} diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts new file mode 100644 index 00000000..5fbd302d --- /dev/null +++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandler.types.ts @@ -0,0 +1,40 @@ +export type ExpoBackgroundNotificationHandlerModule = { + getAllPrefsAsync: () => Promise + getBoolAsync: (forKey: string) => Promise + getStringAsync: (forKey: string) => Promise + getStringArrayAsync: (forKey: string) => Promise + setBoolAsync: ( + forKey: keyof BackgroundNotificationHandlerPreferences, + value: boolean, + ) => Promise + setStringAsync: ( + forKey: keyof BackgroundNotificationHandlerPreferences, + value: string, + ) => Promise + setStringArrayAsync: ( + forKey: keyof BackgroundNotificationHandlerPreferences, + value: string[], + ) => Promise + addToStringArrayAsync: ( + forKey: keyof BackgroundNotificationHandlerPreferences, + value: string, + ) => Promise + removeFromStringArrayAsync: ( + forKey: keyof BackgroundNotificationHandlerPreferences, + value: string, + ) => Promise + addManyToStringArrayAsync: ( + forKey: keyof BackgroundNotificationHandlerPreferences, + value: string[], + ) => Promise + removeManyFromStringArrayAsync: ( + forKey: keyof BackgroundNotificationHandlerPreferences, + value: string[], + ) => Promise +} + +// TODO there are more preferences in the native code, however they have not been added here yet. +// Don't add them until the native logic also handles the notifications for those preference types. +export type BackgroundNotificationHandlerPreferences = { + playSoundChat: boolean +} diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.ts new file mode 100644 index 00000000..d6517893 --- /dev/null +++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.ts @@ -0,0 +1,8 @@ +import {requireNativeModule} from 'expo-modules-core' + +import {ExpoBackgroundNotificationHandlerModule} from './ExpoBackgroundNotificationHandler.types' + +export const BackgroundNotificationHandler = + requireNativeModule( + 'ExpoBackgroundNotificationHandler', + ) diff --git a/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts new file mode 100644 index 00000000..29e27fd0 --- /dev/null +++ b/modules/expo-background-notification-handler/src/ExpoBackgroundNotificationHandlerModule.web.ts @@ -0,0 +1,27 @@ +import { + BackgroundNotificationHandlerPreferences, + ExpoBackgroundNotificationHandlerModule, +} from './ExpoBackgroundNotificationHandler.types' + +// Stub for web +export const BackgroundNotificationHandler = { + getAllPrefsAsync: async () => { + return {} as BackgroundNotificationHandlerPreferences + }, + getBoolAsync: async (_: string) => { + return false + }, + getStringAsync: async (_: string) => { + return '' + }, + getStringArrayAsync: async (_: string) => { + return [] + }, + setBoolAsync: async (_: string, __: boolean) => {}, + setStringAsync: async (_: string, __: string) => {}, + setStringArrayAsync: async (_: string, __: string[]) => {}, + addToStringArrayAsync: async (_: string, __: string) => {}, + removeFromStringArrayAsync: async (_: string, __: string) => {}, + addManyToStringArrayAsync: async (_: string, __: string[]) => {}, + removeManyFromStringArrayAsync: async (_: string, __: string[]) => {}, +} as ExpoBackgroundNotificationHandlerModule diff --git a/patches/expo-notifications+0.27.6.patch b/patches/expo-notifications+0.27.6.patch new file mode 100644 index 00000000..ba196eca --- /dev/null +++ b/patches/expo-notifications+0.27.6.patch @@ -0,0 +1,197 @@ +diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle +index 97bf4f4..6e9d427 100644 +--- a/node_modules/expo-notifications/android/build.gradle ++++ b/node_modules/expo-notifications/android/build.gradle +@@ -118,6 +118,7 @@ dependencies { + api 'com.google.firebase:firebase-messaging:22.0.0' + + api 'me.leolin:ShortcutBadger:1.1.22@aar' ++ implementation project(':expo-background-notification-handler') + + if (project.findProject(':expo-modules-test-core')) { + testImplementation project(':expo-modules-test-core') +diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java +index 0af7fe0..8f2c8d8 100644 +--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java ++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java +@@ -14,6 +14,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority; + import expo.modules.notifications.notifications.model.NotificationContent; + + public class JSONNotificationContentBuilder extends NotificationContent.Builder { ++ private static final String CHANNEL_ID_KEY = "channelId"; + private static final String TITLE_KEY = "title"; + private static final String TEXT_KEY = "message"; + private static final String SUBTITLE_KEY = "subtitle"; +@@ -36,6 +37,7 @@ public class JSONNotificationContentBuilder extends NotificationContent.Builder + + public NotificationContent.Builder setPayload(JSONObject payload) { + this.setTitle(getTitle(payload)) ++ .setChannelId(getChannelId(payload)) + .setSubtitle(getSubtitle(payload)) + .setText(getText(payload)) + .setBody(getBody(payload)) +@@ -60,6 +62,14 @@ public class JSONNotificationContentBuilder extends NotificationContent.Builder + return this; + } + ++ protected String getChannelId(JSONObject payload) { ++ try { ++ return payload.getString(CHANNEL_ID_KEY); ++ } catch (JSONException e) { ++ return null; ++ } ++ } ++ + protected String getTitle(JSONObject payload) { + try { + return payload.getString(TITLE_KEY); +diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java +index f1fed19..1619f59 100644 +--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java ++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java +@@ -20,6 +20,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority; + * should be created using {@link NotificationContent.Builder}. + */ + public class NotificationContent implements Parcelable, Serializable { ++ private String mChannelId; + private String mTitle; + private String mText; + private String mSubtitle; +@@ -50,6 +51,9 @@ public class NotificationContent implements Parcelable, Serializable { + } + }; + ++ @Nullable ++ public String getChannelId() { return mChannelId; } ++ + @Nullable + public String getTitle() { + return mTitle; +@@ -121,6 +125,7 @@ public class NotificationContent implements Parcelable, Serializable { + } + + protected NotificationContent(Parcel in) { ++ mChannelId = in.readString(); + mTitle = in.readString(); + mText = in.readString(); + mSubtitle = in.readString(); +@@ -146,6 +151,7 @@ public class NotificationContent implements Parcelable, Serializable { + + @Override + public void writeToParcel(Parcel dest, int flags) { ++ dest.writeString(mChannelId); + dest.writeString(mTitle); + dest.writeString(mText); + dest.writeString(mSubtitle); +@@ -166,6 +172,7 @@ public class NotificationContent implements Parcelable, Serializable { + private static final long serialVersionUID = 397666843266836802L; + + private void writeObject(java.io.ObjectOutputStream out) throws IOException { ++ out.writeObject(mChannelId); + out.writeObject(mTitle); + out.writeObject(mText); + out.writeObject(mSubtitle); +@@ -190,6 +197,7 @@ public class NotificationContent implements Parcelable, Serializable { + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { ++ mChannelId = (String) in.readObject(); + mTitle = (String) in.readObject(); + mText = (String) in.readObject(); + mSubtitle = (String) in.readObject(); +@@ -240,6 +248,7 @@ public class NotificationContent implements Parcelable, Serializable { + } + + public static class Builder { ++ private String mChannelId; + private String mTitle; + private String mText; + private String mSubtitle; +@@ -260,6 +269,11 @@ public class NotificationContent implements Parcelable, Serializable { + useDefaultVibrationPattern(); + } + ++ public Builder setChannelId(String channelId) { ++ mChannelId = channelId; ++ return this; ++ } ++ + public Builder setTitle(String title) { + mTitle = title; + return this; +@@ -336,6 +350,7 @@ public class NotificationContent implements Parcelable, Serializable { + + public NotificationContent build() { + NotificationContent content = new NotificationContent(); ++ content.mChannelId = mChannelId; + content.mTitle = mTitle; + content.mSubtitle = mSubtitle; + content.mText = mText; +diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java +index 6bd9928..aab71ea 100644 +--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java ++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java +@@ -7,7 +7,6 @@ import android.content.pm.PackageManager; + import android.content.res.Resources; + import android.graphics.Bitmap; + import android.graphics.BitmapFactory; +-import android.os.Build; + import android.os.Bundle; + import android.os.Parcel; + import android.provider.Settings; +@@ -48,6 +47,10 @@ public class ExpoNotificationBuilder extends ChannelAwareNotificationBuilder { + + NotificationContent content = getNotificationContent(); + ++ if (content.getChannelId() != null) { ++ builder.setChannelId(content.getChannelId()); ++ } ++ + builder.setAutoCancel(content.isAutoDismiss()); + builder.setOngoing(content.isSticky()); + +diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt +index 55b3a8d..1b99d5b 100644 +--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt ++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt +@@ -12,11 +12,14 @@ import expo.modules.notifications.notifications.model.triggers.FirebaseNotificat + import expo.modules.notifications.service.NotificationsService + import expo.modules.notifications.service.interfaces.FirebaseMessagingDelegate + import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener ++import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandler ++import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandlerInterface ++import expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule + import org.json.JSONObject + import java.lang.ref.WeakReference + import java.util.* + +-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate { ++open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface { + companion object { + // Unfortunately we cannot save state between instances of a service other way + // than by static properties. Fortunately, using weak references we can +@@ -89,12 +92,21 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM + fun getBackgroundTasks() = sBackgroundTaskConsumerReferences.values.mapNotNull { it.get() } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { +- NotificationsService.receive(context, createNotification(remoteMessage)) +- getBackgroundTasks().forEach { +- it.scheduleJob(RemoteMessageSerializer.toBundle(remoteMessage)) ++ if (!ExpoBackgroundNotificationHandlerModule.isForegrounded) { ++ BackgroundNotificationHandler(context, this).handleMessage(remoteMessage) ++ return ++ } else { ++ showMessage(remoteMessage) ++ getBackgroundTasks().forEach { ++ it.scheduleJob(RemoteMessageSerializer.toBundle(remoteMessage)) ++ } + } + } + ++ override fun showMessage(remoteMessage: RemoteMessage) { ++ NotificationsService.receive(context, createNotification(remoteMessage)) ++ } ++ + protected fun createNotification(remoteMessage: RemoteMessage): Notification { + val identifier = getNotificationIdentifier(remoteMessage) + val payload = JSONObject(remoteMessage.data as Map<*, *>) diff --git a/patches/expo-notifications-0.27.6.patch.md b/patches/expo-notifications-0.27.6.patch.md new file mode 100644 index 00000000..59b2598f --- /dev/null +++ b/patches/expo-notifications-0.27.6.patch.md @@ -0,0 +1,9 @@ +## LOAD BEARING PATCH, DO NOT REMOVE + +## Expo-Notifications Patch + +This patch supports the Android background notification handling module. Incoming messages +in `onMessageReceived` are sent to the module for handling. + +It also allows us to set the Android notification channel ID from the notification `data`, rather +than the `notification` object in the payload. diff --git a/plugins/notificationsExtension/README.md b/plugins/notificationsExtension/README.md new file mode 100644 index 00000000..31b8bfe7 --- /dev/null +++ b/plugins/notificationsExtension/README.md @@ -0,0 +1,17 @@ +# Notifications extension plugin for Expo + +This plugin handles moving the necessary files into their respective iOS directories + +## Steps + +### ios + +1. Update entitlements +2. Set the app group to group. +3. Add the extension plist +4. Add the view controller +5. Update the xcode project's build phases + +## Credits + +Adapted from https://github.com/andrew-levy/react-native-safari-extension and https://github.com/timedtext/expo-config-plugin-ios-share-extension/blob/master/src/withShareExtensionXcodeTarget.ts diff --git a/plugins/notificationsExtension/withAppEntitlements.js b/plugins/notificationsExtension/withAppEntitlements.js new file mode 100644 index 00000000..4ce81ea6 --- /dev/null +++ b/plugins/notificationsExtension/withAppEntitlements.js @@ -0,0 +1,13 @@ +const {withEntitlementsPlist} = require('@expo/config-plugins') + +const withAppEntitlements = config => { + // eslint-disable-next-line no-shadow + return withEntitlementsPlist(config, async config => { + config.modResults['com.apple.security.application-groups'] = [ + `group.app.bsky`, + ] + return config + }) +} + +module.exports = {withAppEntitlements} diff --git a/plugins/notificationsExtension/withExtensionEntitlements.js b/plugins/notificationsExtension/withExtensionEntitlements.js new file mode 100644 index 00000000..0cc1c4ca --- /dev/null +++ b/plugins/notificationsExtension/withExtensionEntitlements.js @@ -0,0 +1,31 @@ +const {withInfoPlist} = require('@expo/config-plugins') +const plist = require('@expo/plist') +const path = require('path') +const fs = require('fs') + +const withExtensionEntitlements = (config, {extensionName}) => { + // eslint-disable-next-line no-shadow + return withInfoPlist(config, config => { + const extensionEntitlementsPath = path.join( + config.modRequest.platformProjectRoot, + extensionName, + `${extensionName}.entitlements`, + ) + + const notificationsExtensionEntitlements = { + 'com.apple.security.application-groups': [`group.app.bsky`], + } + + fs.mkdirSync(path.dirname(extensionEntitlementsPath), { + recursive: true, + }) + fs.writeFileSync( + extensionEntitlementsPath, + plist.default.build(notificationsExtensionEntitlements), + ) + + return config + }) +} + +module.exports = {withExtensionEntitlements} diff --git a/plugins/notificationsExtension/withExtensionInfoPlist.js b/plugins/notificationsExtension/withExtensionInfoPlist.js new file mode 100644 index 00000000..b0c6cfa8 --- /dev/null +++ b/plugins/notificationsExtension/withExtensionInfoPlist.js @@ -0,0 +1,39 @@ +const {withInfoPlist} = require('@expo/config-plugins') +const plist = require('@expo/plist') +const path = require('path') +const fs = require('fs') + +const withExtensionInfoPlist = (config, {extensionName}) => { + // eslint-disable-next-line no-shadow + return withInfoPlist(config, config => { + const plistPath = path.join( + config.modRequest.projectRoot, + 'modules', + extensionName, + 'Info.plist', + ) + const targetPath = path.join( + config.modRequest.platformProjectRoot, + extensionName, + 'Info.plist', + ) + + const extPlist = plist.default.parse(fs.readFileSync(plistPath).toString()) + + extPlist.MainAppScheme = config.scheme + extPlist.CFBundleName = '$(PRODUCT_NAME)' + extPlist.CFBundleDisplayName = 'Bluesky Notifications' + extPlist.CFBundleIdentifier = '$(PRODUCT_BUNDLE_IDENTIFIER)' + extPlist.CFBundleVersion = '$(CURRENT_PROJECT_VERSION)' + extPlist.CFBundleExecutable = '$(EXECUTABLE_NAME)' + extPlist.CFBundlePackageType = '$(PRODUCT_BUNDLE_PACKAGE_TYPE)' + extPlist.CFBundleShortVersionString = '$(MARKETING_VERSION)' + + fs.mkdirSync(path.dirname(targetPath), {recursive: true}) + fs.writeFileSync(targetPath, plist.default.build(extPlist)) + + return config + }) +} + +module.exports = {withExtensionInfoPlist} diff --git a/plugins/notificationsExtension/withExtensionViewController.js b/plugins/notificationsExtension/withExtensionViewController.js new file mode 100644 index 00000000..cd29bea7 --- /dev/null +++ b/plugins/notificationsExtension/withExtensionViewController.js @@ -0,0 +1,31 @@ +const {withXcodeProject} = require('@expo/config-plugins') +const path = require('path') +const fs = require('fs') + +const withExtensionViewController = ( + config, + {controllerName, extensionName}, +) => { + // eslint-disable-next-line no-shadow + return withXcodeProject(config, config => { + const controllerPath = path.join( + config.modRequest.projectRoot, + 'modules', + extensionName, + `${controllerName}.swift`, + ) + + const targetPath = path.join( + config.modRequest.platformProjectRoot, + extensionName, + `${controllerName}.swift`, + ) + + fs.mkdirSync(path.dirname(targetPath), {recursive: true}) + fs.copyFileSync(controllerPath, targetPath) + + return config + }) +} + +module.exports = {withExtensionViewController} diff --git a/plugins/notificationsExtension/withNotificationsExtension.js b/plugins/notificationsExtension/withNotificationsExtension.js new file mode 100644 index 00000000..6a00cfd2 --- /dev/null +++ b/plugins/notificationsExtension/withNotificationsExtension.js @@ -0,0 +1,55 @@ +const {withPlugins} = require('@expo/config-plugins') +const {withAppEntitlements} = require('./withAppEntitlements') +const {withXcodeTarget} = require('./withXcodeTarget') +const {withExtensionEntitlements} = require('./withExtensionEntitlements') +const {withExtensionInfoPlist} = require('./withExtensionInfoPlist') +const {withExtensionViewController} = require('./withExtensionViewController') +const {withSounds} = require('./withSounds') + +const EXTENSION_NAME = 'BlueskyNSE' +const EXTENSION_CONTROLLER_NAME = 'NotificationService' + +const withNotificationsExtension = config => { + const soundFiles = ['dm.aiff'] + + return withPlugins(config, [ + // IOS + withAppEntitlements, + [ + withExtensionEntitlements, + { + extensionName: EXTENSION_NAME, + }, + ], + [ + withExtensionInfoPlist, + { + extensionName: EXTENSION_NAME, + }, + ], + [ + withExtensionViewController, + { + extensionName: EXTENSION_NAME, + controllerName: EXTENSION_CONTROLLER_NAME, + }, + ], + [ + withSounds, + { + extensionName: EXTENSION_NAME, + soundFiles, + }, + ], + [ + withXcodeTarget, + { + extensionName: EXTENSION_NAME, + controllerName: EXTENSION_CONTROLLER_NAME, + soundFiles, + }, + ], + ]) +} + +module.exports = withNotificationsExtension diff --git a/plugins/notificationsExtension/withSounds.js b/plugins/notificationsExtension/withSounds.js new file mode 100644 index 00000000..652afd54 --- /dev/null +++ b/plugins/notificationsExtension/withSounds.js @@ -0,0 +1,27 @@ +const {withXcodeProject} = require('@expo/config-plugins') +const path = require('path') +const fs = require('fs') + +const withSounds = (config, {extensionName, soundFiles}) => { + // eslint-disable-next-line no-shadow + return withXcodeProject(config, config => { + for (const file of soundFiles) { + const soundPath = path.join(config.modRequest.projectRoot, 'assets', file) + + const targetPath = path.join( + config.modRequest.platformProjectRoot, + extensionName, + file, + ) + + if (!fs.existsSync(path.dirname(targetPath))) { + fs.mkdirSync(path.dirname(targetPath), {recursive: true}) + } + fs.copyFileSync(soundPath, targetPath) + } + + return config + }) +} + +module.exports = {withSounds} diff --git a/plugins/notificationsExtension/withXcodeTarget.js b/plugins/notificationsExtension/withXcodeTarget.js new file mode 100644 index 00000000..e9c7dae3 --- /dev/null +++ b/plugins/notificationsExtension/withXcodeTarget.js @@ -0,0 +1,76 @@ +const {withXcodeProject, IOSConfig} = require('@expo/config-plugins') +const path = require('path') +const PBXFile = require('xcode/lib/pbxFile') + +const withXcodeTarget = ( + config, + {extensionName, controllerName, soundFiles}, +) => { + // eslint-disable-next-line no-shadow + return withXcodeProject(config, config => { + let pbxProject = config.modResults + + const target = pbxProject.addTarget( + extensionName, + 'app_extension', + extensionName, + ) + pbxProject.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', target.uuid) + pbxProject.addBuildPhase( + [], + 'PBXResourcesBuildPhase', + 'Resources', + target.uuid, + ) + const pbxGroupKey = pbxProject.pbxCreateGroup(extensionName, extensionName) + pbxProject.addFile(`${extensionName}/Info.plist`, pbxGroupKey) + pbxProject.addSourceFile( + `${extensionName}/${controllerName}.swift`, + {target: target.uuid}, + pbxGroupKey, + ) + + for (const file of soundFiles) { + pbxProject.addSourceFile( + `${extensionName}/${file}`, + {target: target.uuid}, + pbxGroupKey, + ) + } + + var configurations = pbxProject.pbxXCBuildConfigurationSection() + for (var key in configurations) { + if (typeof configurations[key].buildSettings !== 'undefined') { + var buildSettingsObj = configurations[key].buildSettings + if ( + typeof buildSettingsObj.PRODUCT_NAME !== 'undefined' && + buildSettingsObj.PRODUCT_NAME === `"${extensionName}"` + ) { + buildSettingsObj.CLANG_ENABLE_MODULES = 'YES' + buildSettingsObj.INFOPLIST_FILE = `"${extensionName}/Info.plist"` + buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${extensionName}/${extensionName}.entitlements"` + buildSettingsObj.CODE_SIGN_STYLE = 'Automatic' + buildSettingsObj.CURRENT_PROJECT_VERSION = `"${config.ios?.buildNumber}"` + buildSettingsObj.GENERATE_INFOPLIST_FILE = 'YES' + buildSettingsObj.MARKETING_VERSION = `"${config.version}"` + buildSettingsObj.PRODUCT_BUNDLE_IDENTIFIER = `"${config.ios?.bundleIdentifier}.${extensionName}"` + buildSettingsObj.SWIFT_EMIT_LOC_STRINGS = 'YES' + buildSettingsObj.SWIFT_VERSION = '5.0' + buildSettingsObj.TARGETED_DEVICE_FAMILY = `"1,2"` + buildSettingsObj.DEVELOPMENT_TEAM = 'B3LX46C5HS' + } + } + } + + pbxProject.addTargetAttribute( + 'DevelopmentTeam', + 'B3LX46C5HS', + extensionName, + ) + pbxProject.addTargetAttribute('DevelopmentTeam', 'B3LX46C5HS') + + return config + }) +} + +module.exports = {withXcodeTarget} diff --git a/scripts/updateExtensions.sh b/scripts/updateExtensions.sh index f4e462b7..f3e972aa 100755 --- a/scripts/updateExtensions.sh +++ b/scripts/updateExtensions.sh @@ -1,5 +1,6 @@ #!/bin/bash IOS_SHARE_EXTENSION_DIRECTORY="./ios/Share-with-Bluesky" +IOS_NOTIFICATION_EXTENSION_DIRECTORY="./ios/BlueskyNSE" MODULES_DIRECTORY="./modules" if [ ! -d $IOS_SHARE_EXTENSION_DIRECTORY ]; then @@ -8,3 +9,10 @@ if [ ! -d $IOS_SHARE_EXTENSION_DIRECTORY ]; then else cp -R $IOS_SHARE_EXTENSION_DIRECTORY $MODULES_DIRECTORY fi + +if [ ! -d $IOS_NOTIFICATION_EXTENSION_DIRECTORY ]; then + echo "$IOS_NOTIFICATION_EXTENSION_DIRECTORY not found inside of your iOS project." + exit 1 +else + cp -R $IOS_NOTIFICATION_EXTENSION_DIRECTORY $MODULES_DIRECTORY +fi diff --git a/src/App.native.tsx b/src/App.native.tsx index 9356be7a..425d6ac6 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -47,6 +47,7 @@ import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {Provider as PortalProvider} from '#/components/Portal' import {Splash} from '#/Splash' +import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import I18nProvider from './locale/i18nProvider' import {listenSessionDropped} from './state/events' @@ -102,10 +103,12 @@ function InnerApp() { - - - - + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 40ceb694..900ceefd 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -39,6 +39,7 @@ import {Shell} from 'view/shell/index' import {ThemeProvider as Alf} from '#/alf' import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {Provider as PortalProvider} from '#/components/Portal' +import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import I18nProvider from './locale/i18nProvider' import {listenSessionDropped} from './state/events' @@ -92,9 +93,11 @@ function InnerApp() { - - - + + + + + diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 3240a485..6f5fbd66 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -8,6 +8,7 @@ import {track} from 'lib/analytics/analytics' import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher' import {NavigationProp} from 'lib/routes/types' import {logEvent} from 'lib/statsig/statsig' +import {isAndroid} from 'platform/detection' import {useCurrentConvoId} from 'state/messages/current-convo-id' import {RQKEY as RQKEY_NOTIFS} from 'state/queries/notifications/feed' import {invalidateCachedUnreadPage} from 'state/queries/notifications/unread' @@ -40,7 +41,7 @@ type NotificationPayload = } const DEFAULT_HANDLER_OPTIONS = { - shouldShowAlert: false, + shouldShowAlert: true, shouldPlaySound: false, shouldSetBadge: true, } @@ -60,6 +61,28 @@ export function useNotificationsHandler() { // Safety to prevent double handling of the same notification const prevDate = React.useRef(0) + React.useEffect(() => { + if (!isAndroid) return + + Notifications.setNotificationChannelAsync('chat-messages', { + name: 'Chat', + importance: Notifications.AndroidImportance.MAX, + sound: 'dm.mp3', + showBadge: true, + vibrationPattern: [250], + lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE, + }) + + Notifications.setNotificationChannelAsync('chat-messages-muted', { + name: 'Chat - Muted', + importance: Notifications.AndroidImportance.MAX, + sound: null, + showBadge: true, + vibrationPattern: [250], + lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE, + }) + }, []) + React.useEffect(() => { const handleNotification = (payload?: NotificationPayload) => { if (!payload) return diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx index e4fff125..7ad21b40 100644 --- a/src/screens/Messages/Settings.tsx +++ b/src/screens/Messages/Settings.tsx @@ -15,8 +15,10 @@ import * as Toast from '#/view/com/util/Toast' import {ViewHeader} from '#/view/com/util/ViewHeader' import {CenteredView} from '#/view/com/util/Views' import {atoms as a} from '#/alf' +import * as Toggle from '#/components/forms/Toggle' import {RadioGroup} from '#/components/RadioGroup' import {Text} from '#/components/Typography' +import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {ClipClopGate} from './gate' type AllowIncoming = 'all' | 'none' | 'following' @@ -28,6 +30,7 @@ export function MessagesSettingsScreen({}: Props) { const {data: profile} = useProfileQuery({ did: currentAccount!.did, }) as UseQueryResult + const {preferences, setPref} = useBackgroundNotificationPreferences() const {mutate: updateDeclaration} = useUpdateActorDeclaration({ onError: () => { @@ -65,6 +68,18 @@ export function MessagesSettingsScreen({}: Props) { onSelect={onSelectItem} /> + + { + setPref('playSoundChat', !preferences.playSoundChat) + }}> + + Notification Sounds + + ) }