about summary refs log tree commit diff
path: root/modules
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-08-07 14:45:06 -0700
committerGitHub <noreply@github.com>2024-08-07 14:45:06 -0700
commit1b02f81cb85333462e3a9a42accc05d09aca4f2c (patch)
tree766e80438c1f109a1a7d751e9f04b7f6242f9766 /modules
parentfff2c079c2554861764974aaeeb56f79a25ba82a (diff)
downloadvoidsky-1b02f81cb85333462e3a9a42accc05d09aca4f2c.tar.zst
[Video] Visibility detection view (#4741)
Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com>
Diffstat (limited to 'modules')
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/ExpoBlueskyVisibilityViewModule.kt23
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityView.kt63
-rw-r--r--modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt82
-rw-r--r--modules/expo-bluesky-swiss-army/expo-module.config.json8
-rw-r--r--modules/expo-bluesky-swiss-army/index.ts3
-rw-r--r--modules/expo-bluesky-swiss-army/ios/Visibility/ExpoBlueskyVisibilityViewModule.swift21
-rw-r--r--modules/expo-bluesky-swiss-army/ios/Visibility/VisibilityViewManager.swift86
-rw-r--r--modules/expo-bluesky-swiss-army/ios/Visibility/VisiblityView.swift69
-rw-r--r--modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx39
-rw-r--r--modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx10
-rw-r--r--modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts6
11 files changed, 408 insertions, 2 deletions
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/ExpoBlueskyVisibilityViewModule.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/ExpoBlueskyVisibilityViewModule.kt
new file mode 100644
index 000000000..ddbb05cde
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/ExpoBlueskyVisibilityViewModule.kt
@@ -0,0 +1,23 @@
+package expo.modules.blueskyswissarmy.visibilityview
+
+import expo.modules.kotlin.modules.Module
+import expo.modules.kotlin.modules.ModuleDefinition
+
+class ExpoBlueskyVisibilityViewModule : Module() {
+  override fun definition() =
+    ModuleDefinition {
+      Name("ExpoBlueskyVisibilityView")
+
+      AsyncFunction("updateActiveViewAsync") {
+        VisibilityViewManager.updateActiveView()
+      }
+
+      View(VisibilityView::class) {
+        Events(arrayOf("onChangeStatus"))
+
+        Prop("enabled") { view: VisibilityView, prop: Boolean ->
+          view.isViewEnabled = prop
+        }
+      }
+    }
+}
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityView.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityView.kt
new file mode 100644
index 000000000..a55ab80d5
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityView.kt
@@ -0,0 +1,63 @@
+package expo.modules.blueskyswissarmy.visibilityview
+
+import android.content.Context
+import android.graphics.Rect
+import expo.modules.kotlin.AppContext
+import expo.modules.kotlin.viewevent.EventDispatcher
+import expo.modules.kotlin.views.ExpoView
+
+class VisibilityView(
+  context: Context,
+  appContext: AppContext,
+) : ExpoView(context, appContext) {
+  var isViewEnabled: Boolean = false
+
+  private val onChangeStatus by EventDispatcher()
+
+  private var isCurrentlyActive = false
+
+  override fun onAttachedToWindow() {
+    super.onAttachedToWindow()
+    VisibilityViewManager.addView(this)
+  }
+
+  override fun onDetachedFromWindow() {
+    super.onDetachedFromWindow()
+    VisibilityViewManager.removeView(this)
+  }
+
+  fun setIsCurrentlyActive(isActive: Boolean) {
+    if (isCurrentlyActive == isActive) {
+      return
+    }
+
+    this.isCurrentlyActive = isActive
+    this.onChangeStatus(
+      mapOf(
+        "isActive" to isActive,
+      ),
+    )
+  }
+
+  fun getPositionOnScreen(): Rect? {
+    if (!this.isShown) {
+      return null
+    }
+
+    val screenPosition = intArrayOf(0, 0)
+    this.getLocationInWindow(screenPosition)
+    return Rect(
+      screenPosition[0],
+      screenPosition[1],
+      screenPosition[0] + this.width,
+      screenPosition[1] + this.height,
+    )
+  }
+
+  fun isViewableEnough(): Boolean {
+    val positionOnScreen = this.getPositionOnScreen() ?: return false
+    val visibleArea = positionOnScreen.width() * positionOnScreen.height()
+    val totalArea = this.width * this.height
+    return visibleArea >= 0.5 * totalArea
+  }
+}
diff --git a/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt
new file mode 100644
index 000000000..ec1e49816
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/android/src/main/java/expo/modules/blueskyswissarmy/visibilityview/VisibilityViewManager.kt
@@ -0,0 +1,82 @@
+package expo.modules.blueskyswissarmy.visibilityview
+
+import android.graphics.Rect
+
+class VisibilityViewManager {
+  companion object {
+    private val views = HashMap<Int, VisibilityView>()
+    private var currentlyActiveView: VisibilityView? = null
+    private var prevCount = 0
+
+    fun addView(view: VisibilityView) {
+      this.views[view.id] = view
+
+      if (this.prevCount == 0) {
+        this.updateActiveView()
+      }
+      this.prevCount = this.views.count()
+    }
+
+    fun removeView(view: VisibilityView) {
+      this.views.remove(view.id)
+      this.prevCount = this.views.count()
+    }
+
+    fun updateActiveView() {
+      var activeView: VisibilityView? = null
+      val count = this.views.count()
+
+      if (count == 1) {
+        val view = this.views.values.first()
+        if (view.isViewableEnough()) {
+          activeView = view
+        }
+      } else if (count > 1) {
+        val views = this.views.values
+        var mostVisibleView: VisibilityView? = null
+        var mostVisiblePosition: Rect? = null
+
+        views.forEach { view ->
+          if (!view.isViewableEnough()) {
+            return
+          }
+
+          val position = view.getPositionOnScreen() ?: return@forEach
+          val topY = position.centerY() - (position.height() / 2)
+
+          if (topY >= 150) {
+            if (mostVisiblePosition == null) {
+              mostVisiblePosition = position
+            }
+
+            if (position.centerY() <= mostVisiblePosition!!.centerY()) {
+              mostVisibleView = view
+              mostVisiblePosition = position
+            }
+          }
+        }
+
+        activeView = mostVisibleView
+      }
+
+      if (activeView == this.currentlyActiveView) {
+        return
+      }
+
+      this.clearActiveView()
+      if (activeView != null) {
+        this.setActiveView(activeView)
+      }
+    }
+
+    private fun clearActiveView() {
+      this.currentlyActiveView?.setIsCurrentlyActive(false)
+      this.currentlyActiveView = null
+    }
+
+    private fun setActiveView(view: VisibilityView) {
+      view.setIsCurrentlyActive(true)
+      this.currentlyActiveView = view
+    }
+  }
+}
diff --git a/modules/expo-bluesky-swiss-army/expo-module.config.json b/modules/expo-bluesky-swiss-army/expo-module.config.json
index adb535e7f..4cdc11e99 100644
--- a/modules/expo-bluesky-swiss-army/expo-module.config.json
+++ b/modules/expo-bluesky-swiss-army/expo-module.config.json
@@ -1,12 +1,18 @@
 {
   "platforms": ["ios", "tvos", "android", "web"],
   "ios": {
-    "modules": ["ExpoBlueskySharedPrefsModule", "ExpoBlueskyReferrerModule", "ExpoPlatformInfoModule"]
+    "modules": [
+      "ExpoBlueskySharedPrefsModule",
+      "ExpoBlueskyReferrerModule",
+      "ExpoBlueskyVisibilityViewModule",
+      "ExpoPlatformInfoModule"
+    ]
   },
   "android": {
     "modules": [
       "expo.modules.blueskyswissarmy.sharedprefs.ExpoBlueskySharedPrefsModule",
       "expo.modules.blueskyswissarmy.referrer.ExpoBlueskyReferrerModule",
+      "expo.modules.blueskyswissarmy.visibilityview.ExpoBlueskyVisibilityViewModule",
       "expo.modules.blueskyswissarmy.platforminfo.ExpoPlatformInfoModule"
     ]
   }
diff --git a/modules/expo-bluesky-swiss-army/index.ts b/modules/expo-bluesky-swiss-army/index.ts
index f62596cb7..ebd67913e 100644
--- a/modules/expo-bluesky-swiss-army/index.ts
+++ b/modules/expo-bluesky-swiss-army/index.ts
@@ -1,5 +1,6 @@
 import * as PlatformInfo from './src/PlatformInfo'
 import * as Referrer from './src/Referrer'
 import * as SharedPrefs from './src/SharedPrefs'
+import VisibilityView from './src/VisibilityView'
 
-export {PlatformInfo, Referrer, SharedPrefs}
+export {PlatformInfo, Referrer, SharedPrefs, VisibilityView}
diff --git a/modules/expo-bluesky-swiss-army/ios/Visibility/ExpoBlueskyVisibilityViewModule.swift b/modules/expo-bluesky-swiss-army/ios/Visibility/ExpoBlueskyVisibilityViewModule.swift
new file mode 100644
index 000000000..ec12a84af
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/ios/Visibility/ExpoBlueskyVisibilityViewModule.swift
@@ -0,0 +1,21 @@
+import ExpoModulesCore
+
+public class ExpoBlueskyVisibilityViewModule: Module {
+  public func definition() -> ModuleDefinition {
+    Name("ExpoBlueskyVisibilityView")
+
+    AsyncFunction("updateActiveViewAsync") {
+      VisibilityViewManager.shared.updateActiveView()
+    }
+
+    View(VisibilityView.self) {
+      Events([
+        "onChangeStatus"
+      ])
+
+      Prop("enabled") { (view: VisibilityView, prop: Bool) in
+        view.enabled = prop
+      }
+    }
+  }
+}
diff --git a/modules/expo-bluesky-swiss-army/ios/Visibility/VisibilityViewManager.swift b/modules/expo-bluesky-swiss-army/ios/Visibility/VisibilityViewManager.swift
new file mode 100644
index 000000000..ae8e16868
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/ios/Visibility/VisibilityViewManager.swift
@@ -0,0 +1,86 @@
+import Foundation
+
+class VisibilityViewManager {
+  static let shared = VisibilityViewManager()
+
+  private let views = NSHashTable<VisibilityView>(options: .weakMemory)
+  private var currentlyActiveView: VisibilityView?
+  private var screenHeight: CGFloat = UIScreen.main.bounds.height
+  private var prevCount = 0
+
+  func addView(_ view: VisibilityView) {
+    self.views.add(view)
+
+    if self.prevCount == 0 {
+      self.updateActiveView()
+    }
+    self.prevCount = self.views.count
+  }
+
+  func removeView(_ view: VisibilityView) {
+    self.views.remove(view)
+    self.prevCount = self.views.count
+  }
+
+  func updateActiveView() {
+    DispatchQueue.main.async {
+      var activeView: VisibilityView?
+
+      if self.views.count == 1 {
+        let view = self.views.allObjects[0]
+        if view.isViewableEnough() {
+          activeView = view
+        }
+      } else if self.views.count > 1 {
+        let views = self.views.allObjects
+        var mostVisibleView: VisibilityView?
+        var mostVisiblePosition: CGRect?
+
+        views.forEach { view in
+          if !view.isViewableEnough() {
+            return
+          }
+
+          guard let position = view.getPositionOnScreen() else {
+            return
+          }
+
+          if position.minY >= 150 {
+            if mostVisiblePosition == nil {
+              mostVisiblePosition = position
+            }
+
+            if let unwrapped = mostVisiblePosition,
+               position.minY <= unwrapped.minY {
+              mostVisibleView = view
+              mostVisiblePosition = position
+            }
+          }
+        }
+
+        activeView = mostVisibleView
+      }
+
+      if activeView == self.currentlyActiveView {
+        return
+      }
+
+      self.clearActiveView()
+      if let view = activeView {
+        self.setActiveView(view)
+      }
+    }
+  }
+
+  private func clearActiveView() {
+    if let currentlyActiveView = self.currentlyActiveView {
+      currentlyActiveView.setIsCurrentlyActive(isActive: false)
+      self.currentlyActiveView = nil
+    }
+  }
+
+  private func setActiveView(_ view: VisibilityView) {
+    view.setIsCurrentlyActive(isActive: true)
+    self.currentlyActiveView = view
+  }
+}
diff --git a/modules/expo-bluesky-swiss-army/ios/Visibility/VisiblityView.swift b/modules/expo-bluesky-swiss-army/ios/Visibility/VisiblityView.swift
new file mode 100644
index 000000000..fd99ee493
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/ios/Visibility/VisiblityView.swift
@@ -0,0 +1,69 @@
+import ExpoModulesCore
+
+class VisibilityView: ExpoView {
+  var enabled = false {
+    didSet {
+      if enabled {
+        VisibilityViewManager.shared.removeView(self)
+      }
+    }
+  }
+
+  private let onChangeStatus = EventDispatcher()
+  private var isCurrentlyActiveView = false
+
+  required init(appContext: AppContext? = nil) {
+    super.init(appContext: appContext)
+  }
+
+  public override func willMove(toWindow newWindow: UIWindow?) {
+    super.willMove(toWindow: newWindow)
+
+    if !self.enabled {
+      return
+    }
+
+    if newWindow == nil {
+      VisibilityViewManager.shared.removeView(self)
+    } else {
+      VisibilityViewManager.shared.addView(self)
+    }
+  }
+
+  func setIsCurrentlyActive(isActive: Bool) {
+    if isCurrentlyActiveView == isActive {
+      return
+    }
+    self.isCurrentlyActiveView = isActive
+    self.onChangeStatus([
+      "isActive": isActive
+    ])
+  }
+}
+
+// 🚨 DANGER 🚨
+// These functions need to be called from the main thread. Xcode will warn you if you call one of them
+// off the main thread, so pay attention!
+extension UIView {
+  func getPositionOnScreen() -> CGRect? {
+    if let window = self.window {
+      return self.convert(self.bounds, to: window)
+    }
+    return nil
+  }
+
+  func isViewableEnough() -> Bool {
+    guard let window = self.window else {
+      return false
+    }
+
+    let viewFrameOnScreen = self.convert(self.bounds, to: window)
+    let screenBounds = window.bounds
+    let intersection = viewFrameOnScreen.intersection(screenBounds)
+
+    let viewHeight = viewFrameOnScreen.height
+    let intersectionHeight = intersection.height
+
+    return intersectionHeight >= 0.5 * viewHeight
+  }
+}
diff --git a/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx
new file mode 100644
index 000000000..9d0e8cf22
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.native.tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import {StyleProp, ViewStyle} from 'react-native'
+import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
+
+import {VisibilityViewProps} from './types'
+const NativeView: React.ComponentType<{
+  onChangeStatus: (e: {nativeEvent: {isActive: boolean}}) => void
+  children: React.ReactNode
+  enabled: Boolean
+  style: StyleProp<ViewStyle>
+}> = requireNativeViewManager('ExpoBlueskyVisibilityView')
+
+const NativeModule = requireNativeModule('ExpoBlueskyVisibilityView')
+
+export async function updateActiveViewAsync() {
+  await NativeModule.updateActiveViewAsync()
+}
+
+export default function VisibilityView({
+  children,
+  onChangeStatus: onChangeStatusOuter,
+  enabled,
+}: VisibilityViewProps) {
+  const onChangeStatus = React.useCallback(
+    (e: {nativeEvent: {isActive: boolean}}) => {
+      onChangeStatusOuter(e.nativeEvent.isActive)
+    },
+    [onChangeStatusOuter],
+  )
+
+  return (
+    <NativeView
+      onChangeStatus={onChangeStatus}
+      enabled={enabled}
+      style={{flex: 1}}>
+      {children}
+    </NativeView>
+  )
+}
diff --git a/modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx
new file mode 100644
index 000000000..8b4f1928c
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/src/VisibilityView/index.tsx
@@ -0,0 +1,10 @@
+import {NotImplementedError} from '../NotImplemented'
+import {VisibilityViewProps} from './types'
+
+export async function updateActiveViewAsync() {
+  throw new NotImplementedError()
+}
+
+export default function VisibilityView({children}: VisibilityViewProps) {
+  return children
+}
diff --git a/modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts b/modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts
new file mode 100644
index 000000000..312acf2d2
--- /dev/null
+++ b/modules/expo-bluesky-swiss-army/src/VisibilityView/types.ts
@@ -0,0 +1,6 @@
+import React from 'react'
+export interface VisibilityViewProps {
+  children: React.ReactNode
+  onChangeStatus: (isActive: boolean) => void
+  enabled: boolean
+}