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