Bluesky app fork with some witchin' additions 💫

Android sheets edge to edge (#8342)

authored by samuel.fm and committed by

GitHub 338016ed e2058c8e

+161 -228
+1 -1
modules/bottom-sheet/android/build.gradle
··· 44 45 dependencies { 46 implementation project(':expo-modules-core') 47 - implementation 'com.google.android.material:material:1.12.0' 48 implementation "com.facebook.react:react-native:+" 49 }
··· 44 45 dependencies { 46 implementation project(':expo-modules-core') 47 + implementation 'com.google.android.material:material:1.13.0' 48 implementation "com.facebook.react:react-native:+" 49 }
+115 -85
modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt
··· 5 import android.view.View 6 import android.view.ViewGroup 7 import android.view.ViewStructure 8 import android.view.accessibility.AccessibilityEvent 9 import android.widget.FrameLayout 10 import androidx.core.view.allViews 11 import com.facebook.react.bridge.LifecycleEventListener 12 import com.facebook.react.bridge.ReactContext ··· 15 import com.facebook.react.uimanager.events.EventDispatcher 16 import com.google.android.material.bottomsheet.BottomSheetBehavior 17 import com.google.android.material.bottomsheet.BottomSheetDialog 18 import expo.modules.kotlin.AppContext 19 import expo.modules.kotlin.viewevent.EventDispatcher 20 import expo.modules.kotlin.views.ExpoView ··· 29 30 private lateinit var dialogRootViewGroup: DialogRootViewGroup 31 private var eventDispatcher: EventDispatcher? = null 32 33 - private val rawScreenHeight = 34 context.resources.displayMetrics.heightPixels 35 .toFloat() 36 - private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat() 37 38 private fun getNavigationBarHeight(): Int { 39 val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") 40 return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 41 } 42 43 private val onAttemptDismiss by EventDispatcher() 44 private val onSnapPointChange by EventDispatcher() 45 private val onStateChange by EventDispatcher() 46 47 - // Props 48 var disableDrag = false 49 set(value) { 50 field = value ··· 56 field = value 57 this.dialog?.setCancelable(!value) 58 } 59 var preventExpansion = false 60 61 var minHeight = 0f 62 set(value) { 63 - field = 64 - if (value < 0) { 65 - 0f 66 - } else { 67 - dpToPx(value) 68 - } 69 } 70 71 - var maxHeight = this.safeScreenHeight 72 set(value) { 73 val px = dpToPx(value) 74 - field = 75 - if (px > this.safeScreenHeight) { 76 - this.safeScreenHeight 77 - } else { 78 - px 79 - } 80 } 81 82 private var isOpen: Boolean = false 83 set(value) { 84 field = value 85 - onStateChange( 86 - mapOf( 87 - "state" to if (value) "open" else "closed", 88 - ), 89 - ) 90 } 91 92 private var isOpening: Boolean = false 93 set(value) { 94 field = value 95 if (value) { 96 - onStateChange( 97 - mapOf( 98 - "state" to "opening", 99 - ), 100 - ) 101 } 102 } 103 ··· 105 set(value) { 106 field = value 107 if (value) { 108 - onStateChange( 109 - mapOf( 110 - "state" to "closing", 111 - ), 112 - ) 113 } 114 } 115 116 private var selectedSnapPoint = 0 117 set(value) { 118 if (field == value) return 119 - 120 field = value 121 - onSnapPointChange( 122 - mapOf( 123 - "snapPoint" to value, 124 - ), 125 - ) 126 } 127 - 128 - // Lifecycle 129 130 init { 131 (appContext.reactContext as? ReactContext)?.let { 132 it.addLifecycleEventListener(this) 133 this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id) 134 - 135 this.dialogRootViewGroup = DialogRootViewGroup(context) 136 this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher 137 } ··· 161 private fun getHalfExpandedRatio(contentHeight: Float): Float = 162 when { 163 // Full height sheets 164 - contentHeight >= safeScreenHeight -> 0.99f 165 - // Medium height sheets (>50% but <100%) 166 - contentHeight >= safeScreenHeight / 2 -> 167 - this.clampRatio(this.getTargetHeight() / safeScreenHeight) 168 - // Small height sheets (<50%) 169 - else -> 170 - this.clampRatio(this.getTargetHeight() / rawScreenHeight) 171 } 172 173 private fun present() { 174 if (this.isOpen || this.isOpening || this.isClosing) return 175 176 val contentHeight = this.getContentHeight() 177 - val dialog = BottomSheetDialog(context) 178 dialog.setContentView(dialogRootViewGroup) 179 dialog.setCancelable(!preventDismiss) 180 dialog.setOnDismissListener { 181 this.isClosing = true 182 this.destroy() 183 } 184 185 val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet) 186 bottomSheet?.let { 187 it.setBackgroundColor(0) ··· 194 behavior.isDraggable = true 195 behavior.isHideable = true 196 197 - if (contentHeight >= this.safeScreenHeight || this.minHeight >= this.safeScreenHeight) { 198 behavior.state = BottomSheetBehavior.STATE_EXPANDED 199 this.selectedSnapPoint = 2 200 } else { ··· 209 newState: Int, 210 ) { 211 when (newState) { 212 - BottomSheetBehavior.STATE_EXPANDED -> { 213 - selectedSnapPoint = 2 214 - } 215 - BottomSheetBehavior.STATE_COLLAPSED -> { 216 - selectedSnapPoint = 1 217 - } 218 - BottomSheetBehavior.STATE_HALF_EXPANDED -> { 219 - selectedSnapPoint = 1 220 - } 221 - BottomSheetBehavior.STATE_HIDDEN -> { 222 - selectedSnapPoint = 0 223 - } 224 } 225 } 226 ··· 231 }, 232 ) 233 } 234 this.isOpening = true 235 dialog.show() 236 this.dialog = dialog 237 } 238 239 fun updateLayout() { ··· 246 val currentState = behavior.state 247 248 val oldRatio = behavior.halfExpandedRatio 249 - var newRatio = getHalfExpandedRatio(contentHeight) 250 behavior.halfExpandedRatio = newRatio 251 252 - if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { 253 behavior.state = BottomSheetBehavior.STATE_EXPANDED 254 - } else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { 255 behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED 256 } else if (currentState == BottomSheetBehavior.STATE_HALF_EXPANDED && oldRatio != newRatio) { 257 behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED ··· 279 280 private fun getTargetHeight(): Float { 281 val contentHeight = this.getContentHeight() 282 - val height = 283 - if (contentHeight > maxHeight) { 284 - maxHeight 285 - } else if (contentHeight < minHeight) { 286 - minHeight 287 - } else { 288 - contentHeight 289 - } 290 - return height 291 } 292 293 - private fun clampRatio(ratio: Float): Float { 294 - if (ratio < 0.01) { 295 - return 0.01f 296 - } else if (ratio > 0.99) { 297 - return 0.99f 298 } 299 - return ratio 300 - } 301 302 private fun setDraggable(draggable: Boolean) { 303 val dialog = this.dialog ?: return ··· 322 // View overrides to pass to DialogRootViewGroup instead 323 324 override fun dispatchProvideStructure(structure: ViewStructure?) { 325 - if (structure == null) { 326 - return 327 - } 328 dialogRootViewGroup.dispatchProvideStructure(structure) 329 } 330 ··· 363 // https://stackoverflow.com/questions/11862391/getheight-px-or-dpi 364 fun dpToPx(dp: Float): Float { 365 val displayMetrics = context.resources.displayMetrics 366 - val px = dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT) 367 - return px 368 } 369 }
··· 5 import android.view.View 6 import android.view.ViewGroup 7 import android.view.ViewStructure 8 + import android.view.Window 9 import android.view.accessibility.AccessibilityEvent 10 import android.widget.FrameLayout 11 + import androidx.core.view.ViewCompat 12 + import androidx.core.view.WindowInsetsCompat 13 + import androidx.core.view.WindowInsetsControllerCompat 14 import androidx.core.view.allViews 15 import com.facebook.react.bridge.LifecycleEventListener 16 import com.facebook.react.bridge.ReactContext ··· 19 import com.facebook.react.uimanager.events.EventDispatcher 20 import com.google.android.material.bottomsheet.BottomSheetBehavior 21 import com.google.android.material.bottomsheet.BottomSheetDialog 22 + import com.google.android.material.internal.EdgeToEdgeUtils 23 import expo.modules.kotlin.AppContext 24 import expo.modules.kotlin.viewevent.EventDispatcher 25 import expo.modules.kotlin.views.ExpoView ··· 34 35 private lateinit var dialogRootViewGroup: DialogRootViewGroup 36 private var eventDispatcher: EventDispatcher? = null 37 + private var isKeyboardVisible: Boolean = false 38 39 + private val screenHeight = 40 context.resources.displayMetrics.heightPixels 41 .toFloat() 42 43 private fun getNavigationBarHeight(): Int { 44 val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") 45 return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 46 } 47 48 + private fun getStatusBarHeight(): Int { 49 + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") 50 + return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 51 + } 52 + 53 private val onAttemptDismiss by EventDispatcher() 54 private val onSnapPointChange by EventDispatcher() 55 private val onStateChange by EventDispatcher() 56 57 var disableDrag = false 58 set(value) { 59 field = value ··· 65 field = value 66 this.dialog?.setCancelable(!value) 67 } 68 + 69 var preventExpansion = false 70 71 var minHeight = 0f 72 set(value) { 73 + field = if (value < 0) 0f else dpToPx(value) 74 } 75 76 + var maxHeight = this.screenHeight 77 set(value) { 78 val px = dpToPx(value) 79 + field = if (px > this.screenHeight) this.screenHeight else px 80 } 81 82 private var isOpen: Boolean = false 83 set(value) { 84 field = value 85 + onStateChange(mapOf("state" to if (value) "open" else "closed")) 86 } 87 88 private var isOpening: Boolean = false 89 set(value) { 90 field = value 91 if (value) { 92 + onStateChange(mapOf("state" to "opening")) 93 } 94 } 95 ··· 97 set(value) { 98 field = value 99 if (value) { 100 + onStateChange(mapOf("state" to "closing")) 101 } 102 } 103 104 private var selectedSnapPoint = 0 105 set(value) { 106 if (field == value) return 107 field = value 108 + onSnapPointChange(mapOf("snapPoint" to value)) 109 } 110 111 init { 112 (appContext.reactContext as? ReactContext)?.let { 113 it.addLifecycleEventListener(this) 114 this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id) 115 this.dialogRootViewGroup = DialogRootViewGroup(context) 116 this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher 117 } ··· 141 private fun getHalfExpandedRatio(contentHeight: Float): Float = 142 when { 143 // Full height sheets 144 + contentHeight >= screenHeight -> 0.99f 145 + else -> this.clampRatio(this.getTargetHeight() / screenHeight) 146 } 147 148 private fun present() { 149 if (this.isOpen || this.isOpening || this.isClosing) return 150 151 val contentHeight = this.getContentHeight() 152 + 153 + var activityWindow: Window? = null 154 + var currentContext = context 155 + while (currentContext != null) { 156 + if (currentContext is android.app.Activity) { 157 + activityWindow = currentContext.window 158 + break 159 + } 160 + currentContext = (currentContext as? android.content.ContextWrapper)?.baseContext 161 + } 162 + 163 + val originalStatusBarAppearance = 164 + activityWindow?.let { window -> 165 + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars 166 + } 167 + val originalNavBarAppearance = 168 + activityWindow?.let { window -> 169 + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars 170 + } 171 + 172 + val dialog = BottomSheetDialog(context, R.style.EdgeToEdgeBottomSheetDialogTheme) 173 dialog.setContentView(dialogRootViewGroup) 174 dialog.setCancelable(!preventDismiss) 175 + dialog.setDismissWithAnimation(true) 176 dialog.setOnDismissListener { 177 this.isClosing = true 178 this.destroy() 179 } 180 181 + dialog.setOnShowListener { 182 + dialog.window?.let { window -> 183 + val insetsController = WindowInsetsControllerCompat(window, window.decorView) 184 + if (originalNavBarAppearance != null) { 185 + insetsController.isAppearanceLightNavigationBars = originalNavBarAppearance 186 + } 187 + if (originalStatusBarAppearance != null) { 188 + EdgeToEdgeUtils.setLightStatusBar(window, originalStatusBarAppearance) 189 + } 190 + } 191 + } 192 + 193 val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet) 194 bottomSheet?.let { 195 it.setBackgroundColor(0) ··· 202 behavior.isDraggable = true 203 behavior.isHideable = true 204 205 + if (preventExpansion) { 206 + behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() 207 + } else { 208 + behavior.maxHeight = (screenHeight - getStatusBarHeight()).toInt() 209 + } 210 + 211 + val targetHeight = this.getTargetHeight() 212 + val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight() 213 + val shouldBeExpanded = targetHeight >= availableHeight 214 + 215 + if (shouldBeExpanded) { 216 behavior.state = BottomSheetBehavior.STATE_EXPANDED 217 this.selectedSnapPoint = 2 218 } else { ··· 227 newState: Int, 228 ) { 229 when (newState) { 230 + BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2 231 + BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1 232 + BottomSheetBehavior.STATE_HALF_EXPANDED -> selectedSnapPoint = 1 233 + BottomSheetBehavior.STATE_HIDDEN -> selectedSnapPoint = 0 234 } 235 } 236 ··· 241 }, 242 ) 243 } 244 + 245 this.isOpening = true 246 dialog.show() 247 this.dialog = dialog 248 + 249 + ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets -> 250 + val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) 251 + val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet) 252 + val behavior = bottomSheet?.let { BottomSheetBehavior.from(it) } 253 + 254 + val wasKeyboardVisible = isKeyboardVisible 255 + isKeyboardVisible = imeVisible 256 + 257 + if (imeVisible && behavior?.state == BottomSheetBehavior.STATE_HALF_EXPANDED) { 258 + behavior.state = BottomSheetBehavior.STATE_EXPANDED 259 + } else if (!imeVisible && wasKeyboardVisible) { 260 + updateLayout() 261 + } 262 + insets 263 + } 264 } 265 266 fun updateLayout() { ··· 273 val currentState = behavior.state 274 275 val oldRatio = behavior.halfExpandedRatio 276 + val newRatio = getHalfExpandedRatio(contentHeight) 277 behavior.halfExpandedRatio = newRatio 278 279 + if (preventExpansion) { 280 + behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() 281 + } 282 + 283 + val targetHeight = this.getTargetHeight() 284 + val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight() 285 + val shouldBeExpanded = targetHeight >= availableHeight 286 + 287 + if (isKeyboardVisible) { 288 + if (behavior.state != BottomSheetBehavior.STATE_EXPANDED) { 289 + behavior.state = BottomSheetBehavior.STATE_EXPANDED 290 + } 291 + } else if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) { 292 behavior.state = BottomSheetBehavior.STATE_EXPANDED 293 + } else if (!shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { 294 behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED 295 } else if (currentState == BottomSheetBehavior.STATE_HALF_EXPANDED && oldRatio != newRatio) { 296 behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED ··· 318 319 private fun getTargetHeight(): Float { 320 val contentHeight = this.getContentHeight() 321 + return when { 322 + contentHeight > maxHeight -> maxHeight 323 + contentHeight < minHeight -> minHeight 324 + else -> contentHeight 325 + } 326 } 327 328 + private fun clampRatio(ratio: Float): Float = 329 + when { 330 + ratio < 0.01 -> 0.01f 331 + ratio > 0.99 -> 0.99f 332 + else -> ratio 333 } 334 335 private fun setDraggable(draggable: Boolean) { 336 val dialog = this.dialog ?: return ··· 355 // View overrides to pass to DialogRootViewGroup instead 356 357 override fun dispatchProvideStructure(structure: ViewStructure?) { 358 + if (structure == null) return 359 dialogRootViewGroup.dispatchProvideStructure(structure) 360 } 361 ··· 394 // https://stackoverflow.com/questions/11862391/getheight-px-or-dpi 395 fun dpToPx(dp: Float): Float { 396 val displayMetrics = context.resources.displayMetrics 397 + return dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT) 398 } 399 }
+2
modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt
··· 52 if (ReactFeatureFlags.dispatchPointerEvents) { 53 jSPointerDispatcher = JSPointerDispatcher(this) 54 } 55 } 56 57 override fun onSizeChanged(
··· 52 if (ReactFeatureFlags.dispatchPointerEvents) { 53 jSPointerDispatcher = JSPointerDispatcher(this) 54 } 55 + 56 + fitsSystemWindows = false 57 } 58 59 override fun onSizeChanged(
+20
modules/bottom-sheet/android/src/main/res/values/styles.xml
···
··· 1 + <?xml version="1.0" encoding="utf-8"?> 2 + <resources> 3 + <style name="EdgeToEdgeBottomSheetDialogTheme" parent="Theme.Material3.DayNight.BottomSheetDialog"> 4 + <!-- Enable edge-to-edge --> 5 + <item name="android:navigationBarColor">@android:color/transparent</item> 6 + <item name="android:statusBarColor">@android:color/transparent</item> 7 + <item name="android:windowIsFloating">false</item> 8 + <item name="enableEdgeToEdge">true</item> 9 + 10 + <!-- Configure bottom sheet to respect system window insets --> 11 + <item name="bottomSheetStyle">@style/EdgeToEdgeBottomSheet</item> 12 + </style> 13 + 14 + <style name="EdgeToEdgeBottomSheet" parent="Widget.Material3.BottomSheet"> 15 + <item name="paddingBottomSystemWindowInsets">false</item> 16 + <item name="paddingLeftSystemWindowInsets">true</item> 17 + <item name="paddingRightSystemWindowInsets">true</item> 18 + <item name="paddingTopSystemWindowInsets">false</item> 19 + </style> 20 + </resources>
+1
modules/bottom-sheet/src/BottomSheetNativeComponent.tsx
··· 175 Platform.OS === 'android' && { 176 borderTopLeftRadius: cornerRadius, 177 borderTopRightRadius: cornerRadius, 178 }, 179 extraStyles, 180 ]}>
··· 175 Platform.OS === 'android' && { 176 borderTopLeftRadius: cornerRadius, 177 borderTopRightRadius: cornerRadius, 178 + overflow: 'hidden', 179 }, 180 extraStyles, 181 ]}>
+3
modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt
··· 34 is Boolean -> { 35 putBoolean(key, value) 36 } 37 is String -> { 38 putString(key, value) 39 } 40 is Array<*> -> { 41 putStringSet(key, value.map { it.toString() }.toSet()) 42 } 43 is Map<*, *> -> { 44 putStringSet(key, value.map { it.toString() }.toSet()) 45 }
··· 34 is Boolean -> { 35 putBoolean(key, value) 36 } 37 + 38 is String -> { 39 putString(key, value) 40 } 41 + 42 is Array<*> -> { 43 putStringSet(key, value.map { it.toString() }.toSet()) 44 } 45 + 46 is Map<*, *> -> { 47 putStringSet(key, value.map { it.toString() }.toSet()) 48 }
+2 -2
modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt
··· 117 118 private fun handleImageIntents( 119 uris: List<Uri>, 120 - text: String? 121 ) { 122 var allParams = "" 123 ··· 145 146 private fun handleVideoIntents( 147 uris: List<Uri>, 148 - text: String? 149 ) { 150 val uri = uris[0] 151 // If there is no extension for the file, substringAfterLast returns the original string - not
··· 117 118 private fun handleImageIntents( 119 uris: List<Uri>, 120 + text: String?, 121 ) { 122 var allParams = "" 123 ··· 145 146 private fun handleVideoIntents( 147 uris: List<Uri>, 148 + text: String?, 149 ) { 150 val uri = uris[0] 151 // If there is no extension for the file, substringAfterLast returns the original string - not
+1 -1
package.json
··· 202 "react-native-edge-to-edge": "^1.6.0", 203 "react-native-gesture-handler": "~2.28.0", 204 "react-native-get-random-values": "~1.11.0", 205 - "react-native-keyboard-controller": "1.18.5", 206 "react-native-pager-view": "6.8.0", 207 "react-native-progress": "bluesky-social/react-native-progress", 208 "react-native-qrcode-styled": "^0.3.3",
··· 202 "react-native-edge-to-edge": "^1.6.0", 203 "react-native-gesture-handler": "~2.28.0", 204 "react-native-get-random-values": "~1.11.0", 205 + "react-native-keyboard-controller": "^1.20.7", 206 "react-native-pager-view": "6.8.0", 207 "react-native-progress": "bluesky-social/react-native-progress", 208 "react-native-qrcode-styled": "^0.3.3",
+1 -1
src/App.native.tsx
··· 3 4 import React, {useEffect, useState} from 'react' 5 import {GestureHandlerRootView} from 'react-native-gesture-handler' 6 import { 7 initialWindowMetrics, 8 SafeAreaProvider, ··· 14 import {useLingui} from '@lingui/react' 15 import * as Sentry from '@sentry/react-native' 16 17 - import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController' 18 import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' 19 import {QueryProvider} from '#/lib/react-query' 20 import {s} from '#/lib/styles'
··· 3 4 import React, {useEffect, useState} from 'react' 5 import {GestureHandlerRootView} from 'react-native-gesture-handler' 6 + import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller' 7 import { 8 initialWindowMetrics, 9 SafeAreaProvider, ··· 15 import {useLingui} from '@lingui/react' 16 import * as Sentry from '@sentry/react-native' 17 18 import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' 19 import {QueryProvider} from '#/lib/react-query' 20 import {s} from '#/lib/styles'
+3 -7
src/components/Dialog/index.tsx
··· 11 } from 'react-native' 12 import { 13 KeyboardAwareScrollView, 14 useKeyboardHandler, 15 useReanimatedKeyboardAnimation, 16 } from 'react-native-keyboard-controller' ··· 23 import {msg} from '@lingui/macro' 24 import {useLingui} from '@lingui/react' 25 26 - import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 27 import {ScrollProvider} from '#/lib/ScrollContext' 28 import {logger} from '#/logger' 29 import {useA11y} from '#/state/a11y' ··· 209 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 210 const insets = useSafeAreaInsets() 211 212 - useEnableKeyboardController(IS_IOS) 213 - 214 const [keyboardHeight, setKeyboardHeight] = React.useState(0) 215 216 useKeyboardHandler( 217 { 218 onEnd: e => { ··· 231 } 232 paddingBottom = Math.max(paddingBottom, tokens.space._2xl) 233 } else { 234 - paddingBottom += keyboardHeight 235 if (nativeSnapPoint === BottomSheetSnapPoint.Full) { 236 paddingBottom += insets.top 237 } ··· 259 {paddingBottom}, 260 contentContainerStyle, 261 ]} 262 - ref={ref} 263 showsVerticalScrollIndicator={IS_ANDROID ? false : undefined} 264 {...props} 265 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} ··· 288 >(function InnerFlatList({footer, style, ...props}, ref) { 289 const insets = useSafeAreaInsets() 290 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 291 - 292 - useEnableKeyboardController(IS_IOS) 293 294 const onScroll = (e: ScrollEvent) => { 295 'worklet'
··· 11 } from 'react-native' 12 import { 13 KeyboardAwareScrollView, 14 + type KeyboardAwareScrollViewRef, 15 useKeyboardHandler, 16 useReanimatedKeyboardAnimation, 17 } from 'react-native-keyboard-controller' ··· 24 import {msg} from '@lingui/macro' 25 import {useLingui} from '@lingui/react' 26 27 import {ScrollProvider} from '#/lib/ScrollContext' 28 import {logger} from '#/logger' 29 import {useA11y} from '#/state/a11y' ··· 209 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 210 const insets = useSafeAreaInsets() 211 212 const [keyboardHeight, setKeyboardHeight] = React.useState(0) 213 214 + // note: iOS-only. keyboard-controller doesn't seem to work inside the sheets on Android 215 useKeyboardHandler( 216 { 217 onEnd: e => { ··· 230 } 231 paddingBottom = Math.max(paddingBottom, tokens.space._2xl) 232 } else { 233 if (nativeSnapPoint === BottomSheetSnapPoint.Full) { 234 paddingBottom += insets.top 235 } ··· 257 {paddingBottom}, 258 contentContainerStyle, 259 ]} 260 + ref={ref as React.Ref<KeyboardAwareScrollViewRef>} 261 showsVerticalScrollIndicator={IS_ANDROID ? false : undefined} 262 {...props} 263 bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} ··· 286 >(function InnerFlatList({footer, style, ...props}, ref) { 287 const insets = useSafeAreaInsets() 288 const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() 289 290 const onScroll = (e: ScrollEvent) => { 291 'worklet'
+1 -3
src/components/verification/VerifierDialog.tsx
··· 28 verificationState: FullVerificationState 29 }) { 30 return ( 31 - <Dialog.Outer control={control}> 32 <Dialog.Handle /> 33 <Inner 34 control={control} ··· 123 }), 124 )} 125 size="small" 126 - variant="solid" 127 color="primary" 128 style={[a.justify_center]} 129 onPress={() => { ··· 138 <Button 139 label={_(msg`Close dialog`)} 140 size="small" 141 - variant="solid" 142 color="secondary" 143 onPress={() => { 144 control.close()
··· 28 verificationState: FullVerificationState 29 }) { 30 return ( 31 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 32 <Dialog.Handle /> 33 <Inner 34 control={control} ··· 123 }), 124 )} 125 size="small" 126 color="primary" 127 style={[a.justify_center]} 128 onPress={() => { ··· 137 <Button 138 label={_(msg`Close dialog`)} 139 size="small" 140 color="secondary" 141 onPress={() => { 142 control.close()
-107
src/lib/hooks/useEnableKeyboardController.tsx
··· 1 - import { 2 - createContext, 3 - useCallback, 4 - useContext, 5 - useEffect, 6 - useMemo, 7 - useRef, 8 - } from 'react' 9 - import { 10 - KeyboardProvider, 11 - useKeyboardController, 12 - } from 'react-native-keyboard-controller' 13 - import {useFocusEffect} from '@react-navigation/native' 14 - 15 - const KeyboardControllerRefCountContext = createContext<{ 16 - incrementRefCount: () => void 17 - decrementRefCount: () => void 18 - }>({ 19 - incrementRefCount: () => {}, 20 - decrementRefCount: () => {}, 21 - }) 22 - KeyboardControllerRefCountContext.displayName = 23 - 'KeyboardControllerRefCountContext' 24 - 25 - export function KeyboardControllerProvider({ 26 - children, 27 - }: { 28 - children: React.ReactNode 29 - }) { 30 - return ( 31 - <KeyboardProvider enabled={false} preload={false}> 32 - <KeyboardControllerProviderInner> 33 - {children} 34 - </KeyboardControllerProviderInner> 35 - </KeyboardProvider> 36 - ) 37 - } 38 - 39 - function KeyboardControllerProviderInner({ 40 - children, 41 - }: { 42 - children: React.ReactNode 43 - }) { 44 - const {setEnabled} = useKeyboardController() 45 - const refCount = useRef(0) 46 - 47 - const value = useMemo( 48 - () => ({ 49 - incrementRefCount: () => { 50 - refCount.current++ 51 - setEnabled(refCount.current > 0) 52 - }, 53 - decrementRefCount: () => { 54 - refCount.current-- 55 - setEnabled(refCount.current > 0) 56 - 57 - if (__DEV__ && refCount.current < 0) { 58 - console.error('KeyboardController ref count < 0') 59 - } 60 - }, 61 - }), 62 - [setEnabled], 63 - ) 64 - 65 - return ( 66 - <KeyboardControllerRefCountContext.Provider value={value}> 67 - {children} 68 - </KeyboardControllerRefCountContext.Provider> 69 - ) 70 - } 71 - 72 - export function useEnableKeyboardController(shouldEnable: boolean) { 73 - const {incrementRefCount, decrementRefCount} = useContext( 74 - KeyboardControllerRefCountContext, 75 - ) 76 - 77 - useEffect(() => { 78 - if (!shouldEnable) { 79 - return 80 - } 81 - incrementRefCount() 82 - return () => { 83 - decrementRefCount() 84 - } 85 - }, [shouldEnable, incrementRefCount, decrementRefCount]) 86 - } 87 - 88 - /** 89 - * Like `useEnableKeyboardController`, but using `useFocusEffect` 90 - */ 91 - export function useEnableKeyboardControllerScreen(shouldEnable: boolean) { 92 - const {incrementRefCount, decrementRefCount} = useContext( 93 - KeyboardControllerRefCountContext, 94 - ) 95 - 96 - useFocusEffect( 97 - useCallback(() => { 98 - if (!shouldEnable) { 99 - return 100 - } 101 - incrementRefCount() 102 - return () => { 103 - decrementRefCount() 104 - } 105 - }, [shouldEnable, incrementRefCount, decrementRefCount]), 106 - ) 107 - }
···
-3
src/screens/FindContactsFlowScreen.tsx
··· 4 import {useLingui} from '@lingui/react' 5 import {usePreventRemove} from '@react-navigation/native' 6 7 - import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 8 import { 9 type AllNavigatorParams, 10 type NativeStackScreenProps, ··· 36 setTransitionDirection('Forward') 37 }) 38 }) 39 - 40 - useEnableKeyboardControllerScreen(true) 41 42 const setMinimalShellMode = useSetMinimalShellMode() 43 const effect = useCallback(() => {
··· 4 import {useLingui} from '@lingui/react' 5 import {usePreventRemove} from '@react-navigation/native' 6 7 import { 8 type AllNavigatorParams, 9 type NativeStackScreenProps, ··· 35 setTransitionDirection('Forward') 36 }) 37 }) 38 39 const setMinimalShellMode = useSetMinimalShellMode() 40 const effect = useCallback(() => {
-3
src/screens/Messages/Conversation.tsx
··· 15 } from '@react-navigation/native' 16 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 17 18 - import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 19 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 20 import { 21 type CommonNavigatorParams, ··· 67 68 const convoId = route.params.conversation 69 const {setCurrentConvoId} = useCurrentConvoId() 70 - 71 - useEnableKeyboardControllerScreen(true) 72 73 useFocusEffect( 74 useCallback(() => {
··· 15 } from '@react-navigation/native' 16 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 17 18 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 19 import { 20 type CommonNavigatorParams, ··· 66 67 const convoId = route.params.conversation 68 const {setCurrentConvoId} = useCurrentConvoId() 69 70 useFocusEffect( 71 useCallback(() => {
-3
src/screens/Onboarding/index.tsx
··· 2 import {View} from 'react-native' 3 import * as bcp47Match from 'bcp-47-match' 4 5 - import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 6 import {useLanguagePrefs} from '#/state/preferences' 7 import { 8 Layout, ··· 59 createInitialOnboardingState, 60 ) 61 const [contactsFlowState, contactsFlowDispatch] = useFindContactsFlowState() 62 - 63 - useEnableKeyboardControllerScreen(true) 64 65 return ( 66 <Portal>
··· 2 import {View} from 'react-native' 3 import * as bcp47Match from 'bcp-47-match' 4 5 import {useLanguagePrefs} from '#/state/preferences' 6 import { 7 Layout, ··· 58 createInitialOnboardingState, 59 ) 60 const [contactsFlowState, contactsFlowDispatch] = useFindContactsFlowState() 61 62 return ( 63 <Portal>
-3
src/screens/StarterPack/Wizard/index.tsx
··· 16 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 17 18 import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' 19 - import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController' 20 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 21 import { 22 type CommonNavigatorParams, ··· 184 gestureEnabled: false, 185 }) 186 }, [navigation]) 187 - 188 - useEnableKeyboardControllerScreen(true) 189 190 useFocusEffect( 191 React.useCallback(() => {
··· 16 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 17 18 import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' 19 import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' 20 import { 21 type CommonNavigatorParams, ··· 183 gestureEnabled: false, 184 }) 185 }, [navigation]) 186 187 useFocusEffect( 188 React.useCallback(() => {
-3
src/screens/Takendown.tsx
··· 12 BLUESKY_MOD_SERVICE_HEADERS, 13 MAX_REPORT_REASON_GRAPHEME_LENGTH, 14 } from '#/lib/constants' 15 - import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' 16 import {cleanError} from '#/lib/strings/errors' 17 import {useAgent, useSession, useSessionApi} from '#/state/session' 18 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' ··· 120 ) 121 122 const webLayout = IS_WEB && gtMobile 123 - 124 - useEnableKeyboardController(true) 125 126 return ( 127 <View style={[a.util_screen_outer, a.flex_1]}>
··· 12 BLUESKY_MOD_SERVICE_HEADERS, 13 MAX_REPORT_REASON_GRAPHEME_LENGTH, 14 } from '#/lib/constants' 15 import {cleanError} from '#/lib/strings/errors' 16 import {useAgent, useSession, useSessionApi} from '#/state/session' 17 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' ··· 119 ) 120 121 const webLayout = IS_WEB && gtMobile 122 123 return ( 124 <View style={[a.util_screen_outer, a.flex_1]}>
+7 -2
src/view/com/composer/photos/ImageAltTextDialog.tsx
··· 5 import {useLingui} from '@lingui/react' 6 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 import {enforceLen} from '#/lib/strings/helpers' 9 import {type ComposerImage} from '#/state/gallery' 10 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' ··· 28 image, 29 onChange, 30 }: Props): React.ReactNode => { 31 const [altText, setAltText] = React.useState(image.alt) 32 33 return ( ··· 38 ...image, 39 alt: enforceLen(altText, MAX_ALT_TEXT, true), 40 }) 41 - }}> 42 <Dialog.Handle /> 43 <ImageAltTextInner 44 control={control} ··· 64 const {_, i18n} = useLingui() 65 const t = useTheme() 66 const windim = useWindowDimensions() 67 68 const imageStyle = React.useMemo<ImageStyle>(() => { 69 const maxWidth = IS_WEB ? 450 : windim.width ··· 165 </AltTextCounterWrapper> 166 </View> 167 {/* Maybe fix this later -h */} 168 - {IS_ANDROID ? <View style={{height: 300}} /> : null} 169 </Dialog.ScrollableInner> 170 ) 171 }
··· 5 import {useLingui} from '@lingui/react' 6 7 import {MAX_ALT_TEXT} from '#/lib/constants' 8 + import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible' 9 import {enforceLen} from '#/lib/strings/helpers' 10 import {type ComposerImage} from '#/state/gallery' 11 import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' ··· 29 image, 30 onChange, 31 }: Props): React.ReactNode => { 32 + const {height: minHeight} = useWindowDimensions() 33 const [altText, setAltText] = React.useState(image.alt) 34 35 return ( ··· 40 ...image, 41 alt: enforceLen(altText, MAX_ALT_TEXT, true), 42 }) 43 + }} 44 + nativeOptions={{minHeight}}> 45 <Dialog.Handle /> 46 <ImageAltTextInner 47 control={control} ··· 67 const {_, i18n} = useLingui() 68 const t = useTheme() 69 const windim = useWindowDimensions() 70 + 71 + const [isKeyboardVisible] = useIsKeyboardVisible() 72 73 const imageStyle = React.useMemo<ImageStyle>(() => { 74 const maxWidth = IS_WEB ? 450 : windim.width ··· 170 </AltTextCounterWrapper> 171 </View> 172 {/* Maybe fix this later -h */} 173 + {IS_ANDROID && isKeyboardVisible ? <View style={{height: 300}} /> : null} 174 </Dialog.ScrollableInner> 175 ) 176 }
+4 -4
yarn.lock
··· 17163 resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358" 17164 integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q== 17165 17166 - react-native-keyboard-controller@1.18.5: 17167 - version "1.18.5" 17168 - resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.18.5.tgz#ae12131f2019c574178479d2c55784f55e08bb68" 17169 - integrity sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ== 17170 dependencies: 17171 react-native-is-edge-to-edge "^1.2.1" 17172
··· 17163 resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358" 17164 integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q== 17165 17166 + react-native-keyboard-controller@^1.20.7: 17167 + version "1.20.7" 17168 + resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.20.7.tgz#e1be1c15a5eb10b96a40a0812d8472e6e4bd8f29" 17169 + integrity sha512-G8S5jz1FufPrcL1vPtReATx+jJhT/j+sTqxMIb30b1z7cYEfMlkIzOCyaHgf6IMB2KA9uBmnA5M6ve2A9Ou4kw== 17170 dependencies: 17171 react-native-is-edge-to-edge "^1.2.1" 17172