Lint native files (#4768)

zio/stable
Hailey 2024-07-11 18:15:35 -07:00 committed by GitHub
parent b433469ab9
commit 2397104ad6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 393 additions and 375 deletions

2
.editorconfig 100644
View File

@ -0,0 +1,2 @@
[*.{kt,kts}]
indent_size=2

View File

@ -13,43 +13,43 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(request.content) contentHandler(request.content)
return return
} }
if reason == "chat-message" { if reason == "chat-message" {
mutateWithChatMessage(bestAttempt) mutateWithChatMessage(bestAttempt)
} else { } else {
mutateWithBadge(bestAttempt) mutateWithBadge(bestAttempt)
} }
contentHandler(bestAttempt) contentHandler(bestAttempt)
} }
override func serviceExtensionTimeWillExpire() { override func serviceExtensionTimeWillExpire() {
// If for some reason the alloted time expires, we don't actually want to display a notification // If for some reason the alloted time expires, we don't actually want to display a notification
} }
func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? { func createCopy(_ content: UNNotificationContent) -> UNMutableNotificationContent? {
return content.mutableCopy() as? UNMutableNotificationContent return content.mutableCopy() as? UNMutableNotificationContent
} }
func mutateWithBadge(_ content: UNMutableNotificationContent) { func mutateWithBadge(_ content: UNMutableNotificationContent) {
var count = prefs?.integer(forKey: "badgeCount") ?? 0 var count = prefs?.integer(forKey: "badgeCount") ?? 0
count += 1 count += 1
// Set the new badge number for the notification, then store that value for using later // Set the new badge number for the notification, then store that value for using later
content.badge = NSNumber(value: count) content.badge = NSNumber(value: count)
prefs?.setValue(count, forKey: "badgeCount") prefs?.setValue(count, forKey: "badgeCount")
} }
func mutateWithChatMessage(_ content: UNMutableNotificationContent) { func mutateWithChatMessage(_ content: UNMutableNotificationContent) {
if self.prefs?.bool(forKey: "playSoundChat") == true { if self.prefs?.bool(forKey: "playSoundChat") == true {
mutateWithDmSound(content) mutateWithDmSound(content)
} }
} }
func mutateWithDefaultSound(_ content: UNMutableNotificationContent) { func mutateWithDefaultSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound.default content.sound = UNNotificationSound.default
} }
func mutateWithDmSound(_ content: UNMutableNotificationContent) { func mutateWithDmSound(_ content: UNMutableNotificationContent) {
content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff")) content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "dm.aiff"))
} }

View File

@ -30,12 +30,11 @@ class ShareViewController: UIViewController {
} }
} }
private func handleText(item: NSItemProvider) async -> Void { private func handleText(item: NSItemProvider) async {
do { do {
if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String { if let data = try await item.loadItem(forTypeIdentifier: "public.text") as? String {
if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 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) _ = self.openURL(url)
} }
} }
@ -45,12 +44,11 @@ class ShareViewController: UIViewController {
} }
} }
private func handleUrl(item: NSItemProvider) async -> Void { private func handleUrl(item: NSItemProvider) async {
do { do {
if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL { if let data = try await item.loadItem(forTypeIdentifier: "public.url") as? URL {
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 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) _ = 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] let firstFourItems: [NSItemProvider]
if items.count < 4 { if items.count < 4 {
firstFourItems = items firstFourItems = items
@ -72,7 +70,7 @@ class ShareViewController: UIViewController {
var imageUris = "" var imageUris = ""
for (index, item) in firstFourItems.enumerated() { for (index, item) in firstFourItems.enumerated() {
var imageUriInfo: String? = nil var imageUriInfo: String?
do { do {
if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL { if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL {
@ -100,8 +98,7 @@ class ShareViewController: UIViewController {
if valid, if valid,
let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 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) _ = self.openURL(url)
} }
@ -119,13 +116,11 @@ class ShareViewController: UIViewController {
// extension does. // extension does.
if let dir = FileManager() if let dir = FileManager()
.containerURL( .containerURL(
forSecurityApplicationGroupIdentifier: "group.app.bsky") forSecurityApplicationGroupIdentifier: "group.app.bsky") {
{
let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg" let filePath = "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).jpeg"
if let newUri = URL(string: filePath), if let newUri = URL(string: filePath),
let jpegData = image.jpegData(compressionQuality: 1) let jpegData = image.jpegData(compressionQuality: 1) {
{
try jpegData.write(to: newUri) try jpegData.write(to: newUri)
return "\(newUri.absoluteString)|\(image.size.width)|\(image.size.height)" 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) self.extensionContext?.completeRequest(returningItems: nil)
} }

View File

@ -5,7 +5,7 @@ import com.google.firebase.messaging.RemoteMessage
class BackgroundNotificationHandler( class BackgroundNotificationHandler(
private val context: Context, private val context: Context,
private val notifInterface: BackgroundNotificationHandlerInterface private val notifInterface: BackgroundNotificationHandlerInterface,
) { ) {
fun handleMessage(remoteMessage: RemoteMessage) { fun handleMessage(remoteMessage: RemoteMessage) {
if (ExpoBackgroundNotificationHandlerModule.isForegrounded) { if (ExpoBackgroundNotificationHandlerModule.isForegrounded) {

View File

@ -8,67 +8,68 @@ class ExpoBackgroundNotificationHandlerModule : Module() {
var isForegrounded = false var isForegrounded = false
} }
override fun definition() = ModuleDefinition { override fun definition() =
Name("ExpoBackgroundNotificationHandler") ModuleDefinition {
Name("ExpoBackgroundNotificationHandler")
OnCreate { OnCreate {
NotificationPrefs(appContext.reactContext).initialize() NotificationPrefs(appContext.reactContext).initialize()
} }
OnActivityEntersForeground { OnActivityEntersForeground {
isForegrounded = true isForegrounded = true
} }
OnActivityEntersBackground { OnActivityEntersBackground {
isForegrounded = false isForegrounded = false
} }
AsyncFunction("getAllPrefsAsync") { AsyncFunction("getAllPrefsAsync") {
return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs() return@AsyncFunction NotificationPrefs(appContext.reactContext).getAllPrefs()
} }
AsyncFunction("getBoolAsync") { forKey: String -> AsyncFunction("getBoolAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey) return@AsyncFunction NotificationPrefs(appContext.reactContext).getBoolean(forKey)
} }
AsyncFunction("getStringAsync") { forKey: String -> AsyncFunction("getStringAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey) return@AsyncFunction NotificationPrefs(appContext.reactContext).getString(forKey)
} }
AsyncFunction("getStringArrayAsync") { forKey: String -> AsyncFunction("getStringArrayAsync") { forKey: String ->
return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey) return@AsyncFunction NotificationPrefs(appContext.reactContext).getStringArray(forKey)
} }
AsyncFunction("setBoolAsync") { forKey: String, value: Boolean -> AsyncFunction("setBoolAsync") { forKey: String, value: Boolean ->
NotificationPrefs(appContext.reactContext).setBoolean(forKey, value) NotificationPrefs(appContext.reactContext).setBoolean(forKey, value)
} }
AsyncFunction("setStringAsync") { forKey: String, value: String -> AsyncFunction("setStringAsync") { forKey: String, value: String ->
NotificationPrefs(appContext.reactContext).setString(forKey, value) NotificationPrefs(appContext.reactContext).setString(forKey, value)
} }
AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> -> AsyncFunction("setStringArrayAsync") { forKey: String, value: Array<String> ->
NotificationPrefs(appContext.reactContext).setStringArray(forKey, value) NotificationPrefs(appContext.reactContext).setStringArray(forKey, value)
} }
AsyncFunction("addToStringArrayAsync") { forKey: String, string: String -> AsyncFunction("addToStringArrayAsync") { forKey: String, string: String ->
NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string) NotificationPrefs(appContext.reactContext).addToStringArray(forKey, string)
} }
AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String -> AsyncFunction("removeFromStringArrayAsync") { forKey: String, string: String ->
NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string) NotificationPrefs(appContext.reactContext).removeFromStringArray(forKey, string)
} }
AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> -> AsyncFunction("addManyToStringArrayAsync") { forKey: String, strings: Array<String> ->
NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings) NotificationPrefs(appContext.reactContext).addManyToStringArray(forKey, strings)
} }
AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> -> AsyncFunction("removeManyFromStringArrayAsync") { forKey: String, strings: Array<String> ->
NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings) NotificationPrefs(appContext.reactContext).removeManyFromStringArray(forKey, strings)
} }
AsyncFunction("setBadgeCountAsync") { _: Int -> AsyncFunction("setBadgeCountAsync") { _: Int ->
// This does nothing on Android // This does nothing on Android
}
} }
}
} }

View File

@ -2,20 +2,24 @@ package expo.modules.backgroundnotificationhandler
import android.content.Context import android.content.Context
val DEFAULTS = mapOf<String, Any>( val DEFAULTS =
"playSoundChat" to true, mapOf<String, Any>(
"playSoundFollow" to false, "playSoundChat" to true,
"playSoundLike" to false, "playSoundFollow" to false,
"playSoundMention" to false, "playSoundLike" to false,
"playSoundQuote" to false, "playSoundMention" to false,
"playSoundReply" to false, "playSoundQuote" to false,
"playSoundRepost" to false, "playSoundReply" to false,
"mutedThreads" to mapOf<String, List<String>>() "playSoundRepost" to false,
) "mutedThreads" to mapOf<String, List<String>>(),
)
class NotificationPrefs (private val context: Context?) { class NotificationPrefs(
private val prefs = context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE) private val context: Context?,
?: throw Error("Context is null") ) {
private val prefs =
context?.getSharedPreferences("xyz.blueskyweb.app", Context.MODE_PRIVATE)
?: throw Error("Context is null")
fun initialize() { fun initialize() {
prefs prefs
@ -41,94 +45,99 @@ class NotificationPrefs (private val context: Context?) {
} }
} }
} }
} }.apply()
.apply()
} }
fun getAllPrefs(): MutableMap<String, *> { fun getAllPrefs(): MutableMap<String, *> = prefs.all
return prefs.all
}
fun getBoolean(key: String): Boolean { fun getBoolean(key: String): Boolean = prefs.getBoolean(key, false)
return prefs.getBoolean(key, false)
}
fun getString(key: String): String? { fun getString(key: String): String? = prefs.getString(key, null)
return prefs.getString(key, null)
}
fun getStringArray(key: String): Array<String>? { fun getStringArray(key: String): Array<String>? = prefs.getStringSet(key, null)?.toTypedArray()
return prefs.getStringSet(key, null)?.toTypedArray()
}
fun setBoolean(key: String, value: Boolean) { fun setBoolean(
key: String,
value: Boolean,
) {
prefs prefs
.edit() .edit()
.apply { .apply {
putBoolean(key, value) putBoolean(key, value)
} }.apply()
.apply()
} }
fun setString(key: String, value: String) { fun setString(
key: String,
value: String,
) {
prefs prefs
.edit() .edit()
.apply { .apply {
putString(key, value) putString(key, value)
} }.apply()
.apply()
} }
fun setStringArray(key: String, value: Array<String>) { fun setStringArray(
key: String,
value: Array<String>,
) {
prefs prefs
.edit() .edit()
.apply { .apply {
putStringSet(key, value.toSet()) putStringSet(key, value.toSet())
} }.apply()
.apply()
} }
fun addToStringArray(key: String, string: String) { fun addToStringArray(
key: String,
string: String,
) {
prefs prefs
.edit() .edit()
.apply { .apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.add(string) set.add(string)
putStringSet(key, set) putStringSet(key, set)
} }.apply()
.apply()
} }
fun removeFromStringArray(key: String, string: String) { fun removeFromStringArray(
key: String,
string: String,
) {
prefs prefs
.edit() .edit()
.apply { .apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.remove(string) set.remove(string)
putStringSet(key, set) putStringSet(key, set)
} }.apply()
.apply()
} }
fun addManyToStringArray(key: String, strings: Array<String>) { fun addManyToStringArray(
key: String,
strings: Array<String>,
) {
prefs prefs
.edit() .edit()
.apply { .apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.addAll(strings.toSet()) set.addAll(strings.toSet())
putStringSet(key, set) putStringSet(key, set)
} }.apply()
.apply()
} }
fun removeManyFromStringArray(key: String, strings: Array<String>) { fun removeManyFromStringArray(
key: String,
strings: Array<String>,
) {
prefs prefs
.edit() .edit()
.apply { .apply {
val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf() val set = prefs.getStringSet(key, null)?.toMutableSet() ?: mutableSetOf()
set.removeAll(strings.toSet()) set.removeAll(strings.toSet())
putStringSet(key, set) putStringSet(key, set)
} }.apply()
.apply()
} }
} }

View File

@ -2,16 +2,16 @@ import ExpoModulesCore
let APP_GROUP = "group.app.bsky" let APP_GROUP = "group.app.bsky"
let DEFAULTS: [String:Any] = [ let DEFAULTS: [String: Any] = [
"playSoundChat" : true, "playSoundChat": true,
"playSoundFollow": false, "playSoundFollow": false,
"playSoundLike": false, "playSoundLike": false,
"playSoundMention": false, "playSoundMention": false,
"playSoundQuote": false, "playSoundQuote": false,
"playSoundReply": false, "playSoundReply": false,
"playSoundRepost": false, "playSoundRepost": false,
"mutedThreads": [:] as! [String:[String]], "mutedThreads": [:] as! [String: [String]],
"badgeCount": 0, "badgeCount": 0
] ]
/* /*
@ -23,10 +23,10 @@ let DEFAULTS: [String:Any] = [
*/ */
public class ExpoBackgroundNotificationHandlerModule: Module { public class ExpoBackgroundNotificationHandlerModule: Module {
let userDefaults = UserDefaults(suiteName: APP_GROUP) let userDefaults = UserDefaults(suiteName: APP_GROUP)
public func definition() -> ModuleDefinition { public func definition() -> ModuleDefinition {
Name("ExpoBackgroundNotificationHandler") Name("ExpoBackgroundNotificationHandler")
OnCreate { OnCreate {
DEFAULTS.forEach { p in DEFAULTS.forEach { p in
if userDefaults?.value(forKey: p.key) == nil { 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] = [] var keys: [String] = []
DEFAULTS.forEach { p in DEFAULTS.forEach { p in
keys.append(p.key) keys.append(p.key)
} }
return userDefaults?.dictionaryWithValues(forKeys: keys) return userDefaults?.dictionaryWithValues(forKeys: keys)
} }
AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in AsyncFunction("getBoolAsync") { (forKey: String) -> Bool in
if let pref = userDefaults?.bool(forKey: forKey) { if let pref = userDefaults?.bool(forKey: forKey) {
return pref return pref
} }
return false return false
} }
AsyncFunction("getStringAsync") { (forKey: String) -> String? in AsyncFunction("getStringAsync") { (forKey: String) -> String? in
if let pref = userDefaults?.string(forKey: forKey) { if let pref = userDefaults?.string(forKey: forKey) {
return pref return pref
} }
return nil return nil
} }
AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in AsyncFunction("getStringArrayAsync") { (forKey: String) -> [String]? in
if let pref = userDefaults?.stringArray(forKey: forKey) { if let pref = userDefaults?.stringArray(forKey: forKey) {
return pref return pref
} }
return nil return nil
} }
AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) -> Void in AsyncFunction("setBoolAsync") { (forKey: String, value: Bool) in
userDefaults?.setValue(value, forKey: forKey) 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) 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) userDefaults?.setValue(value, forKey: forKey)
} }
AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in AsyncFunction("addToStringArrayAsync") { (forKey: String, string: String) in
if var curr = userDefaults?.stringArray(forKey: forKey), if var curr = userDefaults?.stringArray(forKey: forKey),
!curr.contains(string) !curr.contains(string) {
{
curr.append(string) curr.append(string)
userDefaults?.setValue(curr, forKey: forKey) userDefaults?.setValue(curr, forKey: forKey)
} }
} }
AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in AsyncFunction("removeFromStringArrayAsync") { (forKey: String, string: String) in
if var curr = userDefaults?.stringArray(forKey: forKey) { if var curr = userDefaults?.stringArray(forKey: forKey) {
curr.removeAll { s in curr.removeAll { s in
@ -93,7 +92,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey) userDefaults?.setValue(curr, forKey: forKey)
} }
} }
AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in AsyncFunction("addManyToStringArrayAsync") { (forKey: String, strings: [String]) in
if var curr = userDefaults?.stringArray(forKey: forKey) { if var curr = userDefaults?.stringArray(forKey: forKey) {
strings.forEach { s in strings.forEach { s in
@ -104,7 +103,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey) userDefaults?.setValue(curr, forKey: forKey)
} }
} }
AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in AsyncFunction("removeManyFromStringArrayAsync") { (forKey: String, strings: [String]) in
if var curr = userDefaults?.stringArray(forKey: forKey) { if var curr = userDefaults?.stringArray(forKey: forKey) {
strings.forEach { s in strings.forEach { s in
@ -113,7 +112,7 @@ public class ExpoBackgroundNotificationHandlerModule: Module {
userDefaults?.setValue(curr, forKey: forKey) userDefaults?.setValue(curr, forKey: forKey)
} }
} }
AsyncFunction("setBadgeCountAsync") { (count: Int) in AsyncFunction("setBadgeCountAsync") { (count: Int) in
userDefaults?.setValue(count, forKey: "badgeCount") userDefaults?.setValue(count, forKey: "badgeCount")
} }

View File

@ -5,7 +5,10 @@ import android.graphics.Canvas
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import androidx.appcompat.widget.AppCompatImageView 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) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
@ -34,4 +37,4 @@ class AppCompatImageViewExtended(context: Context, private val parent: GifView):
drawable.start() drawable.start()
} }
} }
} }

View File

@ -6,49 +6,50 @@ import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.modules.ModuleDefinition
class ExpoBlueskyGifViewModule : Module() { class ExpoBlueskyGifViewModule : Module() {
override fun definition() = ModuleDefinition { override fun definition() =
Name("ExpoBlueskyGifView") ModuleDefinition {
Name("ExpoBlueskyGifView")
AsyncFunction("prefetchAsync") { sources: List<String> -> AsyncFunction("prefetchAsync") { sources: List<String> ->
val activity = appContext.currentActivity ?: return@AsyncFunction val activity = appContext.currentActivity ?: return@AsyncFunction
val glide = Glide.with(activity) val glide = Glide.with(activity)
sources.forEach { source -> sources.forEach { source ->
glide glide
.download(source) .download(source)
.diskCacheStrategy(DiskCacheStrategy.DATA) .diskCacheStrategy(DiskCacheStrategy.DATA)
.submit() .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()
}
}
}
} }

View File

@ -1,6 +1,5 @@
package expo.modules.blueskygifview package expo.modules.blueskygifview
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Animatable 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.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView 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 // Events
private val onPlayerStateChange by EventDispatcher() private val onPlayerStateChange by EventDispatcher()
@ -44,8 +46,7 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
} }
} }
// <editor-fold desc="Lifecycle">
//<editor-fold desc="Lifecycle">
init { init {
this.setBackgroundColor(Color.TRANSPARENT) this.setBackgroundColor(Color.TRANSPARENT)
@ -70,80 +71,82 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
super.onDetachedFromWindow() super.onDetachedFromWindow()
} }
//</editor-fold> // </editor-fold>
//<editor-fold desc="Loading"> // <editor-fold desc="Loading">
private fun load() { private fun load() {
if (placeholderSource == null || source == null) { if (placeholderSource == null || source == null) {
return return
} }
this.webpRequest = glide.load(source) this.webpRequest =
.diskCacheStrategy(DiskCacheStrategy.DATA) glide
.skipMemoryCache(false) .load(source)
.listener(object: RequestListener<Drawable> { .diskCacheStrategy(DiskCacheStrategy.DATA)
override fun onResourceReady( .skipMemoryCache(false)
resource: Drawable?, .listener(
model: Any?, object : RequestListener<Drawable> {
target: Target<Drawable>?, override fun onResourceReady(
dataSource: com.bumptech.glide.load.DataSource?, resource: Drawable?,
isFirstResource: Boolean model: Any?,
): Boolean { target: Target<Drawable>?,
if (placeholderRequest != null) { dataSource: com.bumptech.glide.load.DataSource?,
glide.clear(placeholderRequest) isFirstResource: Boolean,
} ): Boolean {
return false if (placeholderRequest != null) {
} glide.clear(placeholderRequest)
}
return false
}
override fun onLoadFailed( override fun onLoadFailed(
e: GlideException?, e: GlideException?,
model: Any?, model: Any?,
target: Target<Drawable>?, target: Target<Drawable>?,
isFirstResource: Boolean isFirstResource: Boolean,
): Boolean { ): Boolean = true
return true },
} ).into(this.imageView)
})
.into(this.imageView)
if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) { if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
this.placeholderRequest = glide.load(placeholderSource) this.placeholderRequest =
.diskCacheStrategy(DiskCacheStrategy.DATA) glide
// Let's not bloat the memory cache with placeholders .load(placeholderSource)
.skipMemoryCache(true) .diskCacheStrategy(DiskCacheStrategy.DATA)
.listener(object: RequestListener<Drawable> { // Let's not bloat the memory cache with placeholders
override fun onResourceReady( .skipMemoryCache(true)
resource: Drawable?, .listener(
model: Any?, object : RequestListener<Drawable> {
target: Target<Drawable>?, override fun onResourceReady(
dataSource: com.bumptech.glide.load.DataSource?, resource: Drawable?,
isFirstResource: Boolean model: Any?,
): Boolean { target: Target<Drawable>?,
// Incase this request finishes after the webp, let's just not set dataSource: com.bumptech.glide.load.DataSource?,
// the drawable. This shouldn't happen because the request should get cancelled isFirstResource: Boolean,
if (imageView.drawable == null) { ): Boolean {
imageView.setImageDrawable(resource) // Incase this request finishes after the webp, let's just not set
} // the drawable. This shouldn't happen because the request should get cancelled
return true if (imageView.drawable == null) {
} imageView.setImageDrawable(resource)
}
return true
}
override fun onLoadFailed( override fun onLoadFailed(
e: GlideException?, e: GlideException?,
model: Any?, model: Any?,
target: Target<Drawable>?, target: Target<Drawable>?,
isFirstResource: Boolean isFirstResource: Boolean,
): Boolean { ): Boolean = true
return true },
} ).submit()
})
.submit()
} }
} }
//</editor-fold> // </editor-fold>
//<editor-fold desc="Controls"> // <editor-fold desc="Controls">
fun play() { fun play() {
this.imageView.play() this.imageView.play()
@ -165,16 +168,18 @@ class GifView(context: Context, appContext: AppContext) : ExpoView(context, appC
} }
} }
//</editor-fold> // </editor-fold>
//<editor-fold desc="Util"> // <editor-fold desc="Util">
fun firePlayerStateChange() { fun firePlayerStateChange() {
onPlayerStateChange(mapOf( onPlayerStateChange(
"isPlaying" to this.isPlaying, mapOf(
"isLoaded" to this.isLoaded, "isPlaying" to this.isPlaying,
)) "isLoaded" to this.isLoaded,
),
)
} }
//</editor-fold> // </editor-fold>
} }

View File

@ -5,11 +5,11 @@ import SDWebImageWebPCoder
public class ExpoBlueskyGifViewModule: Module { public class ExpoBlueskyGifViewModule: Module {
public func definition() -> ModuleDefinition { public func definition() -> ModuleDefinition {
Name("ExpoBlueskyGifView") Name("ExpoBlueskyGifView")
OnCreate { OnCreate {
SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared) SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared)
} }
AsyncFunction("prefetchAsync") { (sources: [URL]) in AsyncFunction("prefetchAsync") { (sources: [URL]) in
SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil) SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil)
} }
@ -18,27 +18,27 @@ public class ExpoBlueskyGifViewModule: Module {
Events( Events(
"onPlayerStateChange" "onPlayerStateChange"
) )
Prop("source") { (view: GifView, prop: String) in Prop("source") { (view: GifView, prop: String) in
view.source = prop view.source = prop
} }
Prop("placeholderSource") { (view: GifView, prop: String) in Prop("placeholderSource") { (view: GifView, prop: String) in
view.placeholderSource = prop view.placeholderSource = prop
} }
Prop("autoplay") { (view: GifView, prop: Bool) in Prop("autoplay") { (view: GifView, prop: Bool) in
view.autoplay = prop view.autoplay = prop
} }
AsyncFunction("toggleAsync") { (view: GifView) in AsyncFunction("toggleAsync") { (view: GifView) in
view.toggle() view.toggle()
} }
AsyncFunction("playAsync") { (view: GifView) in AsyncFunction("playAsync") { (view: GifView) in
view.play() view.play()
} }
AsyncFunction("pauseAsync") { (view: GifView) in AsyncFunction("pauseAsync") { (view: GifView) in
view.pause() view.pause()
} }

View File

@ -16,14 +16,14 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
) )
private var isPlaying = true private var isPlaying = true
private var isLoaded = false private var isLoaded = false
// Requests // Requests
private var webpOperation: SDWebImageCombinedOperation? private var webpOperation: SDWebImageCombinedOperation?
private var placeholderOperation: SDWebImageCombinedOperation? private var placeholderOperation: SDWebImageCombinedOperation?
// Props // Props
var source: String? = nil var source: String?
var placeholderSource: String? = nil var placeholderSource: String?
var autoplay = true { var autoplay = true {
didSet { didSet {
if !autoplay { if !autoplay {
@ -78,8 +78,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
// See: // See:
// https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently // https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently
if !SDImageCache.shared.diskImageDataExists(withKey: source), if !SDImageCache.shared.diskImageDataExists(withKey: source),
let url = URL(string: placeholderSource) let url = URL(string: placeholderSource) {
{
self.placeholderOperation = imageManager.loadImage( self.placeholderOperation = imageManager.loadImage(
with: url, with: url,
options: [.retryFailed], options: [.retryFailed],
@ -132,8 +131,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
if let placeholderSource = self.placeholderSource, if let placeholderSource = self.placeholderSource,
imageUrl?.absoluteString == placeholderSource, imageUrl?.absoluteString == placeholderSource,
self.imageView.image == nil, self.imageView.image == nil,
let image = image let image = image {
{
self.setImage(image) self.setImage(image)
return return
} }
@ -142,8 +140,7 @@ public class GifView: ExpoView, AVPlayerViewControllerDelegate {
imageUrl?.absoluteString == source, imageUrl?.absoluteString == source,
// UIImage perf suckssss if the image is animated // UIImage perf suckssss if the image is animated
let data = data, let data = data,
let animatedImage = SDAnimatedImage(data: data) let animatedImage = SDAnimatedImage(data: data) {
{
self.placeholderOperation?.cancel() self.placeholderOperation?.cancel()
self.isPlaying = self.autoplay self.isPlaying = self.autoplay
self.isLoaded = true self.isLoaded = true

View File

@ -4,7 +4,8 @@ import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.modules.ModuleDefinition
class ExpoBlueskyDevicePrefsModule : Module() { class ExpoBlueskyDevicePrefsModule : Module() {
override fun definition() = ModuleDefinition { override fun definition() =
Name("ExpoBlueskyDevicePrefs") ModuleDefinition {
} Name("ExpoBlueskyDevicePrefs")
}
} }

View File

@ -3,52 +3,55 @@ package expo.modules.blueskyswissarmy.referrer
import android.util.Log import android.util.Log
import com.android.installreferrer.api.InstallReferrerClient import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerStateListener import com.android.installreferrer.api.InstallReferrerStateListener
import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.Promise
class ExpoBlueskyReferrerModule : Module() { class ExpoBlueskyReferrerModule : Module() {
override fun definition() = ModuleDefinition { override fun definition() =
Name("ExpoBlueskyReferrer") ModuleDefinition {
Name("ExpoBlueskyReferrer")
AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise -> AsyncFunction("getGooglePlayReferrerInfoAsync") { promise: Promise ->
val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build() val referrerClient = InstallReferrerClient.newBuilder(appContext.reactContext).build()
referrerClient.startConnection(object : InstallReferrerStateListener { referrerClient.startConnection(
override fun onInstallReferrerSetupFinished(responseCode: Int) { object : InstallReferrerStateListener {
if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) { override fun onInstallReferrerSetupFinished(responseCode: Int) {
Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.") if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
Log.d("ExpoGooglePlayReferrer", "Successfully retrieved referrer info.")
val response = referrerClient.installReferrer val response = referrerClient.installReferrer
Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}") Log.d("ExpoGooglePlayReferrer", "Install referrer: ${response.installReferrer}")
promise.resolve( promise.resolve(
mapOf( mapOf(
"installReferrer" to response.installReferrer, "installReferrer" to response.installReferrer,
"clickTimestamp" to response.referrerClickTimestampSeconds, "clickTimestamp" to response.referrerClickTimestampSeconds,
"installTimestamp" to response.installBeginTimestampSeconds "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")
)
}
})
} }
} }
}

View File

@ -13,16 +13,17 @@ import java.io.FileOutputStream
import java.net.URLEncoder import java.net.URLEncoder
class ExpoReceiveAndroidIntentsModule : Module() { class ExpoReceiveAndroidIntentsModule : Module() {
override fun definition() = ModuleDefinition { override fun definition() =
Name("ExpoReceiveAndroidIntents") ModuleDefinition {
Name("ExpoReceiveAndroidIntents")
OnNewIntent { OnNewIntent {
handleIntent(it) handleIntent(it)
}
} }
}
private fun handleIntent(intent: Intent?) { 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.action == Intent.ACTION_SEND) {
if (intent.type == "text/plain") { if (intent.type == "text/plain") {
@ -40,7 +41,7 @@ class ExpoReceiveAndroidIntentsModule : Module() {
private fun handleTextIntent(intent: Intent) { private fun handleTextIntent(intent: Intent) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let { intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
val encoded = URLEncoder.encode(it, "UTF-8") 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) val newIntent = Intent(Intent.ACTION_VIEW, uri)
appContext.currentActivity?.startActivity(newIntent) appContext.currentActivity?.startActivity(newIntent)
} }
@ -48,11 +49,12 @@ class ExpoReceiveAndroidIntentsModule : Module() {
} }
private fun handleImageIntent(intent: Intent) { private fun handleImageIntent(intent: Intent) {
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val uri =
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
} else { intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
intent.getParcelableExtra(Intent.EXTRA_STREAM) } else {
} intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
if (uri == null) return if (uri == null) return
handleImageIntents(listOf(uri)) handleImageIntents(listOf(uri))
@ -76,16 +78,16 @@ class ExpoReceiveAndroidIntentsModule : Module() {
uris.forEachIndexed { index, uri -> uris.forEachIndexed { index, uri ->
val info = getImageInfo(uri) val info = getImageInfo(uri)
val params = buildUriData(info) val params = buildUriData(info)
allParams = "${allParams}${params}" allParams = "${allParams}$params"
if (index < uris.count() - 1) { if (index < uris.count() - 1) {
allParams = "${allParams}," allParams = "$allParams,"
} }
} }
val encoded = URLEncoder.encode(allParams, "UTF-8") 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) val newIntent = Intent(Intent.ACTION_VIEW, it)
appContext.currentActivity?.startActivity(newIntent) appContext.currentActivity?.startActivity(newIntent)
} }
@ -104,7 +106,7 @@ class ExpoReceiveAndroidIntentsModule : Module() {
return mapOf( return mapOf(
"width" to bitmap.width, "width" to bitmap.width,
"height" to bitmap.height, "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 path = info.getValue("path")
val width = info.getValue("width") val width = info.getValue("width")
val height = info.getValue("height") val height = info.getValue("height")
return "file://${path}|${width}|${height}" return "file://$path|$width|$height"
} }
} }

View File

@ -3,7 +3,7 @@ import ExpoModulesCore
public class ExpoScrollForwarderModule: Module { public class ExpoScrollForwarderModule: Module {
public func definition() -> ModuleDefinition { public func definition() -> ModuleDefinition {
Name("ExpoScrollForwarder") Name("ExpoScrollForwarder")
View(ExpoScrollForwarderView.self) { View(ExpoScrollForwarderView.self) {
Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in Prop("scrollViewTag") { (view: ExpoScrollForwarderView, prop: Int) in
view.scrollViewTag = prop view.scrollViewTag = prop

View File

@ -8,17 +8,17 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
self.tryFindScrollView() self.tryFindScrollView()
} }
} }
private var rctScrollView: RCTScrollView? private var rctScrollView: RCTScrollView?
private var rctRefreshCtrl: RCTRefreshControl? private var rctRefreshCtrl: RCTRefreshControl?
private var cancelGestureRecognizers: [UIGestureRecognizer]? private var cancelGestureRecognizers: [UIGestureRecognizer]?
private var animTimer: Timer? private var animTimer: Timer?
private var initialOffset: CGFloat = 0.0 private var initialOffset: CGFloat = 0.0
private var didImpact: Bool = false private var didImpact: Bool = false
required init(appContext: AppContext? = nil) { required init(appContext: AppContext? = nil) {
super.init(appContext: appContext) super.init(appContext: appContext)
let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:))) let pg = UIPanGestureRecognizer(target: self, action: #selector(callOnPan(_:)))
pg.delegate = self pg.delegate = self
self.addGestureRecognizer(pg) self.addGestureRecognizer(pg)
@ -34,28 +34,27 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
self.cancelGestureRecognizers = [lpg, tg] self.cancelGestureRecognizers = [lpg, tg]
} }
// We don't want to recognize the scroll pan gesture and the swipe back gesture together // We don't want to recognize the scroll pan gesture and the swipe back gesture together
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer { if gestureRecognizer is UIPanGestureRecognizer, otherGestureRecognizer is UIPanGestureRecognizer {
return false return false
} }
return true return true
} }
// We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will // We only want the "scroll" gesture to happen whenever the pan is vertical, otherwise it will
// interfere with the native swipe back gesture. // interfere with the native swipe back gesture.
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { guard let gestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer else {
return true return true
} }
let velocity = gestureRecognizer.velocity(in: self) let velocity = gestureRecognizer.velocity(in: self)
return abs(velocity.y) > abs(velocity.x) 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 // 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. // recognizer for this one.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { override func touchesBegan(_ touches: Set<UITouch>, 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 // 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. // 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() self.stopTimer()
} }
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) -> Void { @IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else { guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else {
return return
} }
let translation = sender.translation(in: self).y let translation = sender.translation(in: self).y
if sender.state == .began { if sender.state == .began {
if sv.contentOffset.y < 0 { if sv.contentOffset.y < 0 {
sv.contentOffset.y = 0 sv.contentOffset.y = 0
} }
self.initialOffset = sv.contentOffset.y self.initialOffset = sv.contentOffset.y
} }
if sender.state == .changed { if sender.state == .changed {
sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset) sv.contentOffset.y = self.dampenOffset(-translation + self.initialOffset)
if sv.contentOffset.y <= -130, !didImpact { if sv.contentOffset.y <= -130, !didImpact {
let generator = UIImpactFeedbackGenerator(style: .light) let generator = UIImpactFeedbackGenerator(style: .light)
generator.impactOccurred() generator.impactOccurred()
self.didImpact = true self.didImpact = true
} }
} }
@ -97,7 +96,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
if sender.state == .ended { if sender.state == .ended {
let velocity = sender.velocity(in: self).y let velocity = sender.velocity(in: self).y
self.didImpact = false self.didImpact = false
if sv.contentOffset.y <= -130 { if sv.contentOffset.y <= -130 {
self.rctRefreshCtrl?.forwarderBeginRefreshing() self.rctRefreshCtrl?.forwarderBeginRefreshing()
return return
@ -108,40 +107,40 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
if abs(velocity) < 250, sv.contentOffset.y >= 0 { if abs(velocity) < 250, sv.contentOffset.y >= 0 {
return return
} }
self.startDecayAnimation(translation, velocity) self.startDecayAnimation(translation, velocity)
} }
} }
func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) { func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
guard let sv = self.rctScrollView?.scrollView else { guard let sv = self.rctScrollView?.scrollView else {
return return
} }
var velocity = velocity var velocity = velocity
self.enableCancelGestureRecognizers() self.enableCancelGestureRecognizers()
if velocity > 0 { if velocity > 0 {
velocity = min(velocity, 5000) velocity = min(velocity, 5000)
} else { } else {
velocity = max(velocity, -5000) velocity = max(velocity, -5000)
} }
var animTranslation = -translation 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 velocity *= 0.9875
animTranslation = (-velocity / 120) + animTranslation animTranslation = (-velocity / 120) + animTranslation
let nextOffset = self.dampenOffset(animTranslation + self.initialOffset) let nextOffset = self.dampenOffset(animTranslation + self.initialOffset)
if nextOffset <= 0 { if nextOffset <= 0 {
if self.initialOffset <= 1 { if self.initialOffset <= 1 {
self.scrollToOffset(0) self.scrollToOffset(0)
} else { } else {
sv.contentOffset.y = 0 sv.contentOffset.y = 0
} }
self.stopTimer() self.stopTimer()
return return
} else { } else {
@ -153,61 +152,60 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
} }
} }
} }
func dampenOffset(_ offset: CGFloat) -> CGFloat { func dampenOffset(_ offset: CGFloat) -> CGFloat {
if offset < 0 { if offset < 0 {
return offset - (offset * 0.55) return offset - (offset * 0.55)
} }
return offset return offset
} }
func tryFindScrollView() { func tryFindScrollView() {
guard let scrollViewTag = scrollViewTag else { guard let scrollViewTag = scrollViewTag else {
return return
} }
// Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer. // 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. // Otherwise we might end up with duplicates when we switch back to that scrollview.
self.removeCancelGestureRecognizers() self.removeCancelGestureRecognizers()
self.rctScrollView = self.appContext? self.rctScrollView = self.appContext?
.findView(withTag: scrollViewTag, ofType: RCTScrollView.self) .findView(withTag: scrollViewTag, ofType: RCTScrollView.self)
self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl
self.addCancelGestureRecognizers() self.addCancelGestureRecognizers()
} }
func addCancelGestureRecognizers() { func addCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in self.cancelGestureRecognizers?.forEach { r in
self.rctScrollView?.scrollView?.addGestureRecognizer(r) self.rctScrollView?.scrollView?.addGestureRecognizer(r)
} }
} }
func removeCancelGestureRecognizers() { func removeCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in self.cancelGestureRecognizers?.forEach { r in
self.rctScrollView?.scrollView?.removeGestureRecognizer(r) self.rctScrollView?.scrollView?.removeGestureRecognizer(r)
} }
} }
func enableCancelGestureRecognizers() { func enableCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in self.cancelGestureRecognizers?.forEach { r in
r.isEnabled = true r.isEnabled = true
} }
} }
func disableCancelGestureRecognizers() { func disableCancelGestureRecognizers() {
self.cancelGestureRecognizers?.forEach { r in self.cancelGestureRecognizers?.forEach { r in
r.isEnabled = false 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) self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
} }
func stopTimer() -> Void { func stopTimer() {
self.disableCancelGestureRecognizers() self.disableCancelGestureRecognizers()
self.animTimer?.invalidate() self.animTimer?.invalidate()
self.animTimer = nil self.animTimer = nil

View File

@ -29,6 +29,8 @@
"test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit", "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "NODE_ENV=test jest --coverage", "test-coverage": "NODE_ENV=test jest --coverage",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src", "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", "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: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", "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",