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