add fullHeight prop, fix Android bottom sheet behavior, keyboard handling

Native module:
- Add `fullHeight` prop to BottomSheetView on Android and iOS
- iOS: fullHeight sets detent to .large, skips content observation
- Android: fullHeight uses isFitToContents=false with expandedOffset=statusBarHeight

Android bottom sheet behavior overhaul:
- Switch normal sheets to isFitToContents=false with expandedOffset, enabling
  proper 3-state snapping (half-expanded ↔ expanded ↔ hidden). Previously
  isFitToContents=true prevented settling at half-expanded during gestures,
  making it impossible to swipe back down from expanded.
- preventExpansion sheets keep isFitToContents=true with maxHeight cap, plus
  a state callback safety net that bounces EXPANDED→HALF_EXPANDED. This
  catches an edge case where rapid content resizes during opening can invert
  the expanded/half-expanded offsets, causing the sheet to dismiss.
- Add requestLayout() after maxHeight changes in updateLayout() so the
  FrameLayout actually re-measures (fixes keyboard padding not resizing sheet)
- Set backgroundTint=transparent in the theme to remove Material3 surface
  tint that was visible in gaps between sheet and screen edge

Keyboard handling:
- Remove native keyboard expansion logic (insets listener, isKeyboardVisible
  tracking, manual STATE_EXPANDED on keyboard show)
- Replace with JS-side keyboard padding: ScrollableInner listens for RN
  Keyboard events and adds bottom padding equal to keyboard height
- Generalize useOnKeyboardDidShow → useOnKeyboard(eventName, cb)

Callers:
- Replace minHeight: screenHeight hack with fullHeight: true in EditProfile,
  ChangeHandle, AddAppPassword, ImageAltText, GifAltText, LanguageSelect,
  FollowDialog, GifSelect, StarterPack, Select, NewChat, ShareViaChat,
  DraftsList, WizardEditList, ListAddRemoveUsers, CreateOrEditList
- Remove unused useWindowDimensions imports
- ServerInput: simplify to just preventExpansion (was platform-specific)
- ImageAltTextDialog: remove old {height: 300} keyboard spacer hack

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-03-01 22:40:44 +02:00
parent e46977ddca
commit 1e8ce36184
26 changed files with 148 additions and 130 deletions
@@ -25,6 +25,10 @@ class BottomSheetModule : Module() {
view.dismiss() view.dismiss()
} }
Prop("fullHeight") { view: BottomSheetView, prop: Boolean ->
view.fullHeight = prop
}
Prop("disableDrag") { view: BottomSheetView, prop: Boolean -> Prop("disableDrag") { view: BottomSheetView, prop: Boolean ->
view.disableDrag = prop view.disableDrag = prop
} }
@@ -8,8 +8,6 @@ import android.view.ViewStructure
import android.view.Window import android.view.Window
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReactContext
@@ -33,7 +31,6 @@ class BottomSheetView(
private lateinit var dialogRootViewGroup: DialogRootViewGroup private lateinit var dialogRootViewGroup: DialogRootViewGroup
private var eventDispatcher: EventDispatcher? = null private var eventDispatcher: EventDispatcher? = null
private var isKeyboardVisible: Boolean = false
// Native content height observation (eliminates JS bridge round-trip) // Native content height observation (eliminates JS bridge round-trip)
private var contentLayoutListener: View.OnLayoutChangeListener? = null private var contentLayoutListener: View.OnLayoutChangeListener? = null
@@ -71,6 +68,8 @@ class BottomSheetView(
this.dialog?.setCancelable(!value) this.dialog?.setCancelable(!value)
} }
var fullHeight = false
var preventExpansion = false var preventExpansion = false
var minHeight = 0f var minHeight = 0f
@@ -202,28 +201,36 @@ class BottomSheetView(
val behavior = BottomSheetBehavior.from(it) val behavior = BottomSheetBehavior.from(it)
behavior.state = BottomSheetBehavior.STATE_HIDDEN behavior.state = BottomSheetBehavior.STATE_HIDDEN
behavior.isFitToContents = true
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.skipCollapsed = true behavior.skipCollapsed = true
behavior.isDraggable = true behavior.isDraggable = true
behavior.isHideable = true behavior.isHideable = true
if (fullHeight) {
if (preventExpansion) { behavior.isFitToContents = false
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() behavior.expandedOffset = getStatusBarHeight()
} 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 behavior.state = BottomSheetBehavior.STATE_EXPANDED
this.selectedSnapPoint = 2 this.selectedSnapPoint = 2
} else { } else if (preventExpansion) {
behavior.isFitToContents = true
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
this.selectedSnapPoint = 1 this.selectedSnapPoint = 1
} else {
behavior.isFitToContents = false
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.expandedOffset = getStatusBarHeight()
val targetHeight = this.getTargetHeight()
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
val shouldBeExpanded = targetHeight >= availableHeight
if (shouldBeExpanded) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
this.selectedSnapPoint = 2
} else {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
this.selectedSnapPoint = 1
}
} }
behavior.addBottomSheetCallback( behavior.addBottomSheetCallback(
@@ -232,6 +239,10 @@ class BottomSheetView(
bottomSheet: View, bottomSheet: View,
newState: Int, newState: Int,
) { ) {
if (newState == BottomSheetBehavior.STATE_EXPANDED && preventExpansion) {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
return
}
when (newState) { when (newState) {
BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2 BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2
BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1 BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1
@@ -258,34 +269,14 @@ class BottomSheetView(
this.isOpening = true this.isOpening = true
dialog.show() dialog.show()
this.dialog = dialog this.dialog = dialog
this.startObservingContentHeight() if (!fullHeight) {
this.startObservingContentHeight()
ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets ->
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
val behavior = bottomSheet?.let { BottomSheetBehavior.from(it) }
val wasKeyboardVisible = isKeyboardVisible
isKeyboardVisible = imeVisible
if (imeVisible && behavior != null && bottomSheet != null && behavior.state != BottomSheetBehavior.STATE_EXPANDED && behavior.state != BottomSheetBehavior.STATE_HIDDEN) {
if (preventExpansion) {
behavior.maxHeight = (screenHeight - getStatusBarHeight()).toInt()
bottomSheet.requestLayout()
bottomSheet.post {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
} else {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
} else if (!imeVisible && wasKeyboardVisible) {
updateLayout()
}
insets
} }
} }
fun updateLayout() { fun updateLayout() {
if (fullHeight) return
val dialog = this.dialog ?: return val dialog = this.dialog ?: return
val contentHeight = this.getContentHeight() val contentHeight = this.getContentHeight()
@@ -300,6 +291,7 @@ class BottomSheetView(
if (preventExpansion) { if (preventExpansion) {
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
it.requestLayout()
} }
val targetHeight = this.getTargetHeight() val targetHeight = this.getTargetHeight()
@@ -315,11 +307,7 @@ class BottomSheetView(
return return
} }
if (isKeyboardVisible) { if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) {
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 behavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (!shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { } else if (!shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
@@ -330,7 +318,7 @@ class BottomSheetView(
} }
fun dismiss() { fun dismiss() {
this.dialog?.dismiss() this.dialog?.cancel()
} }
// Observe each direct child of innerView via OnLayoutChangeListener so that // Observe each direct child of innerView via OnLayoutChangeListener so that
@@ -16,5 +16,6 @@
<item name="paddingLeftSystemWindowInsets">true</item> <item name="paddingLeftSystemWindowInsets">true</item>
<item name="paddingRightSystemWindowInsets">true</item> <item name="paddingRightSystemWindowInsets">true</item>
<item name="paddingTopSystemWindowInsets">false</item> <item name="paddingTopSystemWindowInsets">false</item>
<item name="backgroundTint">@android:color/transparent</item>
</style> </style>
</resources> </resources>
@@ -19,6 +19,10 @@ public class BottomSheetModule: Module {
view.dismiss() view.dismiss()
} }
Prop("fullHeight") { (view: SheetView, prop: Bool) in
view.fullHeight = prop
}
Prop("cornerRadius") { (view: SheetView, prop: Float) in Prop("cornerRadius") { (view: SheetView, prop: Float) in
view.cornerRadius = CGFloat(prop) view.cornerRadius = CGFloat(prop)
} }
+5 -2
View File
@@ -26,6 +26,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
} }
// React view props // React view props
var fullHeight = false
var preventDismiss = false var preventDismiss = false
var preventExpansion = false var preventExpansion = false
var cornerRadius: CGFloat? var cornerRadius: CGFloat?
@@ -132,7 +133,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
} }
let sheetVc = SheetViewController() let sheetVc = SheetViewController()
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion) sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion, fullHeight: self.fullHeight)
if let sheet = sheetVc.sheetPresentationController { if let sheet = sheetVc.sheetPresentationController {
sheet.delegate = self sheet.delegate = self
sheet.preferredCornerRadius = self.cornerRadius sheet.preferredCornerRadius = self.cornerRadius
@@ -151,7 +152,9 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
self.sheetVc = sheetVc self.sheetVc = sheetVc
self.isOpening = true self.isOpening = true
self.startObservingContentHeight() if !self.fullHeight {
self.startObservingContentHeight()
}
rvc.present(sheetVc, animated: true) { [weak self] in rvc.present(sheetVc, animated: true) { [weak self] in
self?.isOpening = false self?.isOpening = false
@@ -20,13 +20,19 @@ class SheetViewController: UIViewController {
} }
} }
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) { func setDetents(contentHeight: CGFloat, preventExpansion: Bool, fullHeight: Bool = false) {
guard let sheet = self.sheetPresentationController, guard let sheet = self.sheetPresentationController,
let screenHeight = Util.getScreenHeight() let screenHeight = Util.getScreenHeight()
else { else {
return return
} }
if fullHeight {
sheet.detents = [.large()]
sheet.selectedDetentIdentifier = .large
return
}
// On iOS 26, the floaty sheet presentation adds the device bottom safe area // On iOS 26, the floaty sheet presentation adds the device bottom safe area
// on top of the custom detent value, creating visible padding inside the pill. // on top of the custom detent value, creating visible padding inside the pill.
// Subtract it so the pill height matches our actual content. // Subtract it so the pill height matches our actual content.
@@ -26,6 +26,7 @@ export interface BottomSheetViewProps {
disableDrag?: boolean disableDrag?: boolean
sourceViewTag?: number sourceViewTag?: number
fullHeight?: boolean
minHeight?: number minHeight?: number
maxHeight?: number maxHeight?: number
+37 -19
View File
@@ -1,5 +1,14 @@
import React, {useImperativeHandle} from 'react'
import { import {
forwardRef,
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import {
Keyboard,
type KeyboardEventListener,
type LayoutChangeEvent, type LayoutChangeEvent,
type NativeScrollEvent, type NativeScrollEvent,
type NativeSyntheticEvent, type NativeSyntheticEvent,
@@ -34,6 +43,7 @@ import {
type DialogOuterProps, type DialogOuterProps,
} from '#/components/Dialog/types' } from '#/components/Dialog/types'
import {createInput} from '#/components/forms/TextField' import {createInput} from '#/components/forms/TextField'
import {useOnKeyboard} from '#/components/hooks/useOnKeyboard'
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS} from '#/env' import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS} from '#/env'
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet' import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
import { import {
@@ -58,21 +68,21 @@ export function Outer({
}: React.PropsWithChildren<DialogOuterProps>) { }: React.PropsWithChildren<DialogOuterProps>) {
const themeName = useThemeName() const themeName = useThemeName()
const t = useTheme(themeName) const t = useTheme(themeName)
const ref = React.useRef<BottomSheetNativeComponent>(null) const ref = useRef<BottomSheetNativeComponent>(null)
const closeCallbacks = React.useRef<(() => void)[]>([]) const closeCallbacks = useRef<(() => void)[]>([])
const {setDialogIsOpen, setFullyExpandedCount} = const {setDialogIsOpen, setFullyExpandedCount} =
useDialogStateControlContext() useDialogStateControlContext()
const prevSnapPoint = React.useRef<BottomSheetSnapPoint>( const prevSnapPoint = useRef<BottomSheetSnapPoint>(
BottomSheetSnapPoint.Hidden, BottomSheetSnapPoint.Hidden,
) )
const [disableDrag, setDisableDrag] = React.useState(false) const [disableDrag, setDisableDrag] = useState(false)
const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>( const [snapPoint, setSnapPoint] = useState<BottomSheetSnapPoint>(
BottomSheetSnapPoint.Partial, BottomSheetSnapPoint.Partial,
) )
const callQueuedCallbacks = React.useCallback(() => { const callQueuedCallbacks = useCallback(() => {
for (const cb of closeCallbacks.current) { for (const cb of closeCallbacks.current) {
try { try {
cb() cb()
@@ -84,7 +94,7 @@ export function Outer({
closeCallbacks.current = [] closeCallbacks.current = []
}, []) }, [])
const open = React.useCallback<DialogControlProps['open']>(() => { const open = useCallback<DialogControlProps['open']>(() => {
// Run any leftover callbacks that might have been queued up before calling `.open()` // Run any leftover callbacks that might have been queued up before calling `.open()`
callQueuedCallbacks() callQueuedCallbacks()
setDialogIsOpen(control.id, true) setDialogIsOpen(control.id, true)
@@ -92,7 +102,7 @@ export function Outer({
}, [setDialogIsOpen, control.id, callQueuedCallbacks]) }, [setDialogIsOpen, control.id, callQueuedCallbacks])
// This is the function that we call when we want to dismiss the dialog. // This is the function that we call when we want to dismiss the dialog.
const close = React.useCallback<DialogControlProps['close']>(cb => { const close = useCallback<DialogControlProps['close']>(cb => {
if (typeof cb === 'function') { if (typeof cb === 'function') {
closeCallbacks.current.push(cb) closeCallbacks.current.push(cb)
} }
@@ -101,7 +111,7 @@ export function Outer({
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to // This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
// happen before we run this. It is passed to the `BottomSheet` component. // happen before we run this. It is passed to the `BottomSheet` component.
const onCloseAnimationComplete = React.useCallback(() => { const onCloseAnimationComplete = useCallback(() => {
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this // This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
// tells us that we need to toggle the accessibility overlay setting // tells us that we need to toggle the accessibility overlay setting
setDialogIsOpen(control.id, false) setDialogIsOpen(control.id, false)
@@ -147,7 +157,7 @@ export function Outer({
[open, close], [open, close],
) )
const context = React.useMemo( const context = useMemo(
() => ({ () => ({
close, close,
isNativeDialog: true, isNativeDialog: true,
@@ -201,7 +211,7 @@ export function Inner({children, style, header}: DialogInnerProps) {
) )
} }
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( export const ScrollableInner = forwardRef<ScrollView, DialogInnerProps>(
function ScrollableInner( function ScrollableInner(
{children, contentContainerStyle, header, ...props}, {children, contentContainerStyle, header, ...props},
ref, ref,
@@ -209,6 +219,15 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const [keyboardHeight, setKeyboardHeight] = useState(() =>
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
)
const keyboardEventHandler = useCallback<KeyboardEventListener>(e => {
setKeyboardHeight(e.endCoordinates.height)
}, [])
useOnKeyboard('keyboardDidShow', keyboardEventHandler)
useOnKeyboard('keyboardDidHide', keyboardEventHandler)
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => { const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
if (!IS_ANDROID) { if (!IS_ANDROID) {
@@ -230,11 +249,10 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
platform({ platform({
ios: a.pb_2xl, ios: a.pb_2xl,
android: { android: {
paddingBottom: insets.bottom + tokens.space.xl, paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
}, },
}), }),
contentContainerStyle, contentContainerStyle,
a.debug,
]} ]}
ref={ref} ref={ref}
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined} showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
@@ -257,13 +275,13 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
// dialogs that use this that actually scroll -sfn // dialogs that use this that actually scroll -sfn
stickyHeaderIndices={ios(header ? [0] : undefined)}> stickyHeaderIndices={ios(header ? [0] : undefined)}>
{header} {header}
<View style={a.debug}>{children}</View> {children}
</ScrollView> </ScrollView>
) )
}, },
) )
export const InnerFlatList = React.forwardRef< export const InnerFlatList = forwardRef<
ListMethods, ListMethods,
ListProps<any> & { ListProps<any> & {
webInnerStyle?: StyleProp<ViewStyle> webInnerStyle?: StyleProp<ViewStyle>
@@ -330,7 +348,7 @@ export function FlatListFooter({
onLayout?: (event: LayoutChangeEvent) => void onLayout?: (event: LayoutChangeEvent) => void
}) { }) {
const t = useTheme() const t = useTheme()
const {top, bottom} = useSafeAreaInsets() const {bottom} = useSafeAreaInsets()
const {height} = useReanimatedKeyboardAnimation() const {height} = useReanimatedKeyboardAnimation()
const animatedStyle = useAnimatedStyle(() => { const animatedStyle = useAnimatedStyle(() => {
@@ -355,8 +373,8 @@ export function FlatListFooter({
a.pt_md, a.pt_md,
{ {
paddingBottom: platform({ paddingBottom: platform({
ios: tokens.space.md + bottom + (IS_LIQUID_GLASS ? top : 0), ios: tokens.space.sm + bottom,
android: tokens.space.md + bottom + top, android: tokens.space.md + bottom,
}), }),
}, },
// TODO: had to admit defeat here, but we should // TODO: had to admit defeat here, but we should
+3 -10
View File
@@ -1,10 +1,5 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react' import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import { import {TextInput, View, type ViewToken} from 'react-native'
TextInput,
useWindowDimensions,
View,
type ViewToken,
} from 'react-native'
import {type ModerationOpts} from '@atproto/api' import {type ModerationOpts} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -72,7 +67,6 @@ export function FollowDialog({
const {_} = useLingui() const {_} = useLingui()
const control = Dialog.useDialogControl() const control = Dialog.useDialogControl()
const {gtPhone} = useBreakpoints() const {gtPhone} = useBreakpoints()
const {height: minHeight} = useWindowDimensions()
return ( return (
<> <>
@@ -89,7 +83,7 @@ export function FollowDialog({
</ButtonText> </ButtonText>
{showArrow && <ButtonIcon icon={ArrowRightIcon} />} {showArrow && <ButtonIcon icon={ArrowRightIcon} />}
</Button> </Button>
<Dialog.Outer control={control} nativeOptions={{minHeight}}> <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<DialogInner guide={guide} /> <DialogInner guide={guide} />
</Dialog.Outer> </Dialog.Outer>
@@ -105,9 +99,8 @@ export function FollowDialogWithoutGuide({
}: { }: {
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
}) { }) {
const {height: minHeight} = useWindowDimensions()
return ( return (
<Dialog.Outer control={control} nativeOptions={{minHeight}}> <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<DialogInner /> <DialogInner />
</Dialog.Outer> </Dialog.Outer>
+1 -1
View File
@@ -151,7 +151,7 @@ export function Content<T>({
}, [items, context.value, valueExtractor, setValue]) }, [items, context.value, valueExtractor, setValue])
return ( return (
<Dialog.Outer control={control}> <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
<ContentInner <ContentInner
control={control} control={control}
items={items} items={items}
@@ -78,7 +78,10 @@ export function WizardEditListDialog({
) )
return ( return (
<Dialog.Outer control={control} testID="newChatDialog"> <Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.InnerFlatList <Dialog.InnerFlatList
ref={listRef} ref={listRef}
+1
View File
@@ -68,6 +68,7 @@ export function GifSelectDialog({
bottomInset: 0, bottomInset: 0,
// use system corner radius on iOS // use system corner radius on iOS
...ios({cornerRadius: undefined}), ...ios({cornerRadius: undefined}),
fullHeight: true,
}}> }}>
<Dialog.Handle /> <Dialog.Handle />
<ErrorBoundary renderError={renderErrorBoundary}> <ErrorBoundary renderError={renderErrorBoundary}>
@@ -1,6 +1,5 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {useWindowDimensions, View} from 'react-native' import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -17,7 +16,7 @@ import {SearchInput} from '#/components/forms/SearchInput'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' import {IS_NATIVE, IS_WEB} from '#/env'
type FlatListItem = type FlatListItem =
| { | {
@@ -51,9 +50,6 @@ export function LanguageSelectDialog({
onSelectLanguages: (languages: string[]) => void onSelectLanguages: (languages: string[]) => void
maxLanguages?: number maxLanguages?: number
}) { }) {
const {height} = useWindowDimensions()
const insets = useSafeAreaInsets()
const renderErrorBoundary = useCallback( const renderErrorBoundary = useCallback(
(error: any) => <DialogError details={String(error)} />, (error: any) => <DialogError details={String(error)} />,
[], [],
@@ -63,7 +59,7 @@ export function LanguageSelectDialog({
<Dialog.Outer <Dialog.Outer
control={control} control={control}
nativeOptions={{ nativeOptions={{
minHeight: IS_LIQUID_GLASS ? height : height - insets.top, fullHeight: true,
}}> }}>
<Dialog.Handle /> <Dialog.Handle />
<ErrorBoundary renderError={renderErrorBoundary}> <ErrorBoundary renderError={renderErrorBoundary}>
+2 -6
View File
@@ -1,5 +1,5 @@
import {useCallback, useImperativeHandle, useRef, useState} from 'react' import {useCallback, useImperativeHandle, useRef, useState} from 'react'
import {useWindowDimensions, View} from 'react-native' import {View} from 'react-native'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
@@ -28,7 +28,6 @@ export function ServerInputDialog({
onSelect: (url: string) => void onSelect: (url: string) => void
}) { }) {
const ax = useAnalytics() const ax = useAnalytics()
const {height} = useWindowDimensions()
const formRef = useRef<DialogInnerRef>(null) const formRef = useRef<DialogInnerRef>(null)
// persist these options between dialog open/close // persist these options between dialog open/close
@@ -53,10 +52,7 @@ export function ServerInputDialog({
<Dialog.Outer <Dialog.Outer
control={control} control={control}
onClose={onClose} onClose={onClose}
nativeOptions={platform({ nativeOptions={{preventExpansion: true}}>
android: {minHeight: height / 2},
ios: {preventExpansion: true},
})}>
<Dialog.Handle /> <Dialog.Handle />
<DialogInner <DialogInner
formRef={formRef} formRef={formRef}
+1 -1
View File
@@ -75,7 +75,7 @@ export function StarterPackDialog({
}) })
return ( return (
<Dialog.Outer control={control}> <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<StarterPackList <StarterPackList
onStartWizard={wrappedNavToWizard} onStartWizard={wrappedNavToWizard}
@@ -1,5 +1,5 @@
import {useCallback, useEffect, useMemo, useState} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import {useWindowDimensions, View} from 'react-native' import {View} from 'react-native'
import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -53,7 +53,6 @@ export function CreateOrEditListDialog({
const {_} = useLingui() const {_} = useLingui()
const cancelControl = Dialog.useDialogControl() const cancelControl = Dialog.useDialogControl()
const [dirty, setDirty] = useState(false) const [dirty, setDirty] = useState(false)
const {height} = useWindowDimensions()
// 'You might lose unsaved changes' warning // 'You might lose unsaved changes' warning
useEffect(() => { useEffect(() => {
@@ -82,7 +81,7 @@ export function CreateOrEditListDialog({
control={control} control={control}
nativeOptions={{ nativeOptions={{
preventDismiss: dirty, preventDismiss: dirty,
minHeight: height, fullHeight: true,
}} }}
testID="createOrEditListDialog"> testID="createOrEditListDialog">
<DialogInner <DialogInner
@@ -39,7 +39,10 @@ export function ListAddRemoveUsersDialog({
) => void | undefined ) => void | undefined
}) { }) {
return ( return (
<Dialog.Outer control={control} testID="listAddRemoveUsersDialog"> <Dialog.Outer
control={control}
testID="listAddRemoveUsersDialog"
nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<DialogInner list={list} onChange={onChange} /> <DialogInner list={list} onChange={onChange} />
</Dialog.Outer> </Dialog.Outer>
+4 -1
View File
@@ -70,7 +70,10 @@ export function NewChat({
accessibilityHint="" accessibilityHint=""
/> />
<Dialog.Outer control={control} testID="newChatDialog"> <Dialog.Outer
control={control}
testID="newChatDialog"
nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<SearchablePeopleList <SearchablePeopleList
title={_(msg`Start a new chat`)} title={_(msg`Start a new chat`)}
@@ -17,7 +17,10 @@ export function SendViaChatDialog({
onSelectChat: (chatId: string) => void onSelectChat: (chatId: string) => void
}) { }) {
return ( return (
<Dialog.Outer control={control} testID="sendViaChatChatDialog"> <Dialog.Outer
control={control}
testID="sendViaChatChatDialog"
nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} /> <SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
</Dialog.Outer> </Dialog.Outer>
+13 -6
View File
@@ -1,12 +1,19 @@
import React from 'react' import {useEffect} from 'react'
import {Keyboard} from 'react-native' import {
Keyboard,
type KeyboardEventListener,
type KeyboardEventName,
} from 'react-native'
export function useOnKeyboardDidShow(cb: () => unknown) { export function useOnKeyboard(
React.useEffect(() => { eventName: KeyboardEventName,
const subscription = Keyboard.addListener('keyboardDidShow', cb) cb: KeyboardEventListener,
) {
useEffect(() => {
const subscription = Keyboard.addListener(eventName, cb)
return () => { return () => {
subscription.remove() subscription.remove()
} }
}, [cb]) }, [eventName, cb])
} }
@@ -1,5 +1,5 @@
import {useCallback, useEffect, useState} from 'react' import {useCallback, useEffect, useState} from 'react'
import {useWindowDimensions, View} from 'react-native' import {View} from 'react-native'
import {type AppBskyActorDefs} from '@atproto/api' import {type AppBskyActorDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -41,7 +41,6 @@ export function EditProfileDialog({
const {_} = useLingui() const {_} = useLingui()
const cancelControl = Dialog.useDialogControl() const cancelControl = Dialog.useDialogControl()
const [dirty, setDirty] = useState(false) const [dirty, setDirty] = useState(false)
const {height} = useWindowDimensions()
const onPressCancel = useCallback(() => { const onPressCancel = useCallback(() => {
if (dirty) { if (dirty) {
@@ -56,7 +55,7 @@ export function EditProfileDialog({
control={control} control={control}
nativeOptions={{ nativeOptions={{
preventDismiss: dirty, preventDismiss: dirty,
minHeight: height, fullHeight: true,
}} }}
webOptions={{ webOptions={{
onBackgroundPress: () => { onBackgroundPress: () => {
@@ -1,5 +1,5 @@
import {useEffect, useMemo, useState} from 'react' import {useEffect, useMemo, useState} from 'react'
import {useWindowDimensions, View} from 'react-native' import {View} from 'react-native'
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeOut, FadeOut,
@@ -34,9 +34,8 @@ export function AddAppPasswordDialog({
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
passwords: string[] passwords: string[]
}) { }) {
const {height} = useWindowDimensions()
return ( return (
<Dialog.Outer control={control} nativeOptions={{minHeight: height}}> <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<CreateDialogInner passwords={passwords} /> <CreateDialogInner passwords={passwords} />
</Dialog.Outer> </Dialog.Outer>
@@ -1,5 +1,5 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {useWindowDimensions, View} from 'react-native' import {View} from 'react-native'
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeOut, FadeOut,
@@ -53,10 +53,8 @@ export function ChangeHandleDialog({
}: { }: {
control: Dialog.DialogControlProps control: Dialog.DialogControlProps
}) { }) {
const {height} = useWindowDimensions()
return ( return (
<Dialog.Outer control={control} nativeOptions={{minHeight: height}}> <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
<ChangeHandleDialogInner /> <ChangeHandleDialogInner />
</Dialog.Outer> </Dialog.Outer>
) )
+2 -3
View File
@@ -1,5 +1,5 @@
import {useState} from 'react' import {useState} from 'react'
import {TouchableOpacity, useWindowDimensions, View} from 'react-native' import {TouchableOpacity, View} from 'react-native'
import {msg} from '@lingui/core/macro' import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro' import {Plural, Trans} from '@lingui/react/macro'
@@ -69,7 +69,6 @@ export function GifAltTextDialogLoaded({
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const [altTextDraft, setAltTextDraft] = useState(altText || vendorAltText) const [altTextDraft, setAltTextDraft] = useState(altText || vendorAltText)
const {height: minHeight} = useWindowDimensions()
return ( return (
<> <>
<TouchableOpacity <TouchableOpacity
@@ -110,7 +109,7 @@ export function GifAltTextDialogLoaded({
onClose={() => { onClose={() => {
onSubmit(altTextDraft) onSubmit(altTextDraft)
}} }}
nativeOptions={{minHeight}}> nativeOptions={{fullHeight: true}}>
<Dialog.Handle /> <Dialog.Handle />
<AltTextInner <AltTextInner
vendorAltText={vendorAltText} vendorAltText={vendorAltText}
@@ -167,7 +167,7 @@ export function DraftsListDialog({
) )
return ( return (
<Dialog.Outer control={control}> <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
{/* We really really need to figure out a nice, consistent API for doing a header cross-platform -sfn */} {/* We really really need to figure out a nice, consistent API for doing a header cross-platform -sfn */}
{IS_NATIVE && header} {IS_NATIVE && header}
<Dialog.InnerFlatList <Dialog.InnerFlatList
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro' import {Plural, Trans} from '@lingui/react/macro'
import {MAX_ALT_TEXT} from '#/lib/constants' import {MAX_ALT_TEXT} from '#/lib/constants'
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
import {enforceLen} from '#/lib/strings/helpers' import {enforceLen} from '#/lib/strings/helpers'
import {type ComposerImage} from '#/state/gallery' import {type ComposerImage} from '#/state/gallery'
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper' import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
@@ -17,7 +16,7 @@ import {type DialogControlProps} from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {IS_ANDROID, IS_LIQUID_GLASS, IS_WEB} from '#/env' import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
type Props = { type Props = {
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
@@ -32,7 +31,6 @@ export const ImageAltTextDialog = ({
onChange, onChange,
sourceViewTag, sourceViewTag,
}: Props): React.ReactNode => { }: Props): React.ReactNode => {
const {height: minHeight} = useWindowDimensions()
const [altText, setAltText] = useState(image.alt) const [altText, setAltText] = useState(image.alt)
return ( return (
@@ -44,7 +42,7 @@ export const ImageAltTextDialog = ({
alt: enforceLen(altText, MAX_ALT_TEXT, true), alt: enforceLen(altText, MAX_ALT_TEXT, true),
}) })
}} }}
nativeOptions={{minHeight, sourceViewTag}}> nativeOptions={{fullHeight: true, sourceViewTag}}>
<Dialog.Handle /> <Dialog.Handle />
<ImageAltTextInner <ImageAltTextInner
control={control} control={control}
@@ -71,8 +69,6 @@ const ImageAltTextInner = ({
const t = useTheme() const t = useTheme()
const {width: screenWidth} = useWindowDimensions() const {width: screenWidth} = useWindowDimensions()
const [isKeyboardVisible] = useIsKeyboardVisible()
const imageStyle = useMemo<ImageStyle>(() => { const imageStyle = useMemo<ImageStyle>(() => {
const maxWidth = IS_WEB const maxWidth = IS_WEB
? 450 ? 450
@@ -179,8 +175,6 @@ const ImageAltTextInner = ({
</Button> </Button>
</AltTextCounterWrapper> </AltTextCounterWrapper>
</View> </View>
{/* Maybe fix this later -h */}
{IS_ANDROID && isKeyboardVisible ? <View style={{height: 300}} /> : null}
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
) )
} }