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
This commit is contained in:
parent
31868b255f
commit
bf7b66d5c1
38 changed files with 1297 additions and 12 deletions
10
modules/BlueskyNSE/BlueskyNSE.entitlements
Normal file
10
modules/BlueskyNSE/BlueskyNSE.entitlements
Normal file
|
@ -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>
|
29
modules/BlueskyNSE/Info.plist
Normal file
29
modules/BlueskyNSE/Info.plist
Normal file
|
@ -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>
|
51
modules/BlueskyNSE/NotificationService.swift
Normal file
51
modules/BlueskyNSE/NotificationService.swift
Normal file
|
@ -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>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
</dict>
|
||||
</plist>
|
||||
</plist>
|
|
@ -7,4 +7,4 @@
|
|||
<string>group.app.bsky</string>
|
||||
</array>
|
||||
</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"]
|
||||
}
|
||||
}
|
2
modules/expo-background-notification-handler/index.ts
Normal file
2
modules/expo-background-notification-handler/index.ts
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue