diff options
author | Hailey <me@haileyok.com> | 2024-07-11 18:37:43 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-11 18:37:43 -0700 |
commit | 83e8522e0a89be28b1733f4c50dbd4379d98d03b (patch) | |
tree | c51a1054ffa8f1b226412a77fa7d69f5c891f7ae /modules | |
parent | 2397104ad6169ced02b1acd9fbbbe426f4cc4da0 (diff) | |
download | voidsky-83e8522e0a89be28b1733f4c50dbd4379d98d03b.tar.zst |
Create shared preferences API (#4654)
Diffstat (limited to 'modules')
12 files changed, 552 insertions, 72 deletions
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt deleted file mode 100644 index 51f9fe45d..000000000 --- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt +++ /dev/null @@ -1,11 +0,0 @@ -package expo.modules.blueskyswissarmy.deviceprefs - -import expo.modules.kotlin.modules.Module -import expo.modules.kotlin.modules.ModuleDefinition - -class ExpoBlueskyDevicePrefsModule : Module() { - override fun definition() = - ModuleDefinition { - Name("ExpoBlueskyDevicePrefs") - } -} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt new file mode 100644 index 000000000..271956878 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/ExpoBlueskySharedPrefsModule.kt @@ -0,0 +1,69 @@ +package expo.modules.blueskyswissarmy.sharedprefs + +import android.content.Context +import android.util.Log +import expo.modules.kotlin.jni.JavaScriptValue +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBlueskySharedPrefsModule : Module() { + private fun getContext(): Context { + val context = appContext.reactContext ?: throw Error("Context is null") + return context + } + + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskySharedPrefs") + + Function("setString") { key: String, value: String -> + return@Function SharedPrefs(getContext()).setValue(key, value) + } + + Function("setValue") { key: String, value: JavaScriptValue -> + val context = getContext() + Log.d("ExpoBlueskySharedPrefs", "Setting value for key: $key") + try { + if (value.isNumber()) { + SharedPrefs(context).setValue(key, value.getFloat()) + } else if (value.isBool()) { + SharedPrefs(context).setValue(key, value.getBool()) + } else if (value.isNull() || value.isUndefined()) { + SharedPrefs(context).removeValue(key) + } else { + Log.d(NAME, "Unsupported type: ${value.kind()}") + } + } catch (e: Error) { + Log.d(NAME, "Error setting value: $e") + } + } + + Function("removeValue") { key: String -> + return@Function SharedPrefs(getContext()).removeValue(key) + } + + Function("getString") { key: String -> + return@Function SharedPrefs(getContext()).getString(key) + } + + Function("getNumber") { key: String -> + return@Function SharedPrefs(getContext()).getFloat(key) + } + + Function("getBool") { key: String -> + return@Function SharedPrefs(getContext()).getBoolean(key) + } + + Function("addToSet") { key: String, value: String -> + return@Function SharedPrefs(getContext()).addToSet(key, value) + } + + Function("removeFromSet") { key: String, value: String -> + return@Function SharedPrefs(getContext()).removeFromSet(key, value) + } + + Function("setContains") { key: String, value: String -> + return@Function SharedPrefs(getContext()).setContains(key, value) + } + } +} diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt new file mode 100644 index 000000000..38d79abf1 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/sharedprefs/SharedPrefs.kt @@ -0,0 +1,241 @@ +package expo.modules.blueskyswissarmy.sharedprefs + +import android.content.Context +import android.content.SharedPreferences +import android.util.Log + +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, + "badgeCount" to 0, + ) + +const val NAME = "SharedPrefs" + +class SharedPrefs( + private val context: Context, +) { + companion object { + private var hasInitialized = false + + private var instance: SharedPreferences? = null + + fun getInstance( + context: Context, + info: String? = "(no info)", + ): SharedPreferences { + if (instance == null) { + Log.d(NAME, "No preferences instance found, creating one.") + instance = context.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) + } + + val safeInstance = instance ?: throw Error("Preferences is null: $info") + + if (!hasInitialized) { + Log.d(NAME, "Preferences instance has not been initialized yet.") + initialize(safeInstance) + hasInitialized = true + Log.d(NAME, "Preferences instance has been initialized.") + } + + return safeInstance + } + + private fun initialize(instance: SharedPreferences) { + instance + .edit() + .apply { + DEFAULTS.forEach { (key, value) -> + if (instance.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 setValue( + key: String, + value: String, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + putString(key, value) + }.apply() + } + + fun setValue( + key: String, + value: Float, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + putFloat(key, value) + }.apply() + } + + fun setValue( + key: String, + value: Boolean, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + putBoolean(key, value) + }.apply() + } + + fun setValue( + key: String, + value: Set<String>, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + putStringSet(key, value) + }.apply() + } + + fun removeValue(key: String) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + remove(key) + }.apply() + } + + fun getString(key: String): String? { + val safeInstance = getInstance(context) + return safeInstance.getString(key, null) + } + + fun getFloat(key: String): Float? { + val safeInstance = getInstance(context) + if (!safeInstance.contains(key)) { + return null + } + return safeInstance.getFloat(key, 0.0f) + } + + @Suppress("ktlint:standard:function-naming") + fun _setAnyValue( + key: String, + value: Any, + ) { + val safeInstance = getInstance(context) + safeInstance + .edit() + .apply { + when (value) { + is String -> putString(key, value) + is Float -> putFloat(key, value) + is Boolean -> putBoolean(key, value) + is Set<*> -> putStringSet(key, value.map { it.toString() }.toSet()) + else -> throw Error("Unsupported type: ${value::class.java}") + } + }.apply() + } + + fun getBoolean(key: String): Boolean? { + val safeInstance = getInstance(context) + if (!safeInstance.contains(key)) { + return null + } + Log.d(NAME, "Getting boolean for key: $key") + val res = safeInstance.getBoolean(key, false) + Log.d(NAME, "Got boolean for key: $key, value: $res") + return res + } + + fun addToSet( + key: String, + value: String, + ) { + val safeInstance = getInstance(context) + val set = safeInstance.getStringSet(key, setOf()) ?: setOf() + val newSet = + set.toMutableSet().apply { + add(value) + } + safeInstance + .edit() + .apply { + putStringSet(key, newSet) + }.apply() + } + + fun removeFromSet( + key: String, + value: String, + ) { + val safeInstance = getInstance(context) + val set = safeInstance.getStringSet(key, setOf()) ?: setOf() + val newSet = + set.toMutableSet().apply { + remove(value) + } + safeInstance + .edit() + .apply { + putStringSet(key, newSet) + }.apply() + } + + fun setContains( + key: String, + value: String, + ): Boolean { + val safeInstance = getInstance(context) + val set = safeInstance.getStringSet(key, setOf()) ?: setOf() + return set.contains(value) + } + + fun hasValue(key: String): Boolean { + val safeInstance = getInstance(context) + return safeInstance.contains(key) + } + + fun getValues(keys: Set<String>): Map<String, Any?> { + val safeInstance = getInstance(context) + return keys.associateWith { key -> + when (val value = safeInstance.all[key]) { + is String -> value + is Float -> value + is Boolean -> value + is Set<*> -> value + else -> null + } + } + } +} diff --git a/modules/expo-bluesky-swiss-army/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json index 730bc6114..1111f8a0b 100644 --- a/modules/expo-bluesky-swiss-army/expo-module.config.json +++ b/modules/expo-bluesky-swiss-army/expo-module.config.json @@ -1,11 +1,11 @@ { "platforms": ["ios", "tvos", "android", "web"], "ios": { - "modules": ["ExpoBlueskyDevicePrefsModule", "ExpoBlueskyReferrerModule"] + "modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule"] }, "android": { "modules": [ - "expo.modules.blueskyswissarmy.deviceprefs.ExpoBlueskyDevicePrefsModule", + "expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule", "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule" ] } diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts index 1b2f89249..89cea00a2 100644 --- a/modules/expo-bluesky-swiss-army/index.ts +++ b/modules/expo-bluesky-swiss-army/index.ts @@ -1,4 +1,4 @@ -import * as DevicePrefs from './src/DevicePrefs' import * as Referrer from './src/Referrer' +import * as SharedPrefs from './src/SharedPrefs' -export {DevicePrefs, Referrer} +export {Referrer, SharedPrefs} diff --git a/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift deleted file mode 100644 index b13a9fe3f..000000000 --- a/modules/expo-bluesky-swiss-army/ios/DevicePrefs/ExpoBlueskyDevicePrefsModule.swift +++ /dev/null @@ -1,23 +0,0 @@ -import ExpoModulesCore - -public class ExpoBlueskyDevicePrefsModule: Module { - func getDefaults(_ useAppGroup: Bool) -> UserDefaults? { - if useAppGroup { - return UserDefaults(suiteName: "group.app.bsky") - } else { - return UserDefaults.standard - } - } - - public func definition() -> ModuleDefinition { - Name("ExpoBlueskyDevicePrefs") - - AsyncFunction("getStringValueAsync") { (key: String, useAppGroup: Bool) in - return self.getDefaults(useAppGroup)?.string(forKey: key) - } - - AsyncFunction("setStringValueAsync") { (key: String, value: String?, useAppGroup: Bool) in - self.getDefaults(useAppGroup)?.setValue(value, forKey: key) - } - } -} diff --git a/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift new file mode 100644 index 000000000..8549e5b48 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/ExpoBlueskySharedPrefsModule.swift @@ -0,0 +1,62 @@ +import Foundation +import ExpoModulesCore + +public class ExpoBlueskySharedPrefsModule: Module { + let defaults = UserDefaults(suiteName: "group.app.bsky") + + func getDefaults(_ info: String = "(no info)") -> UserDefaults? { + guard let defaults = self.defaults else { + NSLog("Failed to get defaults for app group: \(info)") + return nil + } + return defaults + } + + public func definition() -> ModuleDefinition { + Name("ExpoBlueskySharedPrefs") + + // JavaScripValue causes a crash when trying to check `isString()`. Let's + // explicitly define setString instead. + Function("setString") { (key: String, value: String?) in + SharedPrefs.shared.setValue(key, value) + } + + Function("setValue") { (key: String, value: JavaScriptValue) in + if value.isNumber() { + SharedPrefs.shared.setValue(key, value.getDouble()) + } else if value.isBool() { + SharedPrefs.shared.setValue(key, value.getBool()) + } else if value.isNull() || value.isUndefined() { + SharedPrefs.shared.removeValue(key) + } + } + + Function("removeValue") { (key: String) in + SharedPrefs.shared.removeValue(key) + } + + Function("getString") { (key: String) in + return SharedPrefs.shared.getString(key) + } + + Function("getBool") { (key: String) in + return SharedPrefs.shared.getBool(key) + } + + Function("getNumber") { (key: String) in + return SharedPrefs.shared.getNumber(key) + } + + Function("addToSet") { (key: String, value: String) in + SharedPrefs.shared.addToSet(key, value) + } + + Function("removeFromSet") { (key: String, value: String) in + SharedPrefs.shared.removeFromSet(key, value) + } + + Function("setContains") { (key: String, value: String) in + return SharedPrefs.shared.setContains(key, value) + } + } +} diff --git a/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift new file mode 100644 index 000000000..a11a71834 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/ios/SharedPrefs/SharedPrefs.swift @@ -0,0 +1,89 @@ +import Foundation + +public class SharedPrefs { + public static let shared = SharedPrefs() + + private let defaults = UserDefaults(suiteName: "group.app.bsky") + + init() { + if defaults == nil { + NSLog("Failed to get user defaults for app group.") + } + } + + private func getDefaults(_ info: String = "(no info)") -> UserDefaults? { + guard let defaults = self.defaults else { + NSLog("Failed to get defaults for app group: \(info)") + return nil + } + return defaults + } + + public func setValue(_ key: String, _ value: String?) { + getDefaults(key)?.setValue(value, forKey: key) + } + + public func setValue(_ key: String, _ value: Double?) { + getDefaults(key)?.setValue(value, forKey: key) + } + + public func setValue(_ key: String, _ value: Bool?) { + getDefaults(key)?.setValue(value, forKey: key) + } + + public func _setAnyValue(_ key: String, _ value: Any?) { + getDefaults(key)?.setValue(value, forKey: key) + } + + public func removeValue(_ key: String) { + getDefaults(key)?.removeObject(forKey: key) + } + + public func getString(_ key: String) -> String? { + return getDefaults(key)?.string(forKey: key) + } + + public func getNumber(_ key: String) -> Double? { + return getDefaults(key)?.double(forKey: key) + } + + public func getBool(_ key: String) -> Bool? { + return getDefaults(key)?.bool(forKey: key) + } + + public func addToSet(_ key: String, _ value: String) { + var dict: [String: Bool]? + if var currDict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] { + currDict[value] = true + dict = currDict + } else { + dict = [ + value: true + ] + } + getDefaults(key)?.setValue(dict, forKey: key) + } + + public func removeFromSet(_ key: String, _ value: String) { + guard var dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else { + return + } + dict.removeValue(forKey: value) + getDefaults(key)?.setValue(dict, forKey: key) + } + + public func setContains(_ key: String, _ value: String) -> Bool { + guard let dict = getDefaults(key)?.dictionary(forKey: key) as? [String: Bool] else { + return false + } + return dict[value] == true + } + + public func hasValue(_ key: String) -> Bool { + return getDefaults(key)?.value(forKey: key) != nil + } + + public func getValues(_ keys: [String]) -> [String: Any?]? { + return getDefaults("keys:\(keys)")?.dictionaryWithValues(forKeys: keys) + } +} diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts deleted file mode 100644 index 427185086..000000000 --- a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ios.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {requireNativeModule} from 'expo-modules-core' - -const NativeModule = requireNativeModule('ExpoBlueskyDevicePrefs') - -export function getStringValueAsync( - key: string, - useAppGroup?: boolean, -): Promise<string | null> { - return NativeModule.getStringValueAsync(key, useAppGroup) -} - -export function setStringValueAsync( - key: string, - value: string | null, - useAppGroup?: boolean, -): Promise<void> { - return NativeModule.setStringValueAsync(key, value, useAppGroup) -} diff --git a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts b/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts deleted file mode 100644 index f1eee6c28..000000000 --- a/modules/expo-bluesky-swiss-army/src/DevicePrefs/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {NotImplementedError} from '../NotImplemented' - -export function getStringValueAsync( - key: string, - useAppGroup?: boolean, -): Promise<string | null> { - throw new NotImplementedError({key, useAppGroup}) -} - -export function setStringValueAsync( - key: string, - value: string | null, - useAppGroup?: boolean, -): Promise<string | null> { - throw new NotImplementedError({key, value, useAppGroup}) -} diff --git a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts new file mode 100644 index 000000000..0cea7c53b --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.native.ts @@ -0,0 +1,51 @@ +import {requireNativeModule} from 'expo-modules-core' + +const NativeModule = requireNativeModule('ExpoBlueskySharedPrefs') + +export function setValue( + key: string, + value: string | number | boolean | null | undefined, +): void { + // A bug on Android causes `JavaScripValue.isString()` to cause a crash on some occasions, seemingly because of a + // memory violation. Instead, we will use a specific function to set strings on this platform. + if (typeof value === 'string') { + return NativeModule.setString(key, value) + } + return NativeModule.setValue(key, value) +} + +export function removeValue(key: string): void { + return NativeModule.removeValue(key) +} + +export function getString(key: string): string | undefined { + return nullToUndefined(NativeModule.getString(key)) +} + +export function getNumber(key: string): number | undefined { + return nullToUndefined(NativeModule.getNumber(key)) +} + +export function getBool(key: string): boolean | undefined { + return nullToUndefined(NativeModule.getBool(key)) +} + +export function addToSet(key: string, value: string): void { + return NativeModule.addToSet(key, value) +} + +export function removeFromSet(key: string, value: string): void { + return NativeModule.removeFromSet(key, value) +} + +export function setContains(key: string, value: string): boolean { + return NativeModule.setContains(key, value) +} + +// iOS returns `null` if a value does not exist, and Android returns `undefined. Normalize these here for JS types +function nullToUndefined(value: any) { + if (value == null) { + return undefined + } + return value +} diff --git a/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts new file mode 100644 index 000000000..769344007 --- /dev/null +++ b/modules/expo-bluesky-swiss-army/src/SharedPrefs/index.ts @@ -0,0 +1,36 @@ +import {NotImplementedError} from '../NotImplemented' + +export function setValue( + key: string, + value: string | number | boolean | null | undefined, +): void { + throw new NotImplementedError({key, value}) +} + +export function removeValue(key: string): void { + throw new NotImplementedError({key}) +} + +export function getString(key: string): string | null { + throw new NotImplementedError({key}) +} + +export function getNumber(key: string): number | null { + throw new NotImplementedError({key}) +} + +export function getBool(key: string): boolean | null { + throw new NotImplementedError({key}) +} + +export function addToSet(key: string, value: string): void { + throw new NotImplementedError({key, value}) +} + +export function removeFromSet(key: string, value: string): void { + throw new NotImplementedError({key, value}) +} + +export function setContains(key: string, value: string): boolean { + throw new NotImplementedError({key, value}) +} |