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 * nitzio/stable
parent
31868b255f
commit
bf7b66d5c1
|
@ -110,7 +110,7 @@ module.exports = function (config) {
|
||||||
{
|
{
|
||||||
NSPrivacyAccessedAPIType:
|
NSPrivacyAccessedAPIType:
|
||||||
'NSPrivacyAccessedAPICategoryUserDefaults',
|
'NSPrivacyAccessedAPICategoryUserDefaults',
|
||||||
NSPrivacyAccessedAPITypeReasons: ['CA92.1'],
|
NSPrivacyAccessedAPITypeReasons: ['CA92.1', '1C8F.1'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -200,7 +200,7 @@ module.exports = function (config) {
|
||||||
{
|
{
|
||||||
icon: './assets/icon-android-notification.png',
|
icon: './assets/icon-android-notification.png',
|
||||||
color: '#1185fe',
|
color: '#1185fe',
|
||||||
sounds: ['assets/blueskydm.wav'],
|
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'./plugins/withAndroidManifestPlugin.js',
|
'./plugins/withAndroidManifestPlugin.js',
|
||||||
|
@ -209,6 +209,7 @@ module.exports = function (config) {
|
||||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||||
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
'./plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js',
|
||||||
'./plugins/shareExtension/withShareExtensions.js',
|
'./plugins/shareExtension/withShareExtensions.js',
|
||||||
|
'./plugins/notificationsExtension/withNotificationsExtension.js',
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
extra: {
|
extra: {
|
||||||
eas: {
|
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',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.app.bsky</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.usernotifications.service</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||||
|
</dict>
|
||||||
|
<key>MainAppScheme</key>
|
||||||
|
<string>bluesky</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Bluesky Notifications</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -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"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -38,4 +38,4 @@
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
|
@ -7,4 +7,4 @@
|
||||||
<string>group.app.bsky</string>
|
<string>group.app.bsky</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
|
@ -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'
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
<manifest>
|
||||||
|
</manifest>
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package expo.modules.backgroundnotificationhandler
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
|
||||||
|
interface BackgroundNotificationHandlerInterface {
|
||||||
|
fun showMessage(remoteMessage: RemoteMessage)
|
||||||
|
}
|
|
@ -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<String> ->
|
||||||
|
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<String> ->
|
||||||
|
NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
|
||||||
|
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package expo.modules.backgroundnotificationhandler
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
val DEFAULTS = mapOf<String, Any>(
|
||||||
|
"playSoundChat" to true,
|
||||||
|
"playSoundFollow" to false,
|
||||||
|
"playSoundLike" to false,
|
||||||
|
"playSoundMention" to false,
|
||||||
|
"playSoundQuote" to false,
|
||||||
|
"playSoundReply" to false,
|
||||||
|
"playSoundRepost" to false,
|
||||||
|
"mutedThreads" to mapOf<String, List<String>>()
|
||||||
|
)
|
||||||
|
|
||||||
|
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<String, *> {
|
||||||
|
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<String>? {
|
||||||
|
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<String>) {
|
||||||
|
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<String>) {
|
||||||
|
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<String>) {
|
||||||
|
prefs
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
|
||||||
|
set.removeAll(strings.toSet())
|
||||||
|
putStringSet(key, set)
|
||||||
|
}
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"platforms": ["ios", "android"],
|
||||||
|
"ios": {
|
||||||
|
"modules": ["ExpoBackgroundNotificationHandlerModule"]
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"modules": ["expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
import {BackgroundNotificationHandler} from './src/ExpoBackgroundNotificationHandlerModule'
|
||||||
|
export default BackgroundNotificationHandler
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {BackgroundNotificationHandlerPreferences} from './ExpoBackgroundNotificationHandler.types'
|
||||||
|
import {BackgroundNotificationHandler} from './ExpoBackgroundNotificationHandlerModule'
|
||||||
|
|
||||||
|
interface BackgroundNotificationPreferencesContext {
|
||||||
|
preferences: BackgroundNotificationHandlerPreferences
|
||||||
|
setPref: <Key extends keyof BackgroundNotificationHandlerPreferences>(
|
||||||
|
key: Key,
|
||||||
|
value: BackgroundNotificationHandlerPreferences[Key],
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = React.createContext<BackgroundNotificationPreferencesContext>(
|
||||||
|
{} as BackgroundNotificationPreferencesContext,
|
||||||
|
)
|
||||||
|
export const useBackgroundNotificationPreferences = () =>
|
||||||
|
React.useContext(Context)
|
||||||
|
|
||||||
|
export function BackgroundNotificationPreferencesProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const [preferences, setPreferences] =
|
||||||
|
React.useState<BackgroundNotificationHandlerPreferences>({
|
||||||
|
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 <Context.Provider value={value}>{children}</Context.Provider>
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
export type ExpoBackgroundNotificationHandlerModule = {
|
||||||
|
getAllPrefsAsync: () => Promise<BackgroundNotificationHandlerPreferences>
|
||||||
|
getBoolAsync: (forKey: string) => Promise<boolean>
|
||||||
|
getStringAsync: (forKey: string) => Promise<string>
|
||||||
|
getStringArrayAsync: (forKey: string) => Promise<string[]>
|
||||||
|
setBoolAsync: (
|
||||||
|
forKey: keyof BackgroundNotificationHandlerPreferences,
|
||||||
|
value: boolean,
|
||||||
|
) => Promise<void>
|
||||||
|
setStringAsync: (
|
||||||
|
forKey: keyof BackgroundNotificationHandlerPreferences,
|
||||||
|
value: string,
|
||||||
|
) => Promise<void>
|
||||||
|
setStringArrayAsync: (
|
||||||
|
forKey: keyof BackgroundNotificationHandlerPreferences,
|
||||||
|
value: string[],
|
||||||
|
) => Promise<void>
|
||||||
|
addToStringArrayAsync: (
|
||||||
|
forKey: keyof BackgroundNotificationHandlerPreferences,
|
||||||
|
value: string,
|
||||||
|
) => Promise<void>
|
||||||
|
removeFromStringArrayAsync: (
|
||||||
|
forKey: keyof BackgroundNotificationHandlerPreferences,
|
||||||
|
value: string,
|
||||||
|
) => Promise<void>
|
||||||
|
addManyToStringArrayAsync: (
|
||||||
|
forKey: keyof BackgroundNotificationHandlerPreferences,
|
||||||
|
value: string[],
|
||||||
|
) => Promise<void>
|
||||||
|
removeManyFromStringArrayAsync: (
|
||||||
|
forKey: keyof BackgroundNotificationHandlerPreferences,
|
||||||
|
value: string[],
|
||||||
|
) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import {requireNativeModule} from 'expo-modules-core'
|
||||||
|
|
||||||
|
import {ExpoBackgroundNotificationHandlerModule} from './ExpoBackgroundNotificationHandler.types'
|
||||||
|
|
||||||
|
export const BackgroundNotificationHandler =
|
||||||
|
requireNativeModule<ExpoBackgroundNotificationHandlerModule>(
|
||||||
|
'ExpoBackgroundNotificationHandler',
|
||||||
|
)
|
|
@ -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
|
|
@ -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<*, *>)
|
|
@ -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.
|
|
@ -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.<identifier>
|
||||||
|
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
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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}
|
|
@ -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
|
|
@ -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}
|
|
@ -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}
|
|
@ -1,5 +1,6 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
IOS_SHARE_EXTENSION_DIRECTORY="./ios/Share-with-Bluesky"
|
IOS_SHARE_EXTENSION_DIRECTORY="./ios/Share-with-Bluesky"
|
||||||
|
IOS_NOTIFICATION_EXTENSION_DIRECTORY="./ios/BlueskyNSE"
|
||||||
MODULES_DIRECTORY="./modules"
|
MODULES_DIRECTORY="./modules"
|
||||||
|
|
||||||
if [ ! -d $IOS_SHARE_EXTENSION_DIRECTORY ]; then
|
if [ ! -d $IOS_SHARE_EXTENSION_DIRECTORY ]; then
|
||||||
|
@ -8,3 +9,10 @@ if [ ! -d $IOS_SHARE_EXTENSION_DIRECTORY ]; then
|
||||||
else
|
else
|
||||||
cp -R $IOS_SHARE_EXTENSION_DIRECTORY $MODULES_DIRECTORY
|
cp -R $IOS_SHARE_EXTENSION_DIRECTORY $MODULES_DIRECTORY
|
||||||
fi
|
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
|
||||||
|
|
|
@ -47,6 +47,7 @@ import {ThemeProvider as Alf} from '#/alf'
|
||||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
import {Splash} from '#/Splash'
|
import {Splash} from '#/Splash'
|
||||||
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import I18nProvider from './locale/i18nProvider'
|
import I18nProvider from './locale/i18nProvider'
|
||||||
import {listenSessionDropped} from './state/events'
|
import {listenSessionDropped} from './state/events'
|
||||||
|
|
||||||
|
@ -102,10 +103,12 @@ function InnerApp() {
|
||||||
<LoggedOutViewProvider>
|
<LoggedOutViewProvider>
|
||||||
<SelectedFeedProvider>
|
<SelectedFeedProvider>
|
||||||
<UnreadNotifsProvider>
|
<UnreadNotifsProvider>
|
||||||
<GestureHandlerRootView style={s.h100pct}>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<TestCtrls />
|
<GestureHandlerRootView style={s.h100pct}>
|
||||||
<Shell />
|
<TestCtrls />
|
||||||
</GestureHandlerRootView>
|
<Shell />
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</UnreadNotifsProvider>
|
</UnreadNotifsProvider>
|
||||||
</SelectedFeedProvider>
|
</SelectedFeedProvider>
|
||||||
</LoggedOutViewProvider>
|
</LoggedOutViewProvider>
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {Shell} from 'view/shell/index'
|
||||||
import {ThemeProvider as Alf} from '#/alf'
|
import {ThemeProvider as Alf} from '#/alf'
|
||||||
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
|
||||||
import {Provider as PortalProvider} from '#/components/Portal'
|
import {Provider as PortalProvider} from '#/components/Portal'
|
||||||
|
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import I18nProvider from './locale/i18nProvider'
|
import I18nProvider from './locale/i18nProvider'
|
||||||
import {listenSessionDropped} from './state/events'
|
import {listenSessionDropped} from './state/events'
|
||||||
|
|
||||||
|
@ -92,9 +93,11 @@ function InnerApp() {
|
||||||
<LoggedOutViewProvider>
|
<LoggedOutViewProvider>
|
||||||
<SelectedFeedProvider>
|
<SelectedFeedProvider>
|
||||||
<UnreadNotifsProvider>
|
<UnreadNotifsProvider>
|
||||||
<SafeAreaProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<Shell />
|
<SafeAreaProvider>
|
||||||
</SafeAreaProvider>
|
<Shell />
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</BackgroundNotificationPreferencesProvider>
|
||||||
</UnreadNotifsProvider>
|
</UnreadNotifsProvider>
|
||||||
</SelectedFeedProvider>
|
</SelectedFeedProvider>
|
||||||
</LoggedOutViewProvider>
|
</LoggedOutViewProvider>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {track} from 'lib/analytics/analytics'
|
||||||
import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
import {logEvent} from 'lib/statsig/statsig'
|
||||||
|
import {isAndroid} from 'platform/detection'
|
||||||
import {useCurrentConvoId} from 'state/messages/current-convo-id'
|
import {useCurrentConvoId} from 'state/messages/current-convo-id'
|
||||||
import {RQKEY as RQKEY_NOTIFS} from 'state/queries/notifications/feed'
|
import {RQKEY as RQKEY_NOTIFS} from 'state/queries/notifications/feed'
|
||||||
import {invalidateCachedUnreadPage} from 'state/queries/notifications/unread'
|
import {invalidateCachedUnreadPage} from 'state/queries/notifications/unread'
|
||||||
|
@ -40,7 +41,7 @@ type NotificationPayload =
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_HANDLER_OPTIONS = {
|
const DEFAULT_HANDLER_OPTIONS = {
|
||||||
shouldShowAlert: false,
|
shouldShowAlert: true,
|
||||||
shouldPlaySound: false,
|
shouldPlaySound: false,
|
||||||
shouldSetBadge: true,
|
shouldSetBadge: true,
|
||||||
}
|
}
|
||||||
|
@ -60,6 +61,28 @@ export function useNotificationsHandler() {
|
||||||
// Safety to prevent double handling of the same notification
|
// Safety to prevent double handling of the same notification
|
||||||
const prevDate = React.useRef(0)
|
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(() => {
|
React.useEffect(() => {
|
||||||
const handleNotification = (payload?: NotificationPayload) => {
|
const handleNotification = (payload?: NotificationPayload) => {
|
||||||
if (!payload) return
|
if (!payload) return
|
||||||
|
|
|
@ -15,8 +15,10 @@ import * as Toast from '#/view/com/util/Toast'
|
||||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {RadioGroup} from '#/components/RadioGroup'
|
import {RadioGroup} from '#/components/RadioGroup'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {useBackgroundNotificationPreferences} from '../../../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||||
import {ClipClopGate} from './gate'
|
import {ClipClopGate} from './gate'
|
||||||
|
|
||||||
type AllowIncoming = 'all' | 'none' | 'following'
|
type AllowIncoming = 'all' | 'none' | 'following'
|
||||||
|
@ -28,6 +30,7 @@ export function MessagesSettingsScreen({}: Props) {
|
||||||
const {data: profile} = useProfileQuery({
|
const {data: profile} = useProfileQuery({
|
||||||
did: currentAccount!.did,
|
did: currentAccount!.did,
|
||||||
}) as UseQueryResult<AppBskyActorDefs.ProfileViewDetailed, Error>
|
}) as UseQueryResult<AppBskyActorDefs.ProfileViewDetailed, Error>
|
||||||
|
const {preferences, setPref} = useBackgroundNotificationPreferences()
|
||||||
|
|
||||||
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
|
const {mutate: updateDeclaration} = useUpdateActorDeclaration({
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
@ -65,6 +68,18 @@ export function MessagesSettingsScreen({}: Props) {
|
||||||
onSelect={onSelectItem}
|
onSelect={onSelectItem}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={[a.px_md, a.py_lg, a.gap_md]}>
|
||||||
|
<Toggle.Item
|
||||||
|
name="a"
|
||||||
|
label="Click me"
|
||||||
|
value={preferences.playSoundChat}
|
||||||
|
onChange={() => {
|
||||||
|
setPref('playSoundChat', !preferences.playSoundChat)
|
||||||
|
}}>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.LabelText>Notification Sounds</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
</CenteredView>
|
</CenteredView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue