[Sheets] [Pt. 11] Move native code into repo (#5583)
Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
group = 'expo.modules.bottomsheet'
|
||||||
|
version = '0.1.0'
|
||||||
|
|
||||||
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||||
|
apply from: expoModulesCorePlugin
|
||||||
|
applyKotlinExpoModulesCorePlugin()
|
||||||
|
useCoreDependencies()
|
||||||
|
useExpoPublishing()
|
||||||
|
|
||||||
|
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
||||||
|
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
||||||
|
// Most of the time, you may like to manage the Android SDK versions yourself.
|
||||||
|
def useManagedAndroidSdkVersions = false
|
||||||
|
if (useManagedAndroidSdkVersions) {
|
||||||
|
useDefaultAndroidSdkVersions()
|
||||||
|
} else {
|
||||||
|
buildscript {
|
||||||
|
// Simple helper that allows the root project to override versions declared by this library.
|
||||||
|
ext.safeExtGet = { prop, fallback ->
|
||||||
|
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project.android {
|
||||||
|
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||||
|
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "expo.modules.bottomsheet"
|
||||||
|
defaultConfig {
|
||||||
|
versionCode 1
|
||||||
|
versionName "0.1.0"
|
||||||
|
}
|
||||||
|
lintOptions {
|
||||||
|
abortOnError false
|
||||||
|
}
|
||||||
|
buildFeatures {
|
||||||
|
compose true
|
||||||
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "1.5.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':expo-modules-core')
|
||||||
|
implementation "androidx.compose.material3:material3:1.3.0"
|
||||||
|
implementation "androidx.compose.material:material:1.7.2"
|
||||||
|
implementation "com.facebook.react:react-native:+"
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<manifest>
|
||||||
|
</manifest>
|
||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
package expo.modules.bottomsheet
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.core.view.allViews
|
||||||
|
import com.facebook.react.ReactRootView
|
||||||
|
import expo.modules.kotlin.AppContext
|
||||||
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||||
|
import expo.modules.kotlin.views.ExpoView
|
||||||
|
|
||||||
|
class BottomSheetView(
|
||||||
|
context: Context,
|
||||||
|
appContext: AppContext,
|
||||||
|
) : ExpoView(context, appContext) {
|
||||||
|
val sheetState = mutableStateOf(SheetState())
|
||||||
|
|
||||||
|
private var reactRootView: ReactRootView? = null
|
||||||
|
private var innerView: View? = null
|
||||||
|
|
||||||
|
private var sheetView: ComposeView? = null
|
||||||
|
|
||||||
|
private val onStateChange by EventDispatcher()
|
||||||
|
private val onAttemptDismiss by EventDispatcher()
|
||||||
|
|
||||||
|
// Props
|
||||||
|
var preventDismiss = false
|
||||||
|
var minHeight = 0f
|
||||||
|
var maxHeight = 0f
|
||||||
|
|
||||||
|
private var isOpen: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (field == value) return
|
||||||
|
|
||||||
|
field = value
|
||||||
|
this.sheetState.value.isOpen = value
|
||||||
|
onStateChange(
|
||||||
|
mapOf(
|
||||||
|
"state" to if (value) "open" else "closed",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isOpening: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (value) {
|
||||||
|
onStateChange(mapOf("state" to "opening"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isClosing: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (value) {
|
||||||
|
onStateChange(mapOf("state" to "closing"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasInitiallyOpened = false
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
override fun addView(
|
||||||
|
child: View?,
|
||||||
|
index: Int,
|
||||||
|
) {
|
||||||
|
this.innerView = child
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLayout(
|
||||||
|
changed: Boolean,
|
||||||
|
l: Int,
|
||||||
|
t: Int,
|
||||||
|
r: Int,
|
||||||
|
b: Int,
|
||||||
|
) {
|
||||||
|
this.innerView?.let {
|
||||||
|
val height =
|
||||||
|
it.allViews
|
||||||
|
.last()
|
||||||
|
.measuredHeight
|
||||||
|
.toFloat()
|
||||||
|
this.present(height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroy() {
|
||||||
|
this.isClosing = false
|
||||||
|
this.isOpen = false
|
||||||
|
|
||||||
|
this.getRootLayout().removeView(this.sheetView)
|
||||||
|
this.sheetView = null
|
||||||
|
|
||||||
|
this.reactRootView = null
|
||||||
|
this.innerView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presentation
|
||||||
|
|
||||||
|
private fun present(contentHeight: Float) {
|
||||||
|
val innerView = this.innerView ?: return
|
||||||
|
|
||||||
|
// For GestureRootView to work, we need to create a ReactRootView for the innerView to be
|
||||||
|
// contained inside of
|
||||||
|
val reactRootView = ReactRootView(context)
|
||||||
|
reactRootView.addView(innerView)
|
||||||
|
this.reactRootView = reactRootView
|
||||||
|
|
||||||
|
this.isOpening = true
|
||||||
|
this.sheetView =
|
||||||
|
ComposeView(context).also {
|
||||||
|
it.setContent {
|
||||||
|
SheetView(
|
||||||
|
state = sheetState,
|
||||||
|
innerView = reactRootView,
|
||||||
|
contentHeight = innerView.height.toFloat(),
|
||||||
|
onDismissRequest = {
|
||||||
|
onAttemptDismiss(mapOf())
|
||||||
|
if (!preventDismiss) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onExpanded = {
|
||||||
|
isOpening = false
|
||||||
|
isOpen = true
|
||||||
|
hasInitiallyOpened = true
|
||||||
|
},
|
||||||
|
onHidden = {
|
||||||
|
if (hasInitiallyOpened) {
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getRootLayout().addView(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
this.isClosing = true
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
|
||||||
|
private fun getRootLayout(): FrameLayout = appContext.currentActivity!!.findViewById(android.R.id.content)
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
package expo.modules.bottomsheet
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import expo.modules.kotlin.modules.Module
|
||||||
|
import expo.modules.kotlin.modules.ModuleDefinition
|
||||||
|
|
||||||
|
class BottomSheetModule : Module() {
|
||||||
|
override fun definition() =
|
||||||
|
ModuleDefinition {
|
||||||
|
Name("BlueskyBottomSheet")
|
||||||
|
|
||||||
|
Function("getSafeAreaInset") {
|
||||||
|
return@Function 10 // @TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
View(BottomSheetView::class) {
|
||||||
|
Events(
|
||||||
|
arrayOf(
|
||||||
|
"onStateChange",
|
||||||
|
"onAttemptDismiss",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
AsyncFunction("dismiss") { view: BottomSheetView ->
|
||||||
|
view.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("preventDismiss") { view: BottomSheetView, prop: Boolean ->
|
||||||
|
view.preventDismiss = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("minHeight") { view: BottomSheetView, prop: Float ->
|
||||||
|
view.minHeight = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("maxHeight") { view: BottomSheetView, prop: Float ->
|
||||||
|
view.maxHeight = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("cornerRadius") { view: BottomSheetView, prop: Float ->
|
||||||
|
view.sheetState.value.cornerRadius = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("containerBackgroundColor") { view: BottomSheetView, prop: String ->
|
||||||
|
view.sheetState.value.containerBackgroundColor = Color.parseColor(prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("preventExpansion") { view: BottomSheetView, prop: Boolean ->
|
||||||
|
view.sheetState.value.preventExpansion = prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package expo.modules.bottomsheet
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
|
||||||
|
data class SheetState(
|
||||||
|
var isOpen: Boolean = false,
|
||||||
|
var cornerRadius: Float? = null,
|
||||||
|
var containerBackgroundColor: Int? = null,
|
||||||
|
var preventExpansion: Boolean = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SheetView(
|
||||||
|
state: MutableState<SheetState>,
|
||||||
|
innerView: View,
|
||||||
|
contentHeight: Float,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onExpanded: () -> Unit,
|
||||||
|
onHidden: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
shape =
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = state.value.cornerRadius ?: 0f,
|
||||||
|
topEnd = state.value.cornerRadius ?: 0f,
|
||||||
|
),
|
||||||
|
containerColor = Color(state.value.containerBackgroundColor ?: android.graphics.Color.TRANSPARENT),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(contentHeight.dp)
|
||||||
|
// Prevent covering up the handle
|
||||||
|
.padding(top = 34.dp),
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { innerView },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(sheetState.currentValue) {
|
||||||
|
if (sheetState.currentValue == SheetValue.PartiallyExpanded || sheetState.currentValue == SheetValue.Expanded) {
|
||||||
|
onExpanded()
|
||||||
|
} else if (sheetState.currentValue == SheetValue.Hidden) {
|
||||||
|
onHidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"platforms": ["ios", "android"],
|
||||||
|
"ios": {
|
||||||
|
"modules": ["BottomSheetModule"]
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"modules": ["expo.modules.bottomsheet.BottomSheetModule"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import {BottomSheetState, BottomSheetViewProps} from './src/BottomSheet.types'
|
||||||
|
import {BottomSheetView} from './src/BottomSheetView'
|
||||||
|
|
||||||
|
export {type BottomSheetState, BottomSheetView, type BottomSheetViewProps}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'BottomSheet'
|
||||||
|
s.version = '1.0.0'
|
||||||
|
s.summary = 'A bottom sheet for use in Bluesky'
|
||||||
|
s.description = 'A bottom sheet for use in Bluesky'
|
||||||
|
s.author = ''
|
||||||
|
s.homepage = 'https://github.com/bluesky-social/social-app'
|
||||||
|
s.platforms = { :ios => '15.0', :tvos => '15.0' }
|
||||||
|
s.source = { git: '' }
|
||||||
|
s.static_framework = true
|
||||||
|
|
||||||
|
s.dependency 'ExpoModulesCore'
|
||||||
|
|
||||||
|
# Swift/Objective-C compatibility
|
||||||
|
s.pod_target_xcconfig = {
|
||||||
|
'DEFINES_MODULE' => 'YES',
|
||||||
|
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||||
|
}
|
||||||
|
|
||||||
|
s.source_files = "**/*.{h,m,swift}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import ExpoModulesCore
|
||||||
|
|
||||||
|
public class BottomSheetModule: Module {
|
||||||
|
public func definition() -> ModuleDefinition {
|
||||||
|
Name("BottomSheet")
|
||||||
|
|
||||||
|
AsyncFunction("dismissAll") {
|
||||||
|
SheetManager.shared.dismissAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
View(SheetView.self) {
|
||||||
|
Events([
|
||||||
|
"onStateChange",
|
||||||
|
"onAttemptDismiss"
|
||||||
|
])
|
||||||
|
|
||||||
|
AsyncFunction("dismiss") { (view: SheetView) in
|
||||||
|
view.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncFunction("updateLayout") { (view: SheetView) in
|
||||||
|
view.updateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("cornerRadius") { (view: SheetView, prop: Float) in
|
||||||
|
view.cornerRadius = CGFloat(prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("preventDismiss") { (view: SheetView, prop: Bool) in
|
||||||
|
view.preventDismiss = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("minHeight") { (view: SheetView, prop: Double) in
|
||||||
|
view.minHeight = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("maxHeight") { (view: SheetView, prop: Double) in
|
||||||
|
view.maxHeight = prop
|
||||||
|
}
|
||||||
|
|
||||||
|
Prop("preventExpansion") { (view: SheetView, prop: Bool) in
|
||||||
|
view.preventExpansion = prop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
//
|
||||||
|
// SheetManager.swift
|
||||||
|
// Pods
|
||||||
|
//
|
||||||
|
// Created by Hailey on 10/1/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import ExpoModulesCore
|
||||||
|
|
||||||
|
class SheetManager {
|
||||||
|
static let shared = SheetManager()
|
||||||
|
|
||||||
|
private var sheetViews = NSHashTable<SheetView>(options: .weakMemory)
|
||||||
|
|
||||||
|
func add(_ view: SheetView) {
|
||||||
|
sheetViews.add(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(_ view: SheetView) {
|
||||||
|
sheetViews.remove(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissAll() {
|
||||||
|
sheetViews.allObjects.forEach { sheetView in
|
||||||
|
sheetView.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import ExpoModulesCore
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||||
|
// Views
|
||||||
|
private var sheetVc: SheetViewController?
|
||||||
|
private var innerView: UIView?
|
||||||
|
|
||||||
|
// Events
|
||||||
|
private let onStateChange = EventDispatcher()
|
||||||
|
private let onAttemptDismiss = EventDispatcher()
|
||||||
|
|
||||||
|
// Open event firing
|
||||||
|
private var isOpen: Bool = false {
|
||||||
|
didSet {
|
||||||
|
onStateChange([
|
||||||
|
"state": isOpen ? "open" : "closed"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// React view props
|
||||||
|
var preventDismiss = false
|
||||||
|
var preventExpansion = false
|
||||||
|
var cornerRadius: CGFloat?
|
||||||
|
var minHeight = 0.0
|
||||||
|
var maxHeight: CGFloat! {
|
||||||
|
didSet {
|
||||||
|
let screenHeight = Util.getScreenHeight() ?? 0
|
||||||
|
if maxHeight > screenHeight {
|
||||||
|
maxHeight = screenHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isOpening = false {
|
||||||
|
didSet {
|
||||||
|
if isOpening {
|
||||||
|
onStateChange([
|
||||||
|
"state": "opening"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var isClosing = false {
|
||||||
|
didSet {
|
||||||
|
if isClosing {
|
||||||
|
onStateChange([
|
||||||
|
"state": "closing"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
|
required init (appContext: AppContext? = nil) {
|
||||||
|
super.init(appContext: appContext)
|
||||||
|
self.maxHeight = Util.getScreenHeight()
|
||||||
|
SheetManager.shared.add(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want this view to actually get added to the tree, so we'll simply store it for adding
|
||||||
|
// to the SheetViewController
|
||||||
|
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
|
||||||
|
self.innerView = subview
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll grab the content height from here so we know the initial detent to set
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
guard let innerView = self.innerView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if innerView.subviews.count != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.present(contentHeight: innerView.subviews[0].frame.size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func destroy() {
|
||||||
|
self.isClosing = false
|
||||||
|
self.isOpen = false
|
||||||
|
self.sheetVc = nil
|
||||||
|
self.innerView = nil
|
||||||
|
SheetManager.shared.remove(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Presentation
|
||||||
|
|
||||||
|
func present(contentHeight: CGFloat) {
|
||||||
|
guard !self.isOpen,
|
||||||
|
let innerView = self.innerView,
|
||||||
|
let rvc = self.reactViewController() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let sheetVc = SheetViewController()
|
||||||
|
if let sheet = sheetVc.sheetPresentationController {
|
||||||
|
sheet.delegate = self
|
||||||
|
sheet.preferredCornerRadius = self.cornerRadius
|
||||||
|
}
|
||||||
|
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
|
||||||
|
sheetVc.view.addSubview(innerView)
|
||||||
|
|
||||||
|
self.sheetVc = sheetVc
|
||||||
|
self.isOpening = true
|
||||||
|
|
||||||
|
rvc.present(sheetVc, animated: true) { [weak self] in
|
||||||
|
self?.isOpening = false
|
||||||
|
self?.isOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout() {
|
||||||
|
if let contentHeight = self.innerView?.subviews[0].frame.size.height {
|
||||||
|
self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
|
||||||
|
preventExpansion: self.preventExpansion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismiss() {
|
||||||
|
self.isClosing = true
|
||||||
|
self.sheetVc?.dismiss(animated: true) { [weak self] in
|
||||||
|
self?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Utils
|
||||||
|
|
||||||
|
private func clampHeight(_ height: CGFloat) -> CGFloat {
|
||||||
|
if height < self.minHeight {
|
||||||
|
return self.minHeight
|
||||||
|
} else if height > self.maxHeight {
|
||||||
|
return self.maxHeight
|
||||||
|
}
|
||||||
|
return height
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UISheetPresentationControllerDelegate
|
||||||
|
|
||||||
|
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||||
|
self.onAttemptDismiss()
|
||||||
|
return !self.preventDismiss
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
self.isClosing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||||
|
self.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// SheetViewController.swift
|
||||||
|
// Pods
|
||||||
|
//
|
||||||
|
// Created by Hailey on 9/30/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SheetViewController: UIViewController {
|
||||||
|
init() {
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
self.modalPresentationStyle = .formSheet
|
||||||
|
self.isModalInPresentation = false
|
||||||
|
|
||||||
|
if let sheet = self.sheetPresentationController {
|
||||||
|
sheet.prefersGrabberVisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||||
|
guard let sheet = self.sheetPresentationController,
|
||||||
|
let screenHeight = Util.getScreenHeight()
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentHeight > screenHeight {
|
||||||
|
sheet.detents = [
|
||||||
|
.large()
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
sheet.detents = [
|
||||||
|
.custom { _ in
|
||||||
|
return contentHeight
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
sheet.detents = [
|
||||||
|
.medium()
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !preventExpansion {
|
||||||
|
sheet.detents.append(.large())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||||
|
if let sheet = self.sheetPresentationController {
|
||||||
|
sheet.animateChanges {
|
||||||
|
self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion)
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
sheet.invalidateDetents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Util.swift
|
||||||
|
// Pods
|
||||||
|
//
|
||||||
|
// Created by Hailey on 10/2/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
class Util {
|
||||||
|
static func getScreenHeight() -> CGFloat? {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||||
|
let window = windowScene.windows.first {
|
||||||
|
let safeAreaInsets = window.safeAreaInsets
|
||||||
|
let fullScreenHeight = UIScreen.main.bounds.height
|
||||||
|
return fullScreenHeight - (safeAreaInsets.top + safeAreaInsets.bottom)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {ColorValue, NativeSyntheticEvent} from 'react-native'
|
||||||
|
|
||||||
|
export type BottomSheetState = 'closed' | 'closing' | 'open' | 'opening'
|
||||||
|
|
||||||
|
export interface BottomSheetViewProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
cornerRadius?: number
|
||||||
|
preventDismiss?: boolean
|
||||||
|
preventExpansion?: boolean
|
||||||
|
containerBackgroundColor?: ColorValue
|
||||||
|
topInset?: number
|
||||||
|
bottomInset?: number
|
||||||
|
|
||||||
|
minHeight?: number
|
||||||
|
maxHeight?: number
|
||||||
|
|
||||||
|
onStateChange?: (
|
||||||
|
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||||
|
) => void
|
||||||
|
onAttemptDismiss?: (event: NativeSyntheticEvent<object>) => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import {
|
||||||
|
ColorValue,
|
||||||
|
Dimensions,
|
||||||
|
NativeSyntheticEvent,
|
||||||
|
StyleProp,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
} from 'react-native'
|
||||||
|
import {requireNativeViewManager} from 'expo-modules-core'
|
||||||
|
|
||||||
|
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
|
||||||
|
|
||||||
|
const screenHeight = Dimensions.get('screen').height
|
||||||
|
|
||||||
|
const NativeView: React.ComponentType<
|
||||||
|
BottomSheetViewProps & {
|
||||||
|
ref: React.RefObject<any>
|
||||||
|
style: StyleProp<ViewStyle>
|
||||||
|
}
|
||||||
|
> = requireNativeViewManager('BottomSheet')
|
||||||
|
|
||||||
|
export class BottomSheetView extends React.Component<
|
||||||
|
BottomSheetViewProps,
|
||||||
|
{
|
||||||
|
open: boolean
|
||||||
|
}
|
||||||
|
> {
|
||||||
|
ref = React.createRef<any>()
|
||||||
|
|
||||||
|
constructor(props: BottomSheetViewProps) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
present() {
|
||||||
|
this.setState({open: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
this.ref.current?.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStateChange = (
|
||||||
|
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||||
|
) => {
|
||||||
|
const {state} = event.nativeEvent
|
||||||
|
const isOpen = state !== 'closed'
|
||||||
|
this.setState({open: isOpen})
|
||||||
|
this.props.onStateChange?.(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getBackgroundColor = (): ColorValue | undefined => {
|
||||||
|
const parent = React.Children.toArray(
|
||||||
|
this.props.children,
|
||||||
|
)[0] as React.ReactElement
|
||||||
|
if (parent?.props?.style) {
|
||||||
|
const parentStyle = StyleSheet.flatten(parent.props.style) as ViewStyle
|
||||||
|
return parentStyle.backgroundColor ?? 'transparent'
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateLayout = () => {
|
||||||
|
this.ref.current?.updateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {children, ...rest} = this.props
|
||||||
|
const topInset = rest.topInset ?? 0
|
||||||
|
const bottomInset = rest.bottomInset ?? 0
|
||||||
|
|
||||||
|
if (!this.state.open) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const backgroundColor = this.getBackgroundColor()
|
||||||
|
return (
|
||||||
|
<NativeView
|
||||||
|
{...rest}
|
||||||
|
onStateChange={this.onStateChange}
|
||||||
|
ref={this.ref}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
height: screenHeight - topInset - bottomInset,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
containerBackgroundColor={backgroundColor}>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor,
|
||||||
|
paddingTop: topInset,
|
||||||
|
paddingBottom: bottomInset,
|
||||||
|
}}>
|
||||||
|
<View onLayout={this.updateLayout}>{children}</View>
|
||||||
|
</View>
|
||||||
|
</NativeView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {BottomSheetViewProps} from './BottomSheet.types'
|
||||||
|
|
||||||
|
export function BottomSheetView(_: BottomSheetViewProps) {
|
||||||
|
throw new Error('BottomSheetView is not available on web')
|
||||||
|
}
|
||||||
@@ -68,7 +68,6 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||||
"@haileyok/bluesky-bottom-sheet": "^0.1.1-alpha.11",
|
|
||||||
"@haileyok/bluesky-video": "0.1.10",
|
"@haileyok/bluesky-video": "0.1.10",
|
||||||
"@lingui/react": "^4.5.0",
|
"@lingui/react": "^4.5.0",
|
||||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, {useImperativeHandle} from 'react'
|
|||||||
import {StyleProp, TextInput, View, ViewStyle} from 'react-native'
|
import {StyleProp, TextInput, View, ViewStyle} from 'react-native'
|
||||||
import {GestureHandlerRootView, ScrollView} from 'react-native-gesture-handler'
|
import {GestureHandlerRootView, ScrollView} from 'react-native-gesture-handler'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {BlueskyBottomSheetView} from '@haileyok/bluesky-bottom-sheet'
|
|
||||||
|
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
} from '#/components/Dialog/types'
|
} from '#/components/Dialog/types'
|
||||||
import {createInput} from '#/components/forms/TextField'
|
import {createInput} from '#/components/forms/TextField'
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
|
import {BottomSheetView} from '../../../modules/bottom-sheet'
|
||||||
|
|
||||||
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
export {useDialogContext, useDialogControl} from '#/components/Dialog/context'
|
||||||
export * from '#/components/Dialog/types'
|
export * from '#/components/Dialog/types'
|
||||||
@@ -52,7 +52,7 @@ export function OuterWithoutPortal({
|
|||||||
testID,
|
testID,
|
||||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const ref = React.useRef<BlueskyBottomSheetView>(null)
|
const ref = React.useRef<BottomSheetView>(null)
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const closeCallbacks = React.useRef<(() => void)[]>([])
|
const closeCallbacks = React.useRef<(() => void)[]>([])
|
||||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||||
@@ -109,7 +109,7 @@ export function OuterWithoutPortal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Context.Provider value={context}>
|
<Context.Provider value={context}>
|
||||||
<BlueskyBottomSheetView
|
<BottomSheetView
|
||||||
ref={ref}
|
ref={ref}
|
||||||
topInset={30}
|
topInset={30}
|
||||||
bottomInset={insets.bottom}
|
bottomInset={insets.bottom}
|
||||||
@@ -123,7 +123,7 @@ export function OuterWithoutPortal({
|
|||||||
<Wrapper testID={testID} style={[t.atoms.bg]}>
|
<Wrapper testID={testID} style={[t.atoms.bg]}>
|
||||||
{children}
|
{children}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</BlueskyBottomSheetView>
|
</BottomSheetView>
|
||||||
</Context.Provider>
|
</Context.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import type {
|
|||||||
GestureResponderEvent,
|
GestureResponderEvent,
|
||||||
ScrollViewProps,
|
ScrollViewProps,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {BlueskyBottomSheetViewProps} from '@haileyok/bluesky-bottom-sheet'
|
|
||||||
|
|
||||||
import {ViewStyleProp} from '#/alf'
|
import {ViewStyleProp} from '#/alf'
|
||||||
|
import {BottomSheetViewProps} from '../../../modules/bottom-sheet'
|
||||||
|
|
||||||
type A11yProps = Required<AccessibilityProps>
|
type A11yProps = Required<AccessibilityProps>
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export type DialogControlOpenOptions = {
|
|||||||
export type DialogOuterProps = {
|
export type DialogOuterProps = {
|
||||||
control: DialogControlProps
|
control: DialogControlProps
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
nativeOptions?: Omit<BlueskyBottomSheetViewProps, 'children'>
|
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
||||||
webOptions?: {}
|
webOptions?: {}
|
||||||
testID?: string
|
testID?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4120,11 +4120,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||||
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||||
|
|
||||||
"@haileyok/bluesky-bottom-sheet@^0.1.1-alpha.11":
|
|
||||||
version "0.1.1-alpha.11"
|
|
||||||
resolved "https://registry.yarnpkg.com/@haileyok/bluesky-bottom-sheet/-/bluesky-bottom-sheet-0.1.1-alpha.11.tgz#a9c0e1c1903ba066c185481958f4ea9519cd5ba3"
|
|
||||||
integrity sha512-RtiEpEIXtejBzCtspFO7oEw6n87LnVQuSz+FGGEQZ6ywnRUBy6HZ5bpE3IFI7mmyF2YjAPg2nLzsqR9NXRMlhQ==
|
|
||||||
|
|
||||||
"@haileyok/bluesky-video@0.1.10":
|
"@haileyok/bluesky-video@0.1.10":
|
||||||
version "0.1.10"
|
version "0.1.10"
|
||||||
resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.10.tgz#2756e8c83a78caeb6b120a175578eac1eb6889a9"
|
resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.10.tgz#2756e8c83a78caeb6b120a175578eac1eb6889a9"
|
||||||
|
|||||||
Reference in New Issue
Block a user