diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml index 2ebbd66ec9..a2576e329a 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -118,7 +118,12 @@ jobs: fi - name: 🚀 Deploy - run: eas submit -p ios --non-interactive --path ios-build/ios/build/Bluesky.ipa + env: + PROFILE: ${{ inputs.profile || 'testflight' }} + run: > + eas submit -p ios --non-interactive + --path ios-build/ios/build/Bluesky.ipa + --what-to-test "$([[ "$PROFILE" == "production" ]] && echo "Production build" || echo "TestFlight build")" - name: 🪲 Upload dSYM to Sentry run: > diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index ee59cad977..46c848b879 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -242,7 +242,7 @@ jobs: --local --output build.ipa --non-interactive - name: 🚀 Deploy - run: eas submit -p ios --non-interactive --path build.ipa + run: eas submit -p ios --non-interactive --path build.ipa --what-to-test "TestFlight build" - name: ⬇️ Restore Cache id: get-base-commit diff --git a/modules/bottom-sheet/android/build.gradle b/modules/bottom-sheet/android/build.gradle index a1d423044b..c4c051b70f 100644 --- a/modules/bottom-sheet/android/build.gradle +++ b/modules/bottom-sheet/android/build.gradle @@ -44,6 +44,6 @@ android { dependencies { implementation project(':expo-modules-core') - implementation 'com.google.android.material:material:1.12.0' + implementation 'com.google.android.material:material:1.13.0' implementation "com.facebook.react:react-native:+" } diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt index fa42e37d5a..300c338f9f 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt @@ -5,8 +5,12 @@ import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup import android.view.ViewStructure +import android.view.Window import android.view.accessibility.AccessibilityEvent import android.widget.FrameLayout +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.allViews import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.ReactContext @@ -15,6 +19,7 @@ import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.events.EventDispatcher import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.internal.EdgeToEdgeUtils import expo.modules.kotlin.AppContext import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.views.ExpoView @@ -29,22 +34,26 @@ class BottomSheetView( private lateinit var dialogRootViewGroup: DialogRootViewGroup private var eventDispatcher: EventDispatcher? = null + private var isKeyboardVisible: Boolean = false - private val rawScreenHeight = + private val screenHeight = context.resources.displayMetrics.heightPixels .toFloat() - private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).toFloat() private fun getNavigationBarHeight(): Int { val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android") return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 } + private fun getStatusBarHeight(): Int { + val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") + return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 + } + private val onAttemptDismiss by EventDispatcher() private val onSnapPointChange by EventDispatcher() private val onStateChange by EventDispatcher() - // Props var disableDrag = false set(value) { field = value @@ -56,48 +65,31 @@ class BottomSheetView( field = value this.dialog?.setCancelable(!value) } + var preventExpansion = false var minHeight = 0f set(value) { - field = - if (value < 0) { - 0f - } else { - dpToPx(value) - } + field = if (value < 0) 0f else dpToPx(value) } - var maxHeight = this.safeScreenHeight + var maxHeight = this.screenHeight set(value) { val px = dpToPx(value) - field = - if (px > this.safeScreenHeight) { - this.safeScreenHeight - } else { - px - } + field = if (px > this.screenHeight) this.screenHeight else px } private var isOpen: Boolean = false set(value) { field = value - onStateChange( - mapOf( - "state" to if (value) "open" else "closed", - ), - ) + 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", - ), - ) + onStateChange(mapOf("state" to "opening")) } } @@ -105,33 +97,21 @@ class BottomSheetView( set(value) { field = value if (value) { - onStateChange( - mapOf( - "state" to "closing", - ), - ) + onStateChange(mapOf("state" to "closing")) } } private var selectedSnapPoint = 0 set(value) { if (field == value) return - field = value - onSnapPointChange( - mapOf( - "snapPoint" to value, - ), - ) + onSnapPointChange(mapOf("snapPoint" to value)) } - // Lifecycle - init { (appContext.reactContext as? ReactContext)?.let { it.addLifecycleEventListener(this) this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id) - this.dialogRootViewGroup = DialogRootViewGroup(context) this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher } @@ -161,27 +141,55 @@ class BottomSheetView( private fun getHalfExpandedRatio(contentHeight: Float): Float = when { // Full height sheets - contentHeight >= safeScreenHeight -> 0.99f - // Medium height sheets (>50% but <100%) - contentHeight >= safeScreenHeight / 2 -> - this.clampRatio(this.getTargetHeight() / safeScreenHeight) - // Small height sheets (<50%) - else -> - this.clampRatio(this.getTargetHeight() / rawScreenHeight) + contentHeight >= screenHeight -> 0.99f + else -> this.clampRatio(this.getTargetHeight() / screenHeight) } private fun present() { if (this.isOpen || this.isOpening || this.isClosing) return val contentHeight = this.getContentHeight() - val dialog = BottomSheetDialog(context) + + var activityWindow: Window? = null + var currentContext = context + while (currentContext != null) { + if (currentContext is android.app.Activity) { + activityWindow = currentContext.window + break + } + currentContext = (currentContext as? android.content.ContextWrapper)?.baseContext + } + + val originalStatusBarAppearance = + activityWindow?.let { window -> + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars + } + val originalNavBarAppearance = + activityWindow?.let { window -> + WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars + } + + val dialog = BottomSheetDialog(context, R.style.EdgeToEdgeBottomSheetDialogTheme) dialog.setContentView(dialogRootViewGroup) dialog.setCancelable(!preventDismiss) + dialog.setDismissWithAnimation(true) dialog.setOnDismissListener { this.isClosing = true this.destroy() } + dialog.setOnShowListener { + dialog.window?.let { window -> + val insetsController = WindowInsetsControllerCompat(window, window.decorView) + if (originalNavBarAppearance != null) { + insetsController.isAppearanceLightNavigationBars = originalNavBarAppearance + } + if (originalStatusBarAppearance != null) { + EdgeToEdgeUtils.setLightStatusBar(window, originalStatusBarAppearance) + } + } + } + val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) bottomSheet?.let { it.setBackgroundColor(0) @@ -194,7 +202,17 @@ class BottomSheetView( behavior.isDraggable = true behavior.isHideable = true - if (contentHeight >= this.safeScreenHeight || this.minHeight >= this.safeScreenHeight) { + if (preventExpansion) { + behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() + } else { + behavior.maxHeight = (screenHeight - getStatusBarHeight()).toInt() + } + + val targetHeight = this.getTargetHeight() + val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight() + val shouldBeExpanded = targetHeight >= availableHeight + + if (shouldBeExpanded) { behavior.state = BottomSheetBehavior.STATE_EXPANDED this.selectedSnapPoint = 2 } else { @@ -209,18 +227,10 @@ class BottomSheetView( newState: Int, ) { when (newState) { - BottomSheetBehavior.STATE_EXPANDED -> { - selectedSnapPoint = 2 - } - BottomSheetBehavior.STATE_COLLAPSED -> { - selectedSnapPoint = 1 - } - BottomSheetBehavior.STATE_HALF_EXPANDED -> { - selectedSnapPoint = 1 - } - BottomSheetBehavior.STATE_HIDDEN -> { - selectedSnapPoint = 0 - } + BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2 + BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1 + BottomSheetBehavior.STATE_HALF_EXPANDED -> selectedSnapPoint = 1 + BottomSheetBehavior.STATE_HIDDEN -> selectedSnapPoint = 0 } } @@ -231,9 +241,26 @@ class BottomSheetView( }, ) } + this.isOpening = true dialog.show() this.dialog = dialog + + ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets -> + val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime()) + val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) + val behavior = bottomSheet?.let { BottomSheetBehavior.from(it) } + + val wasKeyboardVisible = isKeyboardVisible + isKeyboardVisible = imeVisible + + if (imeVisible && behavior?.state == BottomSheetBehavior.STATE_HALF_EXPANDED) { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } else if (!imeVisible && wasKeyboardVisible) { + updateLayout() + } + insets + } } fun updateLayout() { @@ -246,12 +273,24 @@ class BottomSheetView( val currentState = behavior.state val oldRatio = behavior.halfExpandedRatio - var newRatio = getHalfExpandedRatio(contentHeight) + val newRatio = getHalfExpandedRatio(contentHeight) behavior.halfExpandedRatio = newRatio - if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { + if (preventExpansion) { + behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() + } + + val targetHeight = this.getTargetHeight() + val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight() + val shouldBeExpanded = targetHeight >= availableHeight + + if (isKeyboardVisible) { + if (behavior.state != BottomSheetBehavior.STATE_EXPANDED) { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } else if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) { behavior.state = BottomSheetBehavior.STATE_EXPANDED - } else if (contentHeight < this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { + } else if (!shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED } else if (currentState == BottomSheetBehavior.STATE_HALF_EXPANDED && oldRatio != newRatio) { behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED @@ -279,25 +318,19 @@ class BottomSheetView( private fun getTargetHeight(): Float { val contentHeight = this.getContentHeight() - val height = - if (contentHeight > maxHeight) { - maxHeight - } else if (contentHeight < minHeight) { - minHeight - } else { - contentHeight - } - return height + return when { + contentHeight > maxHeight -> maxHeight + contentHeight < minHeight -> minHeight + else -> contentHeight + } } - private fun clampRatio(ratio: Float): Float { - if (ratio < 0.01) { - return 0.01f - } else if (ratio > 0.99) { - return 0.99f + private fun clampRatio(ratio: Float): Float = + when { + ratio < 0.01 -> 0.01f + ratio > 0.99 -> 0.99f + else -> ratio } - return ratio - } private fun setDraggable(draggable: Boolean) { val dialog = this.dialog ?: return @@ -322,9 +355,7 @@ class BottomSheetView( // View overrides to pass to DialogRootViewGroup instead override fun dispatchProvideStructure(structure: ViewStructure?) { - if (structure == null) { - return - } + if (structure == null) return dialogRootViewGroup.dispatchProvideStructure(structure) } @@ -363,7 +394,6 @@ class BottomSheetView( // https://stackoverflow.com/questions/11862391/getheight-px-or-dpi fun dpToPx(dp: Float): Float { val displayMetrics = context.resources.displayMetrics - val px = dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT) - return px + return dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT) } } diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt index 60b9aac08c..c43d963888 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt @@ -52,6 +52,8 @@ class DialogRootViewGroup( if (ReactFeatureFlags.dispatchPointerEvents) { jSPointerDispatcher = JSPointerDispatcher(this) } + + fitsSystemWindows = false } override fun onSizeChanged( diff --git a/modules/bottom-sheet/android/src/main/res/values/styles.xml b/modules/bottom-sheet/android/src/main/res/values/styles.xml new file mode 100644 index 0000000000..b2a4945da7 --- /dev/null +++ b/modules/bottom-sheet/android/src/main/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx index bbeab8c469..e4e37b1865 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -175,6 +175,7 @@ function BottomSheetNativeComponentInner({ Platform.OS === 'android' && { borderTopLeftRadius: cornerRadius, borderTopRightRadius: cornerRadius, + overflow: 'hidden', }, extraStyles, ]}> diff --git a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt index 97b05b4d73..3f63814bcb 100644 --- a/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt +++ b/modules/expo-background-notification-handler/android/src/main/java/expo/modules/backgroundnotificationhandler/NotificationPrefs.kt @@ -34,12 +34,15 @@ class NotificationPrefs( is Boolean -> { putBoolean(key, value) } + is String -> { putString(key, value) } + is Array<*> -> { putStringSet(key, value.map { it.toString() }.toSet()) } + is Map<*, *> -> { putStringSet(key, value.map { it.toString() }.toSet()) } diff --git a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt index 2aaee3b8ec..d7b9e0e468 100644 --- a/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt +++ b/modules/expo-receive-android-intents/android/src/main/java/xyz/blueskyweb/app/exporeceiveandroidintents/ExpoReceiveAndroidIntentsModule.kt @@ -117,7 +117,7 @@ class ExpoReceiveAndroidIntentsModule : Module() { private fun handleImageIntents( uris: List, - text: String? + text: String?, ) { var allParams = "" @@ -145,7 +145,7 @@ class ExpoReceiveAndroidIntentsModule : Module() { private fun handleVideoIntents( uris: List, - text: String? + text: String?, ) { val uri = uris[0] // If there is no extension for the file, substringAfterLast returns the original string - not diff --git a/package.json b/package.json index 4934691ea9..ccf27c7f54 100644 --- a/package.json +++ b/package.json @@ -202,7 +202,7 @@ "react-native-edge-to-edge": "^1.6.0", "react-native-gesture-handler": "~2.28.0", "react-native-get-random-values": "~1.11.0", - "react-native-keyboard-controller": "1.18.5", + "react-native-keyboard-controller": "^1.20.7", "react-native-pager-view": "6.8.0", "react-native-progress": "bluesky-social/react-native-progress", "react-native-qrcode-styled": "^0.3.3", diff --git a/src/App.native.tsx b/src/App.native.tsx index 1568f7b8ae..5de9f82b7b 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -3,6 +3,7 @@ import '#/view/icons' import React, {useEffect, useState} from 'react' import {GestureHandlerRootView} from 'react-native-gesture-handler' +import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller' import { initialWindowMetrics, SafeAreaProvider, @@ -14,7 +15,6 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import * as Sentry from '@sentry/react-native' -import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController' import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' import {QueryProvider} from '#/lib/react-query' import {s} from '#/lib/styles' diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index b63ab4f1c6..374dc94cd8 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -11,6 +11,7 @@ import { } from 'react-native' import { KeyboardAwareScrollView, + type KeyboardAwareScrollViewRef, useKeyboardHandler, useReanimatedKeyboardAnimation, } from 'react-native-keyboard-controller' @@ -23,7 +24,6 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController' import {ScrollProvider} from '#/lib/ScrollContext' import {logger} from '#/logger' import {useA11y} from '#/state/a11y' @@ -209,10 +209,9 @@ export const ScrollableInner = React.forwardRef( const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const insets = useSafeAreaInsets() - useEnableKeyboardController(IS_IOS) - const [keyboardHeight, setKeyboardHeight] = React.useState(0) + // note: iOS-only. keyboard-controller doesn't seem to work inside the sheets on Android useKeyboardHandler( { onEnd: e => { @@ -231,7 +230,6 @@ export const ScrollableInner = React.forwardRef( } paddingBottom = Math.max(paddingBottom, tokens.space._2xl) } else { - paddingBottom += keyboardHeight if (nativeSnapPoint === BottomSheetSnapPoint.Full) { paddingBottom += insets.top } @@ -259,7 +257,7 @@ export const ScrollableInner = React.forwardRef( {paddingBottom}, contentContainerStyle, ]} - ref={ref} + ref={ref as React.Ref} showsVerticalScrollIndicator={IS_ANDROID ? false : undefined} {...props} bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} @@ -289,8 +287,6 @@ export const InnerFlatList = React.forwardRef< const insets = useSafeAreaInsets() const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() - useEnableKeyboardController(IS_IOS) - const onScroll = (e: ScrollEvent) => { 'worklet' if (!IS_ANDROID) { diff --git a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx index 9cad63e9a8..55ad79e504 100644 --- a/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx +++ b/src/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerNative.tsx @@ -58,7 +58,7 @@ export function VideoEmbedInnerNative({ { setIsActive(e.nativeEvent.isActive) @@ -67,7 +67,9 @@ export function VideoEmbedInnerNative({ setIsLoading(e.nativeEvent.isLoading) }} onMutedChange={e => { - setMuted(e.nativeEvent.isMuted) + if (!isGif) { + setMuted(e.nativeEvent.isMuted) + } }} onStatusChange={e => { setStatus(e.nativeEvent.status) diff --git a/src/components/verification/VerifierDialog.tsx b/src/components/verification/VerifierDialog.tsx index e7548aa92b..39fb456453 100644 --- a/src/components/verification/VerifierDialog.tsx +++ b/src/components/verification/VerifierDialog.tsx @@ -28,7 +28,7 @@ export function VerifierDialog({ verificationState: FullVerificationState }) { return ( - + { @@ -138,7 +137,6 @@ function Inner({