Bluesky app fork with some witchin' additions 💫

Lint native files (#4768)

authored by hailey.at and committed by GitHub 2397104a b433469a

Changed files
+385 -367
modules
BlueskyNSE
Share-with-Bluesky
expo-background-notification-handler
expo-bluesky-gif-view
expo-bluesky-swiss-army
android
src
main
java
expo
modules
blueskyswissarmy
expo-receive-android-intents
android
src
main
java
xyz
blueskyweb
app
exporeceiveandroidintents
expo-scroll-forwarder
+2
.editorconfig
···
··· 1 + [*.{kt,kts}] 2 + indent_size=2
+9 -9
modules/BlueskyNSE/NotificationService.swift
··· 13 contentHandler(request.content) 14 return 15 } 16 - 17 if reason == "chat-message" { 18 mutateWithChatMessage(bestAttempt) 19 } else { 20 mutateWithBadge(bestAttempt) 21 } 22 - 23 contentHandler(bestAttempt) 24 } 25 - 26 override func serviceExtensionTimeWillExpire() { 27 // If for some reason the alloted time expires, we don't actually want to display a notification 28 } 29 - 30 func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { 31 return content.mutableCopy() as? UNMutableNotificationContent 32 } 33 - 34 func mutateWithBadge(_ content: UNMutableNotificationContent) { 35 var count = prefs?.integer(forKey: "badgeCount") ?? 0 36 count += 1 37 - 38 // Set the new badge number for the notification, then store that value for using later 39 content.badge = NSNumber(value: count) 40 prefs?.setValue(count, forKey: "badgeCount") 41 } 42 - 43 func mutateWithChatMessage(_ content: UNMutableNotificationContent) { 44 if self.prefs?.bool(forKey: "playSoundChat") == true { 45 mutateWithDmSound(content) 46 } 47 } 48 - 49 func mutateWithDefaultSound(_ content: UNMutableNotificationContent) { 50 content.sound = UNNotificationSound.default 51 } 52 - 53 func mutateWithDmSound(_ content: UNMutableNotificationContent) { 54 content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) 55 }
··· 13 contentHandler(request.content) 14 return 15 } 16 + 17 if reason == "chat-message" { 18 mutateWithChatMessage(bestAttempt) 19 } else { 20 mutateWithBadge(bestAttempt) 21 } 22 + 23 contentHandler(bestAttempt) 24 } 25 + 26 override func serviceExtensionTimeWillExpire() { 27 // If for some reason the alloted time expires, we don't actually want to display a notification 28 } 29 + 30 func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { 31 return content.mutableCopy() as? UNMutableNotificationContent 32 } 33 + 34 func mutateWithBadge(_ content: UNMutableNotificationContent) { 35 var count = prefs?.integer(forKey: "badgeCount") ?? 0 36 count += 1 37 + 38 // Set the new badge number for the notification, then store that value for using later 39 content.badge = NSNumber(value: count) 40 prefs?.setValue(count, forKey: "badgeCount") 41 } 42 + 43 func mutateWithChatMessage(_ content: UNMutableNotificationContent) { 44 if self.prefs?.bool(forKey: "playSoundChat") == true { 45 mutateWithDmSound(content) 46 } 47 } 48 + 49 func mutateWithDefaultSound(_ content: UNMutableNotificationContent) { 50 content.sound = UNNotificationSound.default 51 } 52 + 53 func mutateWithDmSound(_ content: UNMutableNotificationContent) { 54 content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) 55 }
+10 -15
modules/Share-with-Bluesky/ShareViewController.swift
··· 30 } 31 } 32 33 - private func handleText(item: NSItemProvider) async -> Void { 34 do { 35 if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String { 36 if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 37 - let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") 38 - { 39 _ = self.openURL(url) 40 } 41 } ··· 45 } 46 } 47 48 - private func handleUrl(item: NSItemProvider) async -> Void { 49 do { 50 if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL { 51 if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 52 - let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") 53 - { 54 _ = self.openURL(url) 55 } 56 } ··· 60 } 61 } 62 63 - private func handleImages(items: [NSItemProvider]) async -> Void { 64 let firstFourItems: [NSItemProvider] 65 if items.count < 4 { 66 firstFourItems = items ··· 72 var imageUris = "" 73 74 for (index, item) in firstFourItems.enumerated() { 75 - var imageUriInfo: String? = nil 76 77 do { 78 if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL { ··· 100 101 if valid, 102 let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 103 - let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") 104 - { 105 _ = self.openURL(url) 106 } 107 ··· 119 // extension does. 120 if let dir = FileManager() 121 .containerURL( 122 - forSecurityApplicationGroupIdentifier: "group.app.bsky") 123 - { 124 let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg" 125 126 if let newUri = URL(string: filePath), 127 - let jpegData = image.jpegData(compressionQuality: 1) 128 - { 129 try jpegData.write(to: newUri) 130 return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)" 131 } ··· 136 } 137 } 138 139 - private func completeRequest() -> Void { 140 self.extensionContext?.completeRequest(returningItems: nil) 141 } 142
··· 30 } 31 } 32 33 + private func handleText(item: NSItemProvider) async { 34 do { 35 if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String { 36 if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 37 + let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") { 38 _ = self.openURL(url) 39 } 40 } ··· 44 } 45 } 46 47 + private func handleUrl(item: NSItemProvider) async { 48 do { 49 if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL { 50 if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 51 + let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") { 52 _ = self.openURL(url) 53 } 54 } ··· 58 } 59 } 60 61 + private func handleImages(items: [NSItemProvider]) async { 62 let firstFourItems: [NSItemProvider] 63 if items.count < 4 { 64 firstFourItems = items ··· 70 var imageUris = "" 71 72 for (index, item) in firstFourItems.enumerated() { 73 + var imageUriInfo: String? 74 75 do { 76 if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL { ··· 98 99 if valid, 100 let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 101 + let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") { 102 _ = self.openURL(url) 103 } 104 ··· 116 // extension does. 117 if let dir = FileManager() 118 .containerURL( 119 + forSecurityApplicationGroupIdentifier: "group.app.bsky") { 120 let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg" 121 122 if let newUri = URL(string: filePath), 123 + let jpegData = image.jpegData(compressionQuality: 1) { 124 try jpegData.write(to: newUri) 125 return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)" 126 } ··· 131 } 132 } 133 134 + private func completeRequest() { 135 self.extensionContext?.completeRequest(returningItems: nil) 136 } 137
+1 -1
modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/BackgroundNotificationHandler.kt
··· 5 6 class BackgroundNotificationHandler( 7 private val context: Context, 8 - private val notifInterface: BackgroundNotificationHandlerInterface 9 ) { 10 fun handleMessage(remoteMessage: RemoteMessage) { 11 if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {
··· 5 6 class BackgroundNotificationHandler( 7 private val context: Context, 8 + private val notifInterface: BackgroundNotificationHandlerInterface, 9 ) { 10 fun handleMessage(remoteMessage: RemoteMessage) { 11 if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {
+48 -47
modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/ExpoBackgroundNotificationHandlerModule.kt
··· 8 var isForegrounded = false 9 } 10 11 - override fun definition() = ModuleDefinition { 12 - Name("ExpoBackgroundNotificationHandler") 13 14 - OnCreate { 15 - NotificationPrefs(appContext.reactContext).initialize() 16 - } 17 18 - OnActivityEntersForeground { 19 - isForegrounded = true 20 - } 21 22 - OnActivityEntersBackground { 23 - isForegrounded = false 24 - } 25 26 - AsyncFunction("getAllPrefsAsync") { 27 - return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs() 28 - } 29 30 - AsyncFunction("getBoolAsync") { forKey: String -> 31 - return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey) 32 - } 33 34 - AsyncFunction("getStringAsync") { forKey: String -> 35 - return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey) 36 - } 37 38 - AsyncFunction("getStringArrayAsync") { forKey: String -> 39 - return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey) 40 - } 41 42 - AsyncFunction("setBoolAsync") { forKey: String, value: Boolean -> 43 - NotificationPrefs(appContext.reactContext).setBoolean(forKey, value) 44 - } 45 46 - AsyncFunction("setStringAsync") { forKey: String, value: String -> 47 - NotificationPrefs(appContext.reactContext).setString(forKey, value) 48 - } 49 50 - AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> -> 51 - NotificationPrefs(appContext.reactContext).setStringArray(forKey, value) 52 - } 53 54 - AsyncFunction("addToStringArrayAsync") { forKey: String, string: String -> 55 - NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string) 56 - } 57 58 - AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String -> 59 - NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string) 60 - } 61 62 - AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> -> 63 - NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings) 64 - } 65 66 - AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> -> 67 - NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) 68 - } 69 70 - AsyncFunction("setBadgeCountAsync") { _: Int -> 71 - // This does nothing on Android 72 } 73 - } 74 }
··· 8 var isForegrounded = false 9 } 10 11 + override fun definition() = 12 + ModuleDefinition { 13 + Name("ExpoBackgroundNotificationHandler") 14 15 + OnCreate { 16 + NotificationPrefs(appContext.reactContext).initialize() 17 + } 18 19 + OnActivityEntersForeground { 20 + isForegrounded = true 21 + } 22 23 + OnActivityEntersBackground { 24 + isForegrounded = false 25 + } 26 27 + AsyncFunction("getAllPrefsAsync") { 28 + return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs() 29 + } 30 31 + AsyncFunction("getBoolAsync") { forKey: String -> 32 + return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey) 33 + } 34 35 + AsyncFunction("getStringAsync") { forKey: String -> 36 + return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey) 37 + } 38 39 + AsyncFunction("getStringArrayAsync") { forKey: String -> 40 + return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey) 41 + } 42 43 + AsyncFunction("setBoolAsync") { forKey: String, value: Boolean -> 44 + NotificationPrefs(appContext.reactContext).setBoolean(forKey, value) 45 + } 46 47 + AsyncFunction("setStringAsync") { forKey: String, value: String -> 48 + NotificationPrefs(appContext.reactContext).setString(forKey, value) 49 + } 50 51 + AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> -> 52 + NotificationPrefs(appContext.reactContext).setStringArray(forKey, value) 53 + } 54 55 + AsyncFunction("addToStringArrayAsync") { forKey: String, string: String -> 56 + NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string) 57 + } 58 59 + AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String -> 60 + NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string) 61 + } 62 63 + AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> -> 64 + NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings) 65 + } 66 67 + AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> -> 68 + NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) 69 + } 70 71 + AsyncFunction("setBadgeCountAsync") { _: Int -> 72 + // This does nothing on Android 73 + } 74 } 75 }
+58 -49
modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt
··· 2 3 import android.content.Context 4 5 - val DEFAULTS = mapOf<String, Any>( 6 - "playSoundChat" to true, 7 - "playSoundFollow" to false, 8 - "playSoundLike" to false, 9 - "playSoundMention" to false, 10 - "playSoundQuote" to false, 11 - "playSoundReply" to false, 12 - "playSoundRepost" to false, 13 - "mutedThreads" to mapOf<String, List<String>>() 14 - ) 15 16 - class NotificationPrefs (private val context: Context?) { 17 - private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) 18 - ?: throw Error("Context is null") 19 20 fun initialize() { 21 prefs ··· 41 } 42 } 43 } 44 - } 45 - .apply() 46 } 47 48 - fun getAllPrefs(): MutableMap<String, *> { 49 - return prefs.all 50 - } 51 52 - fun getBoolean(key: String): Boolean { 53 - return prefs.getBoolean(key, false) 54 - } 55 56 - fun getString(key: String): String? { 57 - return prefs.getString(key, null) 58 - } 59 60 - fun getStringArray(key: String): Array<String>? { 61 - return prefs.getStringSet(key, null)?.toTypedArray() 62 - } 63 64 - fun setBoolean(key: String, value: Boolean) { 65 prefs 66 .edit() 67 .apply { 68 putBoolean(key, value) 69 - } 70 - .apply() 71 } 72 73 - fun setString(key: String, value: String) { 74 prefs 75 .edit() 76 .apply { 77 putString(key, value) 78 - } 79 - .apply() 80 } 81 82 - fun setStringArray(key: String, value: Array<String>) { 83 prefs 84 .edit() 85 .apply { 86 putStringSet(key, value.toSet()) 87 - } 88 - .apply() 89 } 90 91 - fun addToStringArray(key: String, string: String) { 92 prefs 93 .edit() 94 .apply { 95 val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() 96 set.add(string) 97 putStringSet(key, set) 98 - } 99 - .apply() 100 } 101 102 - fun removeFromStringArray(key: String, string: String) { 103 prefs 104 .edit() 105 .apply { 106 val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() 107 set.remove(string) 108 putStringSet(key, set) 109 - } 110 - .apply() 111 } 112 113 - fun addManyToStringArray(key: String, strings: Array<String>) { 114 prefs 115 .edit() 116 .apply { 117 val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() 118 set.addAll(strings.toSet()) 119 putStringSet(key, set) 120 - } 121 - .apply() 122 } 123 124 - fun removeManyFromStringArray(key: String, strings: Array<String>) { 125 prefs 126 .edit() 127 .apply { 128 val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() 129 set.removeAll(strings.toSet()) 130 putStringSet(key, set) 131 - } 132 - .apply() 133 } 134 - }
··· 2 3 import android.content.Context 4 5 + val DEFAULTS = 6 + mapOf<String, Any>( 7 + "playSoundChat" to true, 8 + "playSoundFollow" to false, 9 + "playSoundLike" to false, 10 + "playSoundMention" to false, 11 + "playSoundQuote" to false, 12 + "playSoundReply" to false, 13 + "playSoundRepost" to false, 14 + "mutedThreads" to mapOf<String, List<String>>(), 15 + ) 16 17 + class NotificationPrefs( 18 + private val context: Context?, 19 + ) { 20 + private val prefs = 21 + context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) 22 + ?: throw Error("Context is null") 23 24 fun initialize() { 25 prefs ··· 45 } 46 } 47 } 48 + }.apply() 49 } 50 51 + fun getAllPrefs(): MutableMap<String, *> = prefs.all 52 53 + fun getBoolean(key: String): Boolean = prefs.getBoolean(key, false) 54 55 + fun getString(key: String): String? = prefs.getString(key, null) 56 57 + fun getStringArray(key: String): Array<String>? = prefs.getStringSet(key, null)?.toTypedArray() 58 59 + fun setBoolean( 60 + key: String, 61 + value: Boolean, 62 + ) { 63 prefs 64 .edit() 65 .apply { 66 putBoolean(key, value) 67 + }.apply() 68 } 69 70 + fun setString( 71 + key: String, 72 + value: String, 73 + ) { 74 prefs 75 .edit() 76 .apply { 77 putString(key, value) 78 + }.apply() 79 } 80 81 + fun setStringArray( 82 + key: String, 83 + value: Array<String>, 84 + ) { 85 prefs 86 .edit() 87 .apply { 88 putStringSet(key, value.toSet()) 89 + }.apply() 90 } 91 92 + fun addToStringArray( 93 + key: String, 94 + string: String, 95 + ) { 96 prefs 97 .edit() 98 .apply { 99 val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() 100 set.add(string) 101 putStringSet(key, set) 102 + }.apply() 103 } 104 105 + fun removeFromStringArray( 106 + key: String, 107 + string: String, 108 + ) { 109 prefs 110 .edit() 111 .apply { 112 val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() 113 set.remove(string) 114 putStringSet(key, set) 115 + }.apply() 116 } 117 118 + fun addManyToStringArray( 119 + key: String, 120 + strings: Array<String>, 121 + ) { 122 prefs 123 .edit() 124 .apply { 125 val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() 126 set.addAll(strings.toSet()) 127 putStringSet(key, set) 128 + }.apply() 129 } 130 131 + fun removeManyFromStringArray( 132 + key: String, 133 + strings: Array<String>, 134 + ) { 135 prefs 136 .edit() 137 .apply { 138 val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() 139 set.removeAll(strings.toSet()) 140 putStringSet(key, set) 141 + }.apply() 142 } 143 + }
+23 -24
modules/expo-background-notification-handler/ios/ExpoBackgroundNotificationHandlerModule.swift
··· 2 3 let APP_GROUP = "group.app.bsky" 4 5 - let DEFAULTS: [String:Any] = [ 6 - "playSoundChat" : true, 7 "playSoundFollow": false, 8 "playSoundLike": false, 9 "playSoundMention": false, 10 "playSoundQuote": false, 11 "playSoundReply": false, 12 "playSoundRepost": false, 13 - "mutedThreads": [:] as! [String:[String]], 14 - "badgeCount": 0, 15 ] 16 17 /* ··· 23 */ 24 public class ExpoBackgroundNotificationHandlerModule: Module { 25 let userDefaults = UserDefaults(suiteName: APP_GROUP) 26 - 27 public func definition() -> ModuleDefinition { 28 Name("ExpoBackgroundNotificationHandler") 29 - 30 OnCreate { 31 DEFAULTS.forEach { p in 32 if userDefaults?.value(forKey: p.key) == nil { ··· 34 } 35 } 36 } 37 - 38 - AsyncFunction("getAllPrefsAsync") { () -> [String:Any]? in 39 var keys: [String] = [] 40 DEFAULTS.forEach { p in 41 keys.append(p.key) 42 } 43 return userDefaults?.dictionaryWithValues(forKeys: keys) 44 } 45 - 46 AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in 47 if let pref = userDefaults?.bool(forKey: forKey) { 48 return pref 49 } 50 return false 51 } 52 - 53 AsyncFunction("getStringAsync") { (forKey: String) -> String? in 54 if let pref = userDefaults?.string(forKey: forKey) { 55 return pref 56 } 57 return nil 58 } 59 - 60 AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in 61 if let pref = userDefaults?.stringArray(forKey: forKey) { 62 return pref 63 } 64 return nil 65 } 66 - 67 - AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in 68 userDefaults?.setValue(value, forKey: forKey) 69 } 70 - 71 - AsyncFunction("setStringAsync") { (forKey: String, value: String) -> Void in 72 userDefaults?.setValue(value, forKey: forKey) 73 } 74 - 75 - AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) -> Void in 76 userDefaults?.setValue(value, forKey: forKey) 77 } 78 - 79 AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in 80 if var curr = userDefaults?.stringArray(forKey: forKey), 81 - !curr.contains(string) 82 - { 83 curr.append(string) 84 userDefaults?.setValue(curr, forKey: forKey) 85 } 86 } 87 - 88 AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in 89 if var curr = userDefaults?.stringArray(forKey: forKey) { 90 curr.removeAll { s in ··· 93 userDefaults?.setValue(curr, forKey: forKey) 94 } 95 } 96 - 97 AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in 98 if var curr = userDefaults?.stringArray(forKey: forKey) { 99 strings.forEach { s in ··· 104 userDefaults?.setValue(curr, forKey: forKey) 105 } 106 } 107 - 108 AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in 109 if var curr = userDefaults?.stringArray(forKey: forKey) { 110 strings.forEach { s in ··· 113 userDefaults?.setValue(curr, forKey: forKey) 114 } 115 } 116 - 117 AsyncFunction("setBadgeCountAsync") { (count: Int) in 118 userDefaults?.setValue(count, forKey: "badgeCount") 119 }
··· 2 3 let APP_GROUP = "group.app.bsky" 4 5 + let DEFAULTS: [String: Any] = [ 6 + "playSoundChat": true, 7 "playSoundFollow": false, 8 "playSoundLike": false, 9 "playSoundMention": false, 10 "playSoundQuote": false, 11 "playSoundReply": false, 12 "playSoundRepost": false, 13 + "mutedThreads": [:] as! [String: [String]], 14 + "badgeCount": 0 15 ] 16 17 /* ··· 23 */ 24 public class ExpoBackgroundNotificationHandlerModule: Module { 25 let userDefaults = UserDefaults(suiteName: APP_GROUP) 26 + 27 public func definition() -> ModuleDefinition { 28 Name("ExpoBackgroundNotificationHandler") 29 + 30 OnCreate { 31 DEFAULTS.forEach { p in 32 if userDefaults?.value(forKey: p.key) == nil { ··· 34 } 35 } 36 } 37 + 38 + AsyncFunction("getAllPrefsAsync") { () -> [String: Any]? in 39 var keys: [String] = [] 40 DEFAULTS.forEach { p in 41 keys.append(p.key) 42 } 43 return userDefaults?.dictionaryWithValues(forKeys: keys) 44 } 45 + 46 AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in 47 if let pref = userDefaults?.bool(forKey: forKey) { 48 return pref 49 } 50 return false 51 } 52 + 53 AsyncFunction("getStringAsync") { (forKey: String) -> String? in 54 if let pref = userDefaults?.string(forKey: forKey) { 55 return pref 56 } 57 return nil 58 } 59 + 60 AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in 61 if let pref = userDefaults?.stringArray(forKey: forKey) { 62 return pref 63 } 64 return nil 65 } 66 + 67 + AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) in 68 userDefaults?.setValue(value, forKey: forKey) 69 } 70 + 71 + AsyncFunction("setStringAsync") { (forKey: String, value: String) in 72 userDefaults?.setValue(value, forKey: forKey) 73 } 74 + 75 + AsyncFunction("setStringArrayAsync") { (forKey: String, value: [String]) in 76 userDefaults?.setValue(value, forKey: forKey) 77 } 78 + 79 AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in 80 if var curr = userDefaults?.stringArray(forKey: forKey), 81 + !curr.contains(string) { 82 curr.append(string) 83 userDefaults?.setValue(curr, forKey: forKey) 84 } 85 } 86 + 87 AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in 88 if var curr = userDefaults?.stringArray(forKey: forKey) { 89 curr.removeAll { s in ··· 92 userDefaults?.setValue(curr, forKey: forKey) 93 } 94 } 95 + 96 AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in 97 if var curr = userDefaults?.stringArray(forKey: forKey) { 98 strings.forEach { s in ··· 103 userDefaults?.setValue(curr, forKey: forKey) 104 } 105 } 106 + 107 AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in 108 if var curr = userDefaults?.stringArray(forKey: forKey) { 109 strings.forEach { s in ··· 112 userDefaults?.setValue(curr, forKey: forKey) 113 } 114 } 115 + 116 AsyncFunction("setBadgeCountAsync") { (count: Int) in 117 userDefaults?.setValue(count, forKey: "badgeCount") 118 }
+5 -2
modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt
··· 5 import android.graphics.drawable.Animatable 6 import androidx.appcompat.widget.AppCompatImageView 7 8 - class AppCompatImageViewExtended(context: Context, private val parent: GifView): AppCompatImageView(context) { 9 override fun onDraw(canvas: Canvas) { 10 super.onDraw(canvas) 11 ··· 34 drawable.start() 35 } 36 } 37 - }
··· 5 import android.graphics.drawable.Animatable 6 import androidx.appcompat.widget.AppCompatImageView 7 8 + class AppCompatImageViewExtended( 9 + context: Context, 10 + private val parent: GifView, 11 + ) : AppCompatImageView(context) { 12 override fun onDraw(canvas: Canvas) { 13 super.onDraw(canvas) 14 ··· 37 drawable.start() 38 } 39 } 40 + }
+34 -33
modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt
··· 6 import expo.modules.kotlin.modules.ModuleDefinition 7 8 class ExpoBlueskyGifViewModule : Module() { 9 - override fun definition() = ModuleDefinition { 10 - Name("ExpoBlueskyGifView") 11 12 - AsyncFunction("prefetchAsync") { sources: List<String> -> 13 - val activity = appContext.currentActivity ?: return@AsyncFunction 14 - val glide = Glide.with(activity) 15 16 - sources.forEach { source -> 17 - glide 18 - .download(source) 19 - .diskCacheStrategy(DiskCacheStrategy.DATA) 20 - .submit() 21 } 22 - } 23 24 - View(GifView::class) { 25 - Events( 26 - "onPlayerStateChange" 27 - ) 28 29 - Prop("source") { view: GifView, source: String -> 30 - view.source = source 31 - } 32 33 - Prop("placeholderSource") { view: GifView, source: String -> 34 - view.placeholderSource = source 35 - } 36 37 - Prop("autoplay") { view: GifView, autoplay: Boolean -> 38 - view.autoplay = autoplay 39 - } 40 41 - AsyncFunction("playAsync") { view: GifView -> 42 - view.play() 43 - } 44 45 - AsyncFunction("pauseAsync") { view: GifView -> 46 - view.pause() 47 - } 48 49 - AsyncFunction("toggleAsync") { view: GifView -> 50 - view.toggle() 51 } 52 } 53 - } 54 }
··· 6 import expo.modules.kotlin.modules.ModuleDefinition 7 8 class ExpoBlueskyGifViewModule : Module() { 9 + override fun definition() = 10 + ModuleDefinition { 11 + Name("ExpoBlueskyGifView") 12 13 + AsyncFunction("prefetchAsync") { sources: List<String> -> 14 + val activity = appContext.currentActivity ?: return@AsyncFunction 15 + val glide = Glide.with(activity) 16 17 + sources.forEach { source -> 18 + glide 19 + .download(source) 20 + .diskCacheStrategy(DiskCacheStrategy.DATA) 21 + .submit() 22 + } 23 } 24 25 + View(GifView::class) { 26 + Events( 27 + "onPlayerStateChange", 28 + ) 29 30 + Prop("source") { view: GifView, source: String -> 31 + view.source = source 32 + } 33 34 + Prop("placeholderSource") { view: GifView, source: String -> 35 + view.placeholderSource = source 36 + } 37 38 + Prop("autoplay") { view: GifView, autoplay: Boolean -> 39 + view.autoplay = autoplay 40 + } 41 42 + AsyncFunction("playAsync") { view: GifView -> 43 + view.play() 44 + } 45 46 + AsyncFunction("pauseAsync") { view: GifView -> 47 + view.pause() 48 + } 49 50 + AsyncFunction("toggleAsync") { view: GifView -> 51 + view.toggle() 52 + } 53 } 54 } 55 }
+75 -70
modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt
··· 1 package expo.modules.blueskygifview 2 3 - 4 import android.content.Context 5 import android.graphics.Color 6 import android.graphics.drawable.Animatable ··· 15 import expo.modules.kotlin.viewevent.EventDispatcher 16 import expo.modules.kotlin.views.ExpoView 17 18 - class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { 19 // Events 20 private val onPlayerStateChange by EventDispatcher() 21 ··· 44 } 45 } 46 47 - 48 - //<editor-fold desc="Lifecycle"> 49 50 init { 51 this.setBackgroundColor(Color.TRANSPARENT) ··· 70 super.onDetachedFromWindow() 71 } 72 73 - //</editor-fold> 74 75 - //<editor-fold desc="Loading"> 76 77 private fun load() { 78 if (placeholderSource == null || source == null) { 79 return 80 } 81 82 - this.webpRequest = glide.load(source) 83 - .diskCacheStrategy(DiskCacheStrategy.DATA) 84 - .skipMemoryCache(false) 85 - .listener(object: RequestListener<Drawable> { 86 - override fun onResourceReady( 87 - resource: Drawable?, 88 - model: Any?, 89 - target: Target<Drawable>?, 90 - dataSource: com.bumptech.glide.load.DataSource?, 91 - isFirstResource: Boolean 92 - ): Boolean { 93 - if (placeholderRequest != null) { 94 - glide.clear(placeholderRequest) 95 - } 96 - return false 97 - } 98 99 - override fun onLoadFailed( 100 - e: GlideException?, 101 - model: Any?, 102 - target: Target<Drawable>?, 103 - isFirstResource: Boolean 104 - ): Boolean { 105 - return true 106 - } 107 - }) 108 - .into(this.imageView) 109 110 if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) { 111 - this.placeholderRequest = glide.load(placeholderSource) 112 - .diskCacheStrategy(DiskCacheStrategy.DATA) 113 - // Let's not bloat the memory cache with placeholders 114 - .skipMemoryCache(true) 115 - .listener(object: RequestListener<Drawable> { 116 - override fun onResourceReady( 117 - resource: Drawable?, 118 - model: Any?, 119 - target: Target<Drawable>?, 120 - dataSource: com.bumptech.glide.load.DataSource?, 121 - isFirstResource: Boolean 122 - ): Boolean { 123 - // Incase this request finishes after the webp, let's just not set 124 - // the drawable. This shouldn't happen because the request should get cancelled 125 - if (imageView.drawable == null) { 126 - imageView.setImageDrawable(resource) 127 - } 128 - return true 129 - } 130 131 - override fun onLoadFailed( 132 - e: GlideException?, 133 - model: Any?, 134 - target: Target<Drawable>?, 135 - isFirstResource: Boolean 136 - ): Boolean { 137 - return true 138 - } 139 - }) 140 - .submit() 141 } 142 } 143 144 - //</editor-fold> 145 146 - //<editor-fold desc="Controls"> 147 148 fun play() { 149 this.imageView.play() ··· 165 } 166 } 167 168 - //</editor-fold> 169 170 - //<editor-fold desc="Util"> 171 172 fun firePlayerStateChange() { 173 - onPlayerStateChange(mapOf( 174 - "isPlaying" to this.isPlaying, 175 - "isLoaded" to this.isLoaded, 176 - )) 177 } 178 179 - //</editor-fold> 180 }
··· 1 package expo.modules.blueskygifview 2 3 import android.content.Context 4 import android.graphics.Color 5 import android.graphics.drawable.Animatable ··· 14 import expo.modules.kotlin.viewevent.EventDispatcher 15 import expo.modules.kotlin.views.ExpoView 16 17 + class GifView( 18 + context: Context, 19 + appContext: AppContext, 20 + ) : ExpoView(context, appContext) { 21 // Events 22 private val onPlayerStateChange by EventDispatcher() 23 ··· 46 } 47 } 48 49 + // <editor-fold desc="Lifecycle"> 50 51 init { 52 this.setBackgroundColor(Color.TRANSPARENT) ··· 71 super.onDetachedFromWindow() 72 } 73 74 + // </editor-fold> 75 76 + // <editor-fold desc="Loading"> 77 78 private fun load() { 79 if (placeholderSource == null || source == null) { 80 return 81 } 82 83 + this.webpRequest = 84 + glide 85 + .load(source) 86 + .diskCacheStrategy(DiskCacheStrategy.DATA) 87 + .skipMemoryCache(false) 88 + .listener( 89 + object : RequestListener<Drawable> { 90 + override fun onResourceReady( 91 + resource: Drawable?, 92 + model: Any?, 93 + target: Target<Drawable>?, 94 + dataSource: com.bumptech.glide.load.DataSource?, 95 + isFirstResource: Boolean, 96 + ): Boolean { 97 + if (placeholderRequest != null) { 98 + glide.clear(placeholderRequest) 99 + } 100 + return false 101 + } 102 103 + override fun onLoadFailed( 104 + e: GlideException?, 105 + model: Any?, 106 + target: Target<Drawable>?, 107 + isFirstResource: Boolean, 108 + ): Boolean = true 109 + }, 110 + ).into(this.imageView) 111 112 if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) { 113 + this.placeholderRequest = 114 + glide 115 + .load(placeholderSource) 116 + .diskCacheStrategy(DiskCacheStrategy.DATA) 117 + // Let's not bloat the memory cache with placeholders 118 + .skipMemoryCache(true) 119 + .listener( 120 + object : RequestListener<Drawable> { 121 + override fun onResourceReady( 122 + resource: Drawable?, 123 + model: Any?, 124 + target: Target<Drawable>?, 125 + dataSource: com.bumptech.glide.load.DataSource?, 126 + isFirstResource: Boolean, 127 + ): Boolean { 128 + // Incase this request finishes after the webp, let's just not set 129 + // the drawable. This shouldn't happen because the request should get cancelled 130 + if (imageView.drawable == null) { 131 + imageView.setImageDrawable(resource) 132 + } 133 + return true 134 + } 135 136 + override fun onLoadFailed( 137 + e: GlideException?, 138 + model: Any?, 139 + target: Target<Drawable>?, 140 + isFirstResource: Boolean, 141 + ): Boolean = true 142 + }, 143 + ).submit() 144 } 145 } 146 147 + // </editor-fold> 148 149 + // <editor-fold desc="Controls"> 150 151 fun play() { 152 this.imageView.play() ··· 168 } 169 } 170 171 + // </editor-fold> 172 173 + // <editor-fold desc="Util"> 174 175 fun firePlayerStateChange() { 176 + onPlayerStateChange( 177 + mapOf( 178 + "isPlaying" to this.isPlaying, 179 + "isLoaded" to this.isLoaded, 180 + ), 181 + ) 182 } 183 184 + // </editor-fold> 185 }
+8 -8
modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift
··· 5 public class ExpoBlueskyGifViewModule: Module { 6 public func definition() -> ModuleDefinition { 7 Name("ExpoBlueskyGifView") 8 - 9 OnCreate { 10 SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared) 11 } 12 - 13 AsyncFunction("prefetchAsync") { (sources: [URL]) in 14 SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil) 15 } ··· 18 Events( 19 "onPlayerStateChange" 20 ) 21 - 22 Prop("source") { (view: GifView, prop: String) in 23 view.source = prop 24 } 25 - 26 Prop("placeholderSource") { (view: GifView, prop: String) in 27 view.placeholderSource = prop 28 } 29 - 30 Prop("autoplay") { (view: GifView, prop: Bool) in 31 view.autoplay = prop 32 } 33 - 34 AsyncFunction("toggleAsync") { (view: GifView) in 35 view.toggle() 36 } 37 - 38 AsyncFunction("playAsync") { (view: GifView) in 39 view.play() 40 } 41 - 42 AsyncFunction("pauseAsync") { (view: GifView) in 43 view.pause() 44 }
··· 5 public class ExpoBlueskyGifViewModule: Module { 6 public func definition() -> ModuleDefinition { 7 Name("ExpoBlueskyGifView") 8 + 9 OnCreate { 10 SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared) 11 } 12 + 13 AsyncFunction("prefetchAsync") { (sources: [URL]) in 14 SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil) 15 } ··· 18 Events( 19 "onPlayerStateChange" 20 ) 21 + 22 Prop("source") { (view: GifView, prop: String) in 23 view.source = prop 24 } 25 + 26 Prop("placeholderSource") { (view: GifView, prop: String) in 27 view.placeholderSource = prop 28 } 29 + 30 Prop("autoplay") { (view: GifView, prop: Bool) in 31 view.autoplay = prop 32 } 33 + 34 AsyncFunction("toggleAsync") { (view: GifView) in 35 view.toggle() 36 } 37 + 38 AsyncFunction("playAsync") { (view: GifView) in 39 view.play() 40 } 41 + 42 AsyncFunction("pauseAsync") { (view: GifView) in 43 view.pause() 44 }
+6 -9
modules/expo-bluesky-gif-view/ios/GifView.swift
··· 16 ) 17 private var isPlaying = true 18 private var isLoaded = false 19 - 20 // Requests 21 private var webpOperation: SDWebImageCombinedOperation? 22 private var placeholderOperation: SDWebImageCombinedOperation? 23 24 // Props 25 - var source: String? = nil 26 - var placeholderSource: String? = nil 27 var autoplay = true { 28 didSet { 29 if !autoplay { ··· 78 // See: 79 // https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently 80 if !SDImageCache.shared.diskImageDataExists(withKey: source), 81 - let url = URL(string: placeholderSource) 82 - { 83 self.placeholderOperation = imageManager.loadImage( 84 with: url, 85 options: [.retryFailed], ··· 132 if let placeholderSource = self.placeholderSource, 133 imageUrl?.absoluteString == placeholderSource, 134 self.imageView.image == nil, 135 - let image = image 136 - { 137 self.setImage(image) 138 return 139 } ··· 142 imageUrl?.absoluteString == source, 143 // UIImage perf suckssss if the image is animated 144 let data = data, 145 - let animatedImage = SDAnimatedImage(data: data) 146 - { 147 self.placeholderOperation?.cancel() 148 self.isPlaying = self.autoplay 149 self.isLoaded = true
··· 16 ) 17 private var isPlaying = true 18 private var isLoaded = false 19 + 20 // Requests 21 private var webpOperation: SDWebImageCombinedOperation? 22 private var placeholderOperation: SDWebImageCombinedOperation? 23 24 // Props 25 + var source: String? 26 + var placeholderSource: String? 27 var autoplay = true { 28 didSet { 29 if !autoplay { ··· 78 // See: 79 // https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently 80 if !SDImageCache.shared.diskImageDataExists(withKey: source), 81 + let url = URL(string: placeholderSource) { 82 self.placeholderOperation = imageManager.loadImage( 83 with: url, 84 options: [.retryFailed], ··· 131 if let placeholderSource = self.placeholderSource, 132 imageUrl?.absoluteString == placeholderSource, 133 self.imageView.image == nil, 134 + let image = image { 135 self.setImage(image) 136 return 137 } ··· 140 imageUrl?.absoluteString == source, 141 // UIImage perf suckssss if the image is animated 142 let data = data, 143 + let animatedImage = SDAnimatedImage(data: data) { 144 self.placeholderOperation?.cancel() 145 self.isPlaying = self.autoplay 146 self.isLoaded = true
+4 -3
modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/deviceprefs/ExpoBlueskyDevicePrefsModule.kt
··· 4 import expo.modules.kotlin.modules.ModuleDefinition 5 6 class ExpoBlueskyDevicePrefsModule : Module() { 7 - override fun definition() = ModuleDefinition { 8 - Name("ExpoBlueskyDevicePrefs") 9 - } 10 }
··· 4 import expo.modules.kotlin.modules.ModuleDefinition 5 6 class ExpoBlueskyDevicePrefsModule : Module() { 7 + override fun definition() = 8 + ModuleDefinition { 9 + Name("ExpoBlueskyDevicePrefs") 10 + } 11 }
+43 -40
modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/referrer/ExpoBlueskyReferrerModule.kt
··· 3 import android.util.Log 4 import com.android.installreferrer.api.InstallReferrerClient 5 import com.android.installreferrer.api.InstallReferrerStateListener 6 import expo.modules.kotlin.modules.Module 7 import expo.modules.kotlin.modules.ModuleDefinition 8 - import expo.modules.kotlin.Promise 9 10 class ExpoBlueskyReferrerModule : Module() { 11 - override fun definition() = ModuleDefinition { 12 - Name("ExpoBlueskyReferrer") 13 14 - AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> 15 - val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() 16 - referrerClient.startConnection(object : InstallReferrerStateListener { 17 - override fun onInstallReferrerSetupFinished(responseCode: Int) { 18 - if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { 19 - Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") 20 21 - val response = referrerClient.installReferrer 22 - Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") 23 24 - promise.resolve( 25 - mapOf( 26 - "installReferrer" to response.installReferrer, 27 - "clickTimestamp" to response.referrerClickTimestampSeconds, 28 - "installTimestamp" to response.installBeginTimestampSeconds 29 ) 30 - ) 31 - } else { 32 - Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.") 33 - promise.reject( 34 - "ERR_GOOGLE_PLAY_REFERRER_UNKNOWN", 35 - "Failed to get referrer info", 36 - Exception("Failed to get referrer info") 37 - ) 38 - } 39 - referrerClient.endConnection() 40 - } 41 - 42 - override fun onInstallReferrerServiceDisconnected() { 43 - Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.") 44 - referrerClient.endConnection() 45 - promise.reject( 46 - "ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED", 47 - "Failed to get referrer info", 48 - Exception("Failed to get referrer info") 49 - ) 50 - } 51 - }) 52 } 53 - } 54 - }
··· 3 import android.util.Log 4 import com.android.installreferrer.api.InstallReferrerClient 5 import com.android.installreferrer.api.InstallReferrerStateListener 6 + import expo.modules.kotlin.Promise 7 import expo.modules.kotlin.modules.Module 8 import expo.modules.kotlin.modules.ModuleDefinition 9 10 class ExpoBlueskyReferrerModule : Module() { 11 + override fun definition() = 12 + ModuleDefinition { 13 + Name("ExpoBlueskyReferrer") 14 15 + AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> 16 + val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() 17 + referrerClient.startConnection( 18 + object : InstallReferrerStateListener { 19 + override fun onInstallReferrerSetupFinished(responseCode: Int) { 20 + if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { 21 + Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") 22 23 + val response = referrerClient.installReferrer 24 + Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") 25 + 26 + promise.resolve( 27 + mapOf( 28 + "installReferrer" to response.installReferrer, 29 + "clickTimestamp" to response.referrerClickTimestampSeconds, 30 + "installTimestamp" to response.installBeginTimestampSeconds, 31 + ), 32 + ) 33 + } else { 34 + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Unknown error.") 35 + promise.reject( 36 + "ERR_GOOGLE_PLAY_REFERRER_UNKNOWN", 37 + "Failed to get referrer info", 38 + Exception("Failed to get referrer info"), 39 + ) 40 + } 41 + referrerClient.endConnection() 42 + } 43 44 + override fun onInstallReferrerServiceDisconnected() { 45 + Log.d("ExpoGooglePlayReferrer", "Failed to get referrer info. Service disconnected.") 46 + referrerClient.endConnection() 47 + promise.reject( 48 + "ERR_GOOGLE_PLAY_REFERRER_DISCONNECTED", 49 + "Failed to get referrer info", 50 + Exception("Failed to get referrer info"), 51 ) 52 + } 53 + }, 54 + ) 55 + } 56 } 57 + }
+19 -17
modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt
··· 13 import java.net.URLEncoder 14 15 class ExpoReceiveAndroidIntentsModule : Module() { 16 - override fun definition() = ModuleDefinition { 17 - Name("ExpoReceiveAndroidIntents") 18 19 - OnNewIntent { 20 - handleIntent(it) 21 } 22 - } 23 24 private fun handleIntent(intent: Intent?) { 25 - if(appContext.currentActivity == null || intent == null) return 26 27 if (intent.action == Intent.ACTION_SEND) { 28 if (intent.type == "text/plain") { ··· 40 private fun handleTextIntent(intent: Intent) { 41 intent.getStringExtra(Intent.EXTRA_TEXT)?.let { 42 val encoded = URLEncoder.encode(it, "UTF-8") 43 - "bluesky://intent/compose?text=${encoded}".toUri().let { uri -> 44 val newIntent = Intent(Intent.ACTION_VIEW, uri) 45 appContext.currentActivity?.startActivity(newIntent) 46 } ··· 48 } 49 50 private fun handleImageIntent(intent: Intent) { 51 - val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 52 - intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) 53 - } else { 54 - intent.getParcelableExtra(Intent.EXTRA_STREAM) 55 - } 56 if (uri == null) return 57 58 handleImageIntents(listOf(uri)) ··· 76 uris.forEachIndexed { index, uri -> 77 val info = getImageInfo(uri) 78 val params = buildUriData(info) 79 - allParams = "${allParams}${params}" 80 81 if (index < uris.count() - 1) { 82 - allParams = "${allParams}," 83 } 84 } 85 86 val encoded = URLEncoder.encode(allParams, "UTF-8") 87 88 - "bluesky://intent/compose?imageUris=${encoded}".toUri().let { 89 val newIntent = Intent(Intent.ACTION_VIEW, it) 90 appContext.currentActivity?.startActivity(newIntent) 91 } ··· 104 return mapOf( 105 "width" to bitmap.width, 106 "height" to bitmap.height, 107 - "path" to file.path.toString() 108 ) 109 } 110 ··· 114 val path = info.getValue("path") 115 val width = info.getValue("width") 116 val height = info.getValue("height") 117 - return "file://${path}|${width}|${height}" 118 } 119 }
··· 13 import java.net.URLEncoder 14 15 class ExpoReceiveAndroidIntentsModule : Module() { 16 + override fun definition() = 17 + ModuleDefinition { 18 + Name("ExpoReceiveAndroidIntents") 19 20 + OnNewIntent { 21 + handleIntent(it) 22 + } 23 } 24 25 private fun handleIntent(intent: Intent?) { 26 + if (appContext.currentActivity == null || intent == null) return 27 28 if (intent.action == Intent.ACTION_SEND) { 29 if (intent.type == "text/plain") { ··· 41 private fun handleTextIntent(intent: Intent) { 42 intent.getStringExtra(Intent.EXTRA_TEXT)?.let { 43 val encoded = URLEncoder.encode(it, "UTF-8") 44 + "bluesky://intent/compose?text=$encoded".toUri().let { uri -> 45 val newIntent = Intent(Intent.ACTION_VIEW, uri) 46 appContext.currentActivity?.startActivity(newIntent) 47 } ··· 49 } 50 51 private fun handleImageIntent(intent: Intent) { 52 + val uri = 53 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 54 + intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) 55 + } else { 56 + intent.getParcelableExtra(Intent.EXTRA_STREAM) 57 + } 58 if (uri == null) return 59 60 handleImageIntents(listOf(uri)) ··· 78 uris.forEachIndexed { index, uri -> 79 val info = getImageInfo(uri) 80 val params = buildUriData(info) 81 + allParams = "${allParams}$params" 82 83 if (index < uris.count() - 1) { 84 + allParams = "$allParams," 85 } 86 } 87 88 val encoded = URLEncoder.encode(allParams, "UTF-8") 89 90 + "bluesky://intent/compose?imageUris=$encoded".toUri().let { 91 val newIntent = Intent(Intent.ACTION_VIEW, it) 92 appContext.currentActivity?.startActivity(newIntent) 93 } ··· 106 return mapOf( 107 "width" to bitmap.width, 108 "height" to bitmap.height, 109 + "path" to file.path.toString(), 110 ) 111 } 112 ··· 116 val path = info.getValue("path") 117 val width = info.getValue("width") 118 val height = info.getValue("height") 119 + return "file://$path|$width|$height" 120 } 121 }
+1 -1
modules/expo-scroll-forwarder/ios/ExpoScrollForwarderModule.swift
··· 3 public class ExpoScrollForwarderModule: Module { 4 public func definition() -> ModuleDefinition { 5 Name("ExpoScrollForwarder") 6 - 7 View(ExpoScrollForwarderView.self) { 8 Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in 9 view.scrollViewTag = prop
··· 3 public class ExpoScrollForwarderModule: Module { 4 public func definition() -> ModuleDefinition { 5 Name("ExpoScrollForwarder") 6 + 7 View(ExpoScrollForwarderView.self) { 8 Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in 9 view.scrollViewTag = prop
+37 -39
modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift
··· 8 self.tryFindScrollView() 9 } 10 } 11 - 12 private var rctScrollView: RCTScrollView? 13 private var rctRefreshCtrl: RCTRefreshControl? 14 private var cancelGestureRecognizers: [UIGestureRecognizer]? 15 private var animTimer: Timer? 16 private var initialOffset: CGFloat = 0.0 17 private var didImpact: Bool = false 18 - 19 required init(appContext: AppContext? = nil) { 20 super.init(appContext: appContext) 21 - 22 let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:))) 23 pg.delegate = self 24 self.addGestureRecognizer(pg) ··· 34 35 self.cancelGestureRecognizers = [lpg, tg] 36 } 37 - 38 39 // We don't want to recognize the scroll pan gesture and the swipe back gesture together 40 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 41 if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer { 42 return false 43 } 44 - 45 return true 46 } 47 - 48 // We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will 49 // interfere with the native swipe back gesture. 50 override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 51 guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { 52 return true 53 } 54 - 55 let velocity = gestureRecognizer.velocity(in: self) 56 return abs(velocity.y) > abs(velocity.x) 57 } 58 - 59 // This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another 60 // recognizer for this one. 61 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { ··· 64 65 // This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change 66 // the scroll view gesture's delegate, so we add an additional recognizer to detect this. 67 - @IBAction func callOnPress(_ sender: UITapGestureRecognizer) -> Void { 68 self.stopTimer() 69 } 70 - 71 - @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) -> Void { 72 guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else { 73 return 74 } 75 76 let translation = sender.translation(in: self).y 77 - 78 if sender.state == .began { 79 if sv.contentOffset.y < 0 { 80 sv.contentOffset.y = 0 81 } 82 - 83 self.initialOffset = sv.contentOffset.y 84 } 85 86 if sender.state == .changed { 87 sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset) 88 - 89 if sv.contentOffset.y <= -130, !didImpact { 90 let generator = UIImpactFeedbackGenerator(style: .light) 91 generator.impactOccurred() 92 - 93 self.didImpact = true 94 } 95 } ··· 97 if sender.state == .ended { 98 let velocity = sender.velocity(in: self).y 99 self.didImpact = false 100 - 101 if sv.contentOffset.y <= -130 { 102 self.rctRefreshCtrl?.forwarderBeginRefreshing() 103 return ··· 108 if abs(velocity) < 250, sv.contentOffset.y >= 0 { 109 return 110 } 111 - 112 self.startDecayAnimation(translation, velocity) 113 } 114 } 115 - 116 func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) { 117 guard let sv = self.rctScrollView?.scrollView else { 118 return 119 } 120 - 121 var velocity = velocity 122 - 123 self.enableCancelGestureRecognizers() 124 - 125 if velocity > 0 { 126 velocity = min(velocity, 5000) 127 } else { 128 velocity = max(velocity, -5000) 129 } 130 - 131 var animTranslation = -translation 132 - self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { timer in 133 velocity *= 0.9875 134 animTranslation = (-velocity / 120) + animTranslation 135 - 136 let nextOffset = self.dampenOffset(animTranslation + self.initialOffset) 137 - 138 if nextOffset <= 0 { 139 if self.initialOffset <= 1 { 140 self.scrollToOffset(0) 141 } else { 142 sv.contentOffset.y = 0 143 } 144 - 145 self.stopTimer() 146 return 147 } else { ··· 153 } 154 } 155 } 156 - 157 func dampenOffset(_ offset: CGFloat) -> CGFloat { 158 if offset < 0 { 159 return offset - (offset * 0.55) 160 } 161 - 162 return offset 163 } 164 - 165 func tryFindScrollView() { 166 guard let scrollViewTag = scrollViewTag else { 167 return 168 } 169 - 170 // Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer. 171 // Otherwise we might end up with duplicates when we switch back to that scrollview. 172 self.removeCancelGestureRecognizers() 173 - 174 self.rctScrollView = self.appContext? 175 .findView(withTag: scrollViewTag, ofType: RCTScrollView.self) 176 self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl 177 - 178 self.addCancelGestureRecognizers() 179 } 180 - 181 func addCancelGestureRecognizers() { 182 self.cancelGestureRecognizers?.forEach { r in 183 self.rctScrollView?.scrollView?.addGestureRecognizer(r) 184 } 185 } 186 - 187 func removeCancelGestureRecognizers() { 188 self.cancelGestureRecognizers?.forEach { r in 189 self.rctScrollView?.scrollView?.removeGestureRecognizer(r) 190 } 191 } 192 193 - 194 func enableCancelGestureRecognizers() { 195 self.cancelGestureRecognizers?.forEach { r in 196 r.isEnabled = true 197 } 198 } 199 - 200 func disableCancelGestureRecognizers() { 201 self.cancelGestureRecognizers?.forEach { r in 202 r.isEnabled = false 203 } 204 } 205 - 206 - func scrollToOffset(_ offset: Int, animated: Bool = true) -> Void { 207 self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated) 208 } 209 210 - func stopTimer() -> Void { 211 self.disableCancelGestureRecognizers() 212 self.animTimer?.invalidate() 213 self.animTimer = nil
··· 8 self.tryFindScrollView() 9 } 10 } 11 + 12 private var rctScrollView: RCTScrollView? 13 private var rctRefreshCtrl: RCTRefreshControl? 14 private var cancelGestureRecognizers: [UIGestureRecognizer]? 15 private var animTimer: Timer? 16 private var initialOffset: CGFloat = 0.0 17 private var didImpact: Bool = false 18 + 19 required init(appContext: AppContext? = nil) { 20 super.init(appContext: appContext) 21 + 22 let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:))) 23 pg.delegate = self 24 self.addGestureRecognizer(pg) ··· 34 35 self.cancelGestureRecognizers = [lpg, tg] 36 } 37 38 // We don't want to recognize the scroll pan gesture and the swipe back gesture together 39 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 40 if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer { 41 return false 42 } 43 + 44 return true 45 } 46 + 47 // We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will 48 // interfere with the native swipe back gesture. 49 override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 50 guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { 51 return true 52 } 53 + 54 let velocity = gestureRecognizer.velocity(in: self) 55 return abs(velocity.y) > abs(velocity.x) 56 } 57 + 58 // This will be used to cancel the scroll animation whenever we tap inside of the header. We don't need another 59 // recognizer for this one. 60 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { ··· 63 64 // This will be used to cancel the animation whenever we press inside of the scroll view. We don't want to change 65 // the scroll view gesture's delegate, so we add an additional recognizer to detect this. 66 + @IBAction func callOnPress(_ sender: UITapGestureRecognizer) { 67 self.stopTimer() 68 } 69 + 70 + @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) { 71 guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else { 72 return 73 } 74 75 let translation = sender.translation(in: self).y 76 + 77 if sender.state == .began { 78 if sv.contentOffset.y < 0 { 79 sv.contentOffset.y = 0 80 } 81 + 82 self.initialOffset = sv.contentOffset.y 83 } 84 85 if sender.state == .changed { 86 sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset) 87 + 88 if sv.contentOffset.y <= -130, !didImpact { 89 let generator = UIImpactFeedbackGenerator(style: .light) 90 generator.impactOccurred() 91 + 92 self.didImpact = true 93 } 94 } ··· 96 if sender.state == .ended { 97 let velocity = sender.velocity(in: self).y 98 self.didImpact = false 99 + 100 if sv.contentOffset.y <= -130 { 101 self.rctRefreshCtrl?.forwarderBeginRefreshing() 102 return ··· 107 if abs(velocity) < 250, sv.contentOffset.y >= 0 { 108 return 109 } 110 + 111 self.startDecayAnimation(translation, velocity) 112 } 113 } 114 + 115 func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) { 116 guard let sv = self.rctScrollView?.scrollView else { 117 return 118 } 119 + 120 var velocity = velocity 121 + 122 self.enableCancelGestureRecognizers() 123 + 124 if velocity > 0 { 125 velocity = min(velocity, 5000) 126 } else { 127 velocity = max(velocity, -5000) 128 } 129 + 130 var animTranslation = -translation 131 + self.animTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 120, repeats: true) { _ in 132 velocity *= 0.9875 133 animTranslation = (-velocity / 120) + animTranslation 134 + 135 let nextOffset = self.dampenOffset(animTranslation + self.initialOffset) 136 + 137 if nextOffset <= 0 { 138 if self.initialOffset <= 1 { 139 self.scrollToOffset(0) 140 } else { 141 sv.contentOffset.y = 0 142 } 143 + 144 self.stopTimer() 145 return 146 } else { ··· 152 } 153 } 154 } 155 + 156 func dampenOffset(_ offset: CGFloat) -> CGFloat { 157 if offset < 0 { 158 return offset - (offset * 0.55) 159 } 160 + 161 return offset 162 } 163 + 164 func tryFindScrollView() { 165 guard let scrollViewTag = scrollViewTag else { 166 return 167 } 168 + 169 // Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer. 170 // Otherwise we might end up with duplicates when we switch back to that scrollview. 171 self.removeCancelGestureRecognizers() 172 + 173 self.rctScrollView = self.appContext? 174 .findView(withTag: scrollViewTag, ofType: RCTScrollView.self) 175 self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl 176 + 177 self.addCancelGestureRecognizers() 178 } 179 + 180 func addCancelGestureRecognizers() { 181 self.cancelGestureRecognizers?.forEach { r in 182 self.rctScrollView?.scrollView?.addGestureRecognizer(r) 183 } 184 } 185 + 186 func removeCancelGestureRecognizers() { 187 self.cancelGestureRecognizers?.forEach { r in 188 self.rctScrollView?.scrollView?.removeGestureRecognizer(r) 189 } 190 } 191 192 func enableCancelGestureRecognizers() { 193 self.cancelGestureRecognizers?.forEach { r in 194 r.isEnabled = true 195 } 196 } 197 + 198 func disableCancelGestureRecognizers() { 199 self.cancelGestureRecognizers?.forEach { r in 200 r.isEnabled = false 201 } 202 } 203 + 204 + func scrollToOffset(_ offset: Int, animated: Bool = true) { 205 self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated) 206 } 207 208 + func stopTimer() { 209 self.disableCancelGestureRecognizers() 210 self.animTimer?.invalidate() 211 self.animTimer = nil
+2
package.json
··· 29 "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit", 30 "test-coverage": "NODE_ENV=test jest --coverage", 31 "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src", 32 "typecheck": "tsc --project ./tsconfig.check.json", 33 "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts", 34 "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
··· 29 "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit", 30 "test-coverage": "NODE_ENV=test jest --coverage", 31 "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src", 32 + "lint-native": "swiftlint ./modules && ktlint ./modules", 33 + "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", 34 "typecheck": "tsc --project ./tsconfig.check.json", 35 "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts", 36 "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",