diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..32a53f18 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.{kt,kts}] +indent_size=2 diff --git a/modules/BlueskyNSE/NotificationService.swift b/modules/BlueskyNSE/NotificationService.swift index 384180d8..f863eaf2 100644 --- a/modules/BlueskyNSE/NotificationService.swift +++ b/modules/BlueskyNSE/NotificationService.swift @@ -13,43 +13,43 @@ class NotificationService: UNNotificationServiceExtension { contentHandler(request.content) return } - + if reason == "chat-message" { mutateWithChatMessage(bestAttempt) } else { 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) { var count = prefs?.integer(forKey: "badgeCount") ?? 0 count += 1 - + // Set the new badge number for the notification, then store that value for using later content.badge = NSNumber(value: count) prefs?.setValue(count, forKey: "badgeCount") } - + func mutateWithChatMessage(_ content: UNMutableNotificationContent) { if self.prefs?.bool(forKey: "playSoundChat") == true { mutateWithDmSound(content) } } - + func mutateWithDefaultSound(_ content: UNMutableNotificationContent) { content.sound = UNNotificationSound.default } - + func mutateWithDmSound(_ content: UNMutableNotificationContent) { content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) } diff --git a/modules/Share-with-Bluesky/ShareViewController.swift b/modules/Share-with-Bluesky/ShareViewController.swift index 4c1d635c..c045d578 100644 --- a/modules/Share-with-Bluesky/ShareViewController.swift +++ b/modules/Share-with-Bluesky/ShareViewController.swift @@ -30,12 +30,11 @@ class ShareViewController: UIViewController { } } - private func handleText(item: NSItemProvider) async -> Void { + private func handleText(item: NSItemProvider) async { do { if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String { if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), - let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") - { + let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") { _ = self.openURL(url) } } @@ -45,12 +44,11 @@ class ShareViewController: UIViewController { } } - private func handleUrl(item: NSItemProvider) async -> Void { + private func handleUrl(item: NSItemProvider) async { do { if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL { if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), - let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") - { + let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") { _ = self.openURL(url) } } @@ -60,7 +58,7 @@ class ShareViewController: UIViewController { } } - private func handleImages(items: [NSItemProvider]) async -> Void { + private func handleImages(items: [NSItemProvider]) async { let firstFourItems: [NSItemProvider] if items.count < 4 { firstFourItems = items @@ -72,7 +70,7 @@ class ShareViewController: UIViewController { var imageUris = "" for (index, item) in firstFourItems.enumerated() { - var imageUriInfo: String? = nil + var imageUriInfo: String? do { if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL { @@ -100,8 +98,7 @@ class ShareViewController: UIViewController { if valid, let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), - let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") - { + let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") { _ = self.openURL(url) } @@ -119,13 +116,11 @@ class ShareViewController: UIViewController { // extension does. if let dir = FileManager() .containerURL( - forSecurityApplicationGroupIdentifier: "group.app.bsky") - { + forSecurityApplicationGroupIdentifier: "group.app.bsky") { let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg" if let newUri = URL(string: filePath), - let jpegData = image.jpegData(compressionQuality: 1) - { + let jpegData = image.jpegData(compressionQuality: 1) { try jpegData.write(to: newUri) return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)" } @@ -136,7 +131,7 @@ class ShareViewController: UIViewController { } } - private func completeRequest() -> Void { + private func completeRequest() { self.extensionContext?.completeRequest(returningItems: nil) } diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt index 0a8737b8..7c1494c7 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt @@ -5,7 +5,7 @@ import com.google.firebase.messaging.RemoteMessage class BackgroundNotificationHandler( private val context: Context, - private val notifInterface: BackgroundNotificationHandlerInterface + private val notifInterface: BackgroundNotificationHandlerInterface, ) { fun handleMessage(remoteMessage: RemoteMessage) { if (ExpoBackgroundNotificationHandlerModule.isForegrounded) { diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt index c876f899..0cc5fa6a 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt @@ -8,67 +8,68 @@ class ExpoBackgroundNotificationHandlerModule : Module() { var isForegrounded = false } - override fun definition() = ModuleDefinition { - Name("ExpoBackgroundNotificationHandler") + override fun definition() = + ModuleDefinition { + Name("ExpoBackgroundNotificationHandler") - OnCreate { - NotificationPrefs(appContext.reactContext).initialize() - } + OnCreate { + NotificationPrefs(appContext.reactContext).initialize() + } - OnActivityEntersForeground { - isForegrounded = true - } + OnActivityEntersForeground { + isForegrounded = true + } - OnActivityEntersBackground { - isForegrounded = false - } + OnActivityEntersBackground { + isForegrounded = false + } - AsyncFunction("getAllPrefsAsync") { - return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs() - } + AsyncFunction("getAllPrefsAsync") { + return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs() + } - AsyncFunction("getBoolAsync") { forKey: String -> - return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey) - } + AsyncFunction("getBoolAsync") { forKey: String -> + return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey) + } - AsyncFunction("getStringAsync") { forKey: String -> - return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey) - } + AsyncFunction("getStringAsync") { forKey: String -> + return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey) + } - AsyncFunction("getStringArrayAsync") { forKey: String -> - return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(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("setBoolAsync") { forKey: String, value: Boolean -> + NotificationPrefs(appContext.reactContext).setBoolean(forKey, value) + } - AsyncFunction("setStringAsync") { forKey: String, value: String -> - NotificationPrefs(appContext.reactContext).setString(forKey, value) - } + AsyncFunction("setStringAsync") { forKey: String, value: String -> + NotificationPrefs(appContext.reactContext).setString(forKey, value) + } - AsyncFunction("setStringArrayAsync") { forKey: String, value: Array -> - NotificationPrefs(appContext.reactContext).setStringArray(forKey, value) - } + AsyncFunction("setStringArrayAsync") { forKey: String, value: Array -> + NotificationPrefs(appContext.reactContext).setStringArray(forKey, value) + } - AsyncFunction("addToStringArrayAsync") { forKey: String, string: String -> - NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string) - } + 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("removeFromStringArrayAsync") { forKey: String, string: String -> + NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string) + } - AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array -> - NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings) - } + AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array -> + NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings) + } - AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array -> - NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) - } + AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array -> + NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) + } - AsyncFunction("setBadgeCountAsync") { _: Int -> - // This does nothing on Android + AsyncFunction("setBadgeCountAsync") { _: Int -> + // This does nothing on Android + } } - } } diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt index 17ef9205..97b05b4d 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt @@ -2,20 +2,24 @@ package expo.modules.backgroundnotificationhandler import android.content.Context -val DEFAULTS = mapOf( - "playSoundChat" to true, - "playSoundFollow" to false, - "playSoundLike" to false, - "playSoundMention" to false, - "playSoundQuote" to false, - "playSoundReply" to false, - "playSoundRepost" to false, - "mutedThreads" to mapOf>() -) +val DEFAULTS = + mapOf( + "playSoundChat" to true, + "playSoundFollow" to false, + "playSoundLike" to false, + "playSoundMention" to false, + "playSoundQuote" to false, + "playSoundReply" to false, + "playSoundRepost" to false, + "mutedThreads" to mapOf>(), + ) -class NotificationPrefs (private val context: Context?) { - private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) - ?: throw Error("Context is null") +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 @@ -41,94 +45,99 @@ class NotificationPrefs (private val context: Context?) { } } } - } - .apply() + }.apply() } - fun getAllPrefs(): MutableMap { - return prefs.all - } + fun getAllPrefs(): MutableMap = prefs.all - fun getBoolean(key: String): Boolean { - return prefs.getBoolean(key, false) - } + fun getBoolean(key: String): Boolean = prefs.getBoolean(key, false) - fun getString(key: String): String? { - return prefs.getString(key, null) - } + fun getString(key: String): String? = prefs.getString(key, null) - fun getStringArray(key: String): Array? { - return prefs.getStringSet(key, null)?.toTypedArray() - } + fun getStringArray(key: String): Array? = prefs.getStringSet(key, null)?.toTypedArray() - fun setBoolean(key: String, value: Boolean) { + fun setBoolean( + key: String, + value: Boolean, + ) { prefs .edit() .apply { putBoolean(key, value) - } - .apply() + }.apply() } - fun setString(key: String, value: String) { + fun setString( + key: String, + value: String, + ) { prefs .edit() .apply { putString(key, value) - } - .apply() + }.apply() } - fun setStringArray(key: String, value: Array) { + fun setStringArray( + key: String, + value: Array, + ) { prefs .edit() .apply { putStringSet(key, value.toSet()) - } - .apply() + }.apply() } - fun addToStringArray(key: String, string: String) { + fun addToStringArray( + key: String, + string: String, + ) { prefs .edit() .apply { val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() set.add(string) putStringSet(key, set) - } - .apply() + }.apply() } - fun removeFromStringArray(key: String, string: String) { + fun removeFromStringArray( + key: String, + string: String, + ) { prefs .edit() .apply { val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() set.remove(string) putStringSet(key, set) - } - .apply() + }.apply() } - fun addManyToStringArray(key: String, strings: Array) { + fun addManyToStringArray( + key: String, + strings: Array, + ) { prefs .edit() .apply { val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() set.addAll(strings.toSet()) putStringSet(key, set) - } - .apply() + }.apply() } - fun removeManyFromStringArray(key: String, strings: Array) { + fun removeManyFromStringArray( + key: String, + strings: Array, + ) { prefs .edit() .apply { val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() set.removeAll(strings.toSet()) putStringSet(key, set) - } - .apply() + }.apply() } -} \ No newline at end of file +} diff --git a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift index 5f8c7fc3..3845fe76 100644 --- a/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift +++ b/modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift @@ -2,16 +2,16 @@ import ExpoModulesCore let APP_GROUP = "group.app.bsky" -let DEFAULTS: [String:Any] = [ - "playSoundChat" : true, +let DEFAULTS: [String: Any] = [ + "playSoundChat": true, "playSoundFollow": false, "playSoundLike": false, "playSoundMention": false, "playSoundQuote": false, "playSoundReply": false, "playSoundRepost": false, - "mutedThreads": [:] as! [String:[String]], - "badgeCount": 0, + "mutedThreads": [:] as! [String: [String]], + "badgeCount": 0 ] /* @@ -23,10 +23,10 @@ let DEFAULTS: [String:Any] = [ */ 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 { @@ -34,57 +34,56 @@ public class ExpoBackgroundNotificationHandlerModule: Module { } } } - - AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in + + 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 + + AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) in userDefaults?.setValue(value, forKey: forKey) } - - AsyncFunction("setStringAsync") { (forKey: String, value: String) -> Void in + + AsyncFunction("setStringAsync") { (forKey: String, value: String) in userDefaults?.setValue(value, forKey: forKey) } - - AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in + + AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) in userDefaults?.setValue(value, forKey: forKey) } - + AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in if var curr = userDefaults?.stringArray(forKey: forKey), - !curr.contains(string) - { + !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 @@ -93,7 +92,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module { userDefaults?.setValue(curr, forKey: forKey) } } - + AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in if var curr = userDefaults?.stringArray(forKey: forKey) { strings.forEach { s in @@ -104,7 +103,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module { userDefaults?.setValue(curr, forKey: forKey) } } - + AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in if var curr = userDefaults?.stringArray(forKey: forKey) { strings.forEach { s in @@ -113,7 +112,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module { userDefaults?.setValue(curr, forKey: forKey) } } - + AsyncFunction("setBadgeCountAsync") { (count: Int) in userDefaults?.setValue(count, forKey: "badgeCount") } diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt index 5d208484..5ba5ccf7 100644 --- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt @@ -5,7 +5,10 @@ import android.graphics.Canvas import android.graphics.drawable.Animatable import androidx.appcompat.widget.AppCompatImageView -class AppCompatImageViewExtended(context: Context, private val parent: GifView): AppCompatImageView(context) { +class AppCompatImageViewExtended( + context: Context, + private val parent: GifView, +) : AppCompatImageView(context) { override fun onDraw(canvas: Canvas) { super.onDraw(canvas) @@ -34,4 +37,4 @@ class AppCompatImageViewExtended(context: Context, private val parent: GifView): drawable.start() } } -} \ No newline at end of file +} diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt index 625e1d45..b0b3e8ee 100644 --- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt @@ -6,49 +6,50 @@ import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoBlueskyGifViewModule : Module() { - override fun definition() = ModuleDefinition { - Name("ExpoBlueskyGifView") + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskyGifView") - AsyncFunction("prefetchAsync") { sources: List -> - val activity = appContext.currentActivity ?: return@AsyncFunction - val glide = Glide.with(activity) + AsyncFunction("prefetchAsync") { sources: List -> + val activity = appContext.currentActivity ?: return@AsyncFunction + val glide = Glide.with(activity) - sources.forEach { source -> - glide - .download(source) - .diskCacheStrategy(DiskCacheStrategy.DATA) - .submit() + sources.forEach { source -> + glide + .download(source) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .submit() + } + } + + View(GifView::class) { + Events( + "onPlayerStateChange", + ) + + Prop("source") { view: GifView, source: String -> + view.source = source + } + + Prop("placeholderSource") { view: GifView, source: String -> + view.placeholderSource = source + } + + Prop("autoplay") { view: GifView, autoplay: Boolean -> + view.autoplay = autoplay + } + + AsyncFunction("playAsync") { view: GifView -> + view.play() + } + + AsyncFunction("pauseAsync") { view: GifView -> + view.pause() + } + + AsyncFunction("toggleAsync") { view: GifView -> + view.toggle() + } } } - - View(GifView::class) { - Events( - "onPlayerStateChange" - ) - - Prop("source") { view: GifView, source: String -> - view.source = source - } - - Prop("placeholderSource") { view: GifView, source: String -> - view.placeholderSource = source - } - - Prop("autoplay") { view: GifView, autoplay: Boolean -> - view.autoplay = autoplay - } - - AsyncFunction("playAsync") { view: GifView -> - view.play() - } - - AsyncFunction("pauseAsync") { view: GifView -> - view.pause() - } - - AsyncFunction("toggleAsync") { view: GifView -> - view.toggle() - } - } - } } diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt index be5830df..5e467a16 100644 --- a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt +++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt @@ -1,6 +1,5 @@ package expo.modules.blueskygifview - import android.content.Context import android.graphics.Color import android.graphics.drawable.Animatable @@ -15,7 +14,10 @@ import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView -class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { +class GifView( + context: Context, + appContext: AppContext, +) : ExpoView(context, appContext) { // Events private val onPlayerStateChange by EventDispatcher() @@ -44,8 +46,7 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC } } - - // + // init { this.setBackgroundColor(Color.TRANSPARENT) @@ -70,80 +71,82 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC super.onDetachedFromWindow() } - // + // - // + // private fun load() { if (placeholderSource == null || source == null) { return } - this.webpRequest = glide.load(source) - .diskCacheStrategy(DiskCacheStrategy.DATA) - .skipMemoryCache(false) - .listener(object: RequestListener { - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: com.bumptech.glide.load.DataSource?, - isFirstResource: Boolean - ): Boolean { - if (placeholderRequest != null) { - glide.clear(placeholderRequest) - } - return false - } + this.webpRequest = + glide + .load(source) + .diskCacheStrategy(DiskCacheStrategy.DATA) + .skipMemoryCache(false) + .listener( + object : RequestListener { + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: com.bumptech.glide.load.DataSource?, + isFirstResource: Boolean, + ): Boolean { + if (placeholderRequest != null) { + glide.clear(placeholderRequest) + } + return false + } - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - return true - } - }) - .into(this.imageView) + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean, + ): Boolean = true + }, + ).into(this.imageView) if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) { - this.placeholderRequest = glide.load(placeholderSource) - .diskCacheStrategy(DiskCacheStrategy.DATA) - // Let's not bloat the memory cache with placeholders - .skipMemoryCache(true) - .listener(object: RequestListener { - override fun onResourceReady( - resource: Drawable?, - model: Any?, - target: Target?, - dataSource: com.bumptech.glide.load.DataSource?, - isFirstResource: Boolean - ): Boolean { - // Incase this request finishes after the webp, let's just not set - // the drawable. This shouldn't happen because the request should get cancelled - if (imageView.drawable == null) { - imageView.setImageDrawable(resource) - } - return true - } + this.placeholderRequest = + glide + .load(placeholderSource) + .diskCacheStrategy(DiskCacheStrategy.DATA) + // Let's not bloat the memory cache with placeholders + .skipMemoryCache(true) + .listener( + object : RequestListener { + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: com.bumptech.glide.load.DataSource?, + isFirstResource: Boolean, + ): Boolean { + // Incase this request finishes after the webp, let's just not set + // the drawable. This shouldn't happen because the request should get cancelled + if (imageView.drawable == null) { + imageView.setImageDrawable(resource) + } + return true + } - override fun onLoadFailed( - e: GlideException?, - model: Any?, - target: Target?, - isFirstResource: Boolean - ): Boolean { - return true - } - }) - .submit() + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean, + ): Boolean = true + }, + ).submit() } } - // + // - // + // fun play() { this.imageView.play() @@ -165,16 +168,18 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC } } - // + // - // + // fun firePlayerStateChange() { - onPlayerStateChange(mapOf( - "isPlaying" to this.isPlaying, - "isLoaded" to this.isLoaded, - )) + onPlayerStateChange( + mapOf( + "isPlaying" to this.isPlaying, + "isLoaded" to this.isLoaded, + ), + ) } - // + // } diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift index 7c713229..9156bd19 100644 --- a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift +++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift @@ -5,11 +5,11 @@ import SDWebImageWebPCoder public class ExpoBlueskyGifViewModule: Module { public func definition() -> ModuleDefinition { Name("ExpoBlueskyGifView") - + OnCreate { SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared) } - + AsyncFunction("prefetchAsync") { (sources: [URL]) in SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil) } @@ -18,27 +18,27 @@ public class ExpoBlueskyGifViewModule: Module { Events( "onPlayerStateChange" ) - + Prop("source") { (view: GifView, prop: String) in view.source = prop } - + Prop("placeholderSource") { (view: GifView, prop: String) in view.placeholderSource = prop } - + Prop("autoplay") { (view: GifView, prop: Bool) in view.autoplay = prop } - + AsyncFunction("toggleAsync") { (view: GifView) in view.toggle() } - + AsyncFunction("playAsync") { (view: GifView) in view.play() } - + AsyncFunction("pauseAsync") { (view: GifView) in view.pause() } diff --git a/modules/expo-bluesky-gif-view/ios/GifView.swift b/modules/expo-bluesky-gif-view/ios/GifView.swift index de722d7a..b42a4735 100644 --- a/modules/expo-bluesky-gif-view/ios/GifView.swift +++ b/modules/expo-bluesky-gif-view/ios/GifView.swift @@ -16,14 +16,14 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate { ) private var isPlaying = true private var isLoaded = false - + // Requests private var webpOperation: SDWebImageCombinedOperation? private var placeholderOperation: SDWebImageCombinedOperation? // Props - var source: String? = nil - var placeholderSource: String? = nil + var source: String? + var placeholderSource: String? var autoplay = true { didSet { if !autoplay { @@ -78,8 +78,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate { // See: // https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently if !SDImageCache.shared.diskImageDataExists(withKey: source), - let url = URL(string: placeholderSource) - { + let url = URL(string: placeholderSource) { self.placeholderOperation = imageManager.loadImage( with: url, options: [.retryFailed], @@ -132,8 +131,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate { if let placeholderSource = self.placeholderSource, imageUrl?.absoluteString == placeholderSource, self.imageView.image == nil, - let image = image - { + let image = image { self.setImage(image) return } @@ -142,8 +140,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate { imageUrl?.absoluteString == source, // UIImage perf suckssss if the image is animated let data = data, - let animatedImage = SDAnimatedImage(data: data) - { + let animatedImage = SDAnimatedImage(data: data) { self.placeholderOperation?.cancel() self.isPlaying = self.autoplay self.isLoaded = true 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 index 29017f17..51f9fe45 100644 --- 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 @@ -4,7 +4,8 @@ import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoBlueskyDevicePrefsModule : Module() { - override fun definition() = ModuleDefinition { - Name("ExpoBlueskyDevicePrefs") - } + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskyDevicePrefs") + } } diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt index 3589b364..ac6ed90b 100644 --- a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt +++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt @@ -3,52 +3,55 @@ package expo.modules.blueskyswissarmy.referrer import android.util.Log import com.android.installreferrer.api.InstallReferrerClient import com.android.installreferrer.api.InstallReferrerStateListener +import expo.modules.kotlin.Promise import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition -import expo.modules.kotlin.Promise class ExpoBlueskyReferrerModule : Module() { - override fun definition() = ModuleDefinition { - Name("ExpoBlueskyReferrer") + override fun definition() = + ModuleDefinition { + Name("ExpoBlueskyReferrer") - AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> - val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() - referrerClient.startConnection(object : InstallReferrerStateListener { - override fun onInstallReferrerSetupFinished(responseCode: Int) { - if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { - Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") + AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> + val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() + referrerClient.startConnection( + object : InstallReferrerStateListener { + override fun onInstallReferrerSetupFinished(responseCode: Int) { + if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { + Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") - val response = referrerClient.installReferrer - Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") + val response = referrerClient.installReferrer + Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") - promise.resolve( - mapOf( - "installReferrer" to response.installReferrer, - "clickTimestamp" to response.referrerClickTimestampSeconds, - "installTimestamp" to response.installBeginTimestampSeconds + promise.resolve( + mapOf( + "installReferrer" to response.installReferrer, + "clickTimestamp" to response.referrerClickTimestampSeconds, + "installTimestamp" to response.installBeginTimestampSeconds, + ), + ) + } else { + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.") + promise.reject( + "ERR_GOOGLE_PLAY_REFERRER_UNKNOWN", + "Failed to get referrer info", + Exception("Failed to get referrer info"), + ) + } + referrerClient.endConnection() + } + + override fun onInstallReferrerServiceDisconnected() { + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.") + referrerClient.endConnection() + promise.reject( + "ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED", + "Failed to get referrer info", + Exception("Failed to get referrer info"), ) - ) - } else { - Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.") - promise.reject( - "ERR_GOOGLE_PLAY_REFERRER_UNKNOWN", - "Failed to get referrer info", - Exception("Failed to get referrer info") - ) - } - referrerClient.endConnection() - } - - override fun onInstallReferrerServiceDisconnected() { - Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.") - referrerClient.endConnection() - promise.reject( - "ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED", - "Failed to get referrer info", - Exception("Failed to get referrer info") - ) - } - }) + } + }, + ) + } } - } -} \ No newline at end of file +} diff --git a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt index c2e17fb8..7ecea163 100644 --- a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt +++ b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt @@ -13,16 +13,17 @@ import java.io.FileOutputStream import java.net.URLEncoder class ExpoReceiveAndroidIntentsModule : Module() { - override fun definition() = ModuleDefinition { - Name("ExpoReceiveAndroidIntents") + override fun definition() = + ModuleDefinition { + Name("ExpoReceiveAndroidIntents") - OnNewIntent { - handleIntent(it) + OnNewIntent { + handleIntent(it) + } } - } private fun handleIntent(intent: Intent?) { - if(appContext.currentActivity == null || intent == null) return + if (appContext.currentActivity == null || intent == null) return if (intent.action == Intent.ACTION_SEND) { if (intent.type == "text/plain") { @@ -40,7 +41,7 @@ class ExpoReceiveAndroidIntentsModule : Module() { private fun handleTextIntent(intent: Intent) { intent.getStringExtra(Intent.EXTRA_TEXT)?.let { val encoded = URLEncoder.encode(it, "UTF-8") - "bluesky://intent/compose?text=${encoded}".toUri().let { uri -> + "bluesky://intent/compose?text=$encoded".toUri().let { uri -> val newIntent = Intent(Intent.ACTION_VIEW, uri) appContext.currentActivity?.startActivity(newIntent) } @@ -48,11 +49,12 @@ class ExpoReceiveAndroidIntentsModule : Module() { } private fun handleImageIntent(intent: Intent) { - val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) - } else { - intent.getParcelableExtra(Intent.EXTRA_STREAM) - } + val uri = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) + } else { + intent.getParcelableExtra(Intent.EXTRA_STREAM) + } if (uri == null) return handleImageIntents(listOf(uri)) @@ -76,16 +78,16 @@ class ExpoReceiveAndroidIntentsModule : Module() { uris.forEachIndexed { index, uri -> val info = getImageInfo(uri) val params = buildUriData(info) - allParams = "${allParams}${params}" + allParams = "${allParams}$params" if (index < uris.count() - 1) { - allParams = "${allParams}," + allParams = "$allParams," } } val encoded = URLEncoder.encode(allParams, "UTF-8") - "bluesky://intent/compose?imageUris=${encoded}".toUri().let { + "bluesky://intent/compose?imageUris=$encoded".toUri().let { val newIntent = Intent(Intent.ACTION_VIEW, it) appContext.currentActivity?.startActivity(newIntent) } @@ -104,7 +106,7 @@ class ExpoReceiveAndroidIntentsModule : Module() { return mapOf( "width" to bitmap.width, "height" to bitmap.height, - "path" to file.path.toString() + "path" to file.path.toString(), ) } @@ -114,6 +116,6 @@ class ExpoReceiveAndroidIntentsModule : Module() { val path = info.getValue("path") val width = info.getValue("width") val height = info.getValue("height") - return "file://${path}|${width}|${height}" + return "file://$path|$width|$height" } } diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift index c4ecc788..53e25882 100644 --- a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift +++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift @@ -3,7 +3,7 @@ import ExpoModulesCore public class ExpoScrollForwarderModule: Module { public func definition() -> ModuleDefinition { Name("ExpoScrollForwarder") - + View(ExpoScrollForwarderView.self) { Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in view.scrollViewTag = prop diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift index 9c0e2f87..15993ef2 100644 --- a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift +++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift @@ -8,17 +8,17 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { self.tryFindScrollView() } } - + private var rctScrollView: RCTScrollView? private var rctRefreshCtrl: RCTRefreshControl? private var cancelGestureRecognizers: [UIGestureRecognizer]? private var animTimer: Timer? private var initialOffset: CGFloat = 0.0 private var didImpact: Bool = false - + required init(appContext: AppContext? = nil) { super.init(appContext: appContext) - + let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:))) pg.delegate = self self.addGestureRecognizer(pg) @@ -34,28 +34,27 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { self.cancelGestureRecognizers = [lpg, tg] } - // We don't want to recognize the scroll pan gesture and the swipe back gesture together func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer { return false } - + return true } - + // We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will // interfere with the native swipe back gesture. override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return true } - + let velocity = gestureRecognizer.velocity(in: self) return abs(velocity.y) > abs(velocity.x) } - + // This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another // recognizer for this one. override func touchesBegan(_ touches: Set, with event: UIEvent?) { @@ -64,32 +63,32 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { // This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change // the scroll view gesture's delegate, so we add an additional recognizer to detect this. - @IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void { + @IBAction func callOnPress(_ sender: UITapGestureRecognizer) { self.stopTimer() } - - @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) -> Void { + + @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) { guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else { return } let translation = sender.translation(in: self).y - + if sender.state == .began { if sv.contentOffset.y < 0 { sv.contentOffset.y = 0 } - + self.initialOffset = sv.contentOffset.y } if sender.state == .changed { sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset) - + if sv.contentOffset.y <= -130, !didImpact { let generator = UIImpactFeedbackGenerator(style: .light) generator.impactOccurred() - + self.didImpact = true } } @@ -97,7 +96,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { if sender.state == .ended { let velocity = sender.velocity(in: self).y self.didImpact = false - + if sv.contentOffset.y <= -130 { self.rctRefreshCtrl?.forwarderBeginRefreshing() return @@ -108,40 +107,40 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { if abs(velocity) < 250, sv.contentOffset.y >= 0 { return } - + self.startDecayAnimation(translation, velocity) } } - + func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) { guard let sv = self.rctScrollView?.scrollView else { return } - + var velocity = velocity - + self.enableCancelGestureRecognizers() - + if velocity > 0 { velocity = min(velocity, 5000) } else { velocity = max(velocity, -5000) } - + var animTranslation = -translation - self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { timer in + self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in velocity *= 0.9875 animTranslation = (-velocity / 120) + animTranslation - + let nextOffset = self.dampenOffset(animTranslation + self.initialOffset) - + if nextOffset <= 0 { if self.initialOffset <= 1 { self.scrollToOffset(0) } else { sv.contentOffset.y = 0 } - + self.stopTimer() return } else { @@ -153,61 +152,60 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate { } } } - + func dampenOffset(_ offset: CGFloat) -> CGFloat { if offset < 0 { return offset - (offset * 0.55) } - + return offset } - + func tryFindScrollView() { guard let scrollViewTag = scrollViewTag else { return } - + // Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer. // Otherwise we might end up with duplicates when we switch back to that scrollview. self.removeCancelGestureRecognizers() - + self.rctScrollView = self.appContext? .findView(withTag: scrollViewTag, ofType: RCTScrollView.self) self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl - + self.addCancelGestureRecognizers() } - + func addCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in self.rctScrollView?.scrollView?.addGestureRecognizer(r) } } - + func removeCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in self.rctScrollView?.scrollView?.removeGestureRecognizer(r) } } - func enableCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in r.isEnabled = true } } - + func disableCancelGestureRecognizers() { self.cancelGestureRecognizers?.forEach { r in r.isEnabled = false } } - - func scrollToOffset(_ offset: Int, animated: Bool = true) -> Void { + + func scrollToOffset(_ offset: Int, animated: Bool = true) { self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated) } - func stopTimer() -> Void { + func stopTimer() { self.disableCancelGestureRecognizers() self.animTimer?.invalidate() self.animTimer = nil diff --git a/package.json b/package.json index 6b11fc13..141646d1 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit", "test-coverage": "NODE_ENV=test jest --coverage", "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src", + "lint-native": "swiftlint ./modules && ktlint ./modules", + "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", "typecheck": "tsc --project ./tsconfig.check.json", "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts", "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",