Fix bottom sheet content width on Android tablets (native-owned canvas sizing) (#11396)
This commit is contained in:
@@ -61,7 +61,8 @@ The component uses a class-based approach to expose imperative methods (`present
|
||||
- Preserves status/nav bar appearance from host activity
|
||||
- **DialogRootViewGroup.kt**: Custom ViewGroup acting as RootView for the dialog
|
||||
- Forwards touch events to React Native event system
|
||||
- Updates shadow node size to match window dimensions
|
||||
- Reports its measured width to `BottomSheetView` so the content canvas can follow it
|
||||
- Also carries the legacy `UIManagerModule.updateNodeSize()` shadow node sizing, which only runs on the old architecture
|
||||
- Based on React Native's ReactModalHostView pattern
|
||||
- **SheetManager.kt**: Singleton for tracking sheets (same pattern as iOS)
|
||||
|
||||
@@ -74,6 +75,24 @@ Both platforms detect content height changes natively without JS bridge round-tr
|
||||
|
||||
This eliminates layout jank when content changes (e.g., keyboard appearance, dynamic content loading).
|
||||
|
||||
### Content Canvas Sizing
|
||||
|
||||
The "canvas" is the size the sheet content is laid out on by Yoga. **On Android the native side owns it**; on iOS it is still sized from JS.
|
||||
|
||||
- **Android**: JS renders unsized `flex: 1` content and `BottomSheetView` pushes the canvas size into the Fabric shadow tree through `ExpoView`'s `setViewSize` state channel (`shadowNodeProxy.setViewSize()`). Only native knows the real sheet frame - Material caps the frame at 640dp on tablets and centers it, and it changes on rotation.
|
||||
- **iOS**: `BottomSheetNativeComponent` sets `height: screenHeight - insets.top` and `width: '100%'` on the native view. Moving iOS onto the same state channel is deferred: it needs on-device iteration on iOS 26 sheet geometry (large-detent and floating-card metrics, where the visible sheet is shorter than the window minus the top inset).
|
||||
|
||||
How the Android path works:
|
||||
|
||||
- The JS style on the native view **must not set `width` or `height`** on Android. `ExpoViewComponentDescriptor::adopt()` only applies the state size on an axis where the style leaves that dimension undefined, so a style dimension would silently win.
|
||||
- The two axes come from different places, and the distinction is load-bearing:
|
||||
- **Width** is authoritatively the dialog container's measured width, reported through `DialogRootViewGroup`'s size-change listener - that is the real sheet width, with the horizontal window insets and Material's 640dp cap already applied. It is seeded from `min(window width, material_bottom_sheet_max_width)` on the first `onLayout` so content has something to lay out in before the dialog exists.
|
||||
- **Height** is always computed natively as `screenHeight - statusBarHeight` (matching the behavior's `expandedOffset`) - the whole expanded frame, **never** the dialog's measured height. The canvas has to be room for the content to grow *into*, because the content's height is what drives the snap points. Sizing it from the dialog's own height is circular: `BottomSheetBehavior` measures the container against the sheet, so the canvas collapses onto the content height and the content is then pinned - extra `ScrollView` padding (the Android keyboard path) or a longer list becomes scroll extent instead of a height change, `OnLayoutChangeListener` never fires, and the sheet stops responding to its content.
|
||||
- Seeding runs once per open cycle - re-seeding would fight the width the dialog reported and the two would push each other back and forth.
|
||||
- Because the content measures 0x0 until that first state commit lands, `present()` bails out early when the content height is still zero. The commit resizes the native view, which re-fires `onLayout`, which re-enters `present()` - so presentation self-retries rather than needing an explicit callback. Full-height sheets skip the check, since they don't need a content measurement.
|
||||
- Rotation is handled by the container push: the RN activity handles configuration changes itself, so the view is never recreated. `screenHeight` is read per access so the computed height follows the rotation, and the container reports the new width (plus a deferred `updateLayout()` to reposition the sheet).
|
||||
- On the **old architecture** there is no state channel (`stateWrapper` is null, so `setViewSize` no-ops) and Android falls back to `DialogRootViewGroup`'s legacy `UIManagerModule.updateNodeSize()` path. The `present()` gate is skipped there for the same reason - nothing would ever resize the view.
|
||||
|
||||
## Props
|
||||
|
||||
```typescript
|
||||
@@ -213,6 +232,10 @@ BottomSheetNativeComponent.dismissAll()
|
||||
|
||||
4. **Layout Updates During Gestures**: Content height changes are deferred during drag gestures to prevent fighting the user's input.
|
||||
|
||||
5. **Tablet Width**: Material caps the sheet frame at 640dp (`material_bottom_sheet_max_width`, the `android:maxWidth` on `Widget.MaterialComponents.BottomSheet`) and centers it horizontally, so on tablets the sheet is narrower than the screen. `BottomSheetView` reads that cap from resources when seeding the canvas width, and the dialog container's measured width then corrects it - see [Content Canvas Sizing](#content-canvas-sizing).
|
||||
|
||||
6. **Rotation**: The RN activity handles configuration changes itself, so a rotation resizes the display without recreating `BottomSheetView`. Screen height is therefore read per access rather than cached, and `maxHeight` is stored unclamped and clamped against the current screen at use time.
|
||||
|
||||
### Platform Differences
|
||||
|
||||
- **cornerRadius**: Applied to sheet on iOS, to content wrapper on Android (Android clips with `overflow: hidden`)
|
||||
|
||||
+158
-24
@@ -21,6 +21,12 @@ import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
/**
|
||||
* Fallback for Material's `material_bottom_sheet_max_width` dimen (in dp), used only
|
||||
* if the resource lookup fails. 640dp is the value Material ships.
|
||||
*/
|
||||
private const val FALLBACK_MAX_SHEET_WIDTH_DP = 640f
|
||||
|
||||
class BottomSheetView(
|
||||
context: Context,
|
||||
appContext: AppContext,
|
||||
@@ -38,26 +44,30 @@ class BottomSheetView(
|
||||
private var lastObservedContentHeight: Float = 0f
|
||||
private var pendingLayoutUpdate: Boolean = false
|
||||
|
||||
private val screenHeight: Float =
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
// API 35+: edge-to-edge is mandatory, heightPixels is the full display
|
||||
context.resources.displayMetrics.heightPixels
|
||||
.toFloat()
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
// API 30-34: heightPixels may exclude nav bar, use currentWindowMetrics
|
||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
|
||||
wm.currentWindowMetrics.bounds
|
||||
.height()
|
||||
.toFloat()
|
||||
} else {
|
||||
// API < 30: currentWindowMetrics not available, use getRealSize
|
||||
// which includes system bars (heightPixels may exclude them)
|
||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
|
||||
val size = android.graphics.Point()
|
||||
@Suppress("DEPRECATION")
|
||||
wm.defaultDisplay.getRealSize(size)
|
||||
size.y.toFloat()
|
||||
}
|
||||
// Computed per read rather than cached at construction: the RN activity handles
|
||||
// configuration changes itself, so a rotation resizes the display without
|
||||
// recreating this view and a cached value would stay stale for the sheet's life.
|
||||
private val screenHeight: Float
|
||||
get() =
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
// API 35+: edge-to-edge is mandatory, heightPixels is the full display
|
||||
context.resources.displayMetrics.heightPixels
|
||||
.toFloat()
|
||||
} else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
// API 30-34: heightPixels may exclude nav bar, use currentWindowMetrics
|
||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
|
||||
wm.currentWindowMetrics.bounds
|
||||
.height()
|
||||
.toFloat()
|
||||
} else {
|
||||
// API < 30: currentWindowMetrics not available, use getRealSize
|
||||
// which includes system bars (heightPixels may exclude them)
|
||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
|
||||
val size = android.graphics.Point()
|
||||
@Suppress("DEPRECATION")
|
||||
wm.defaultDisplay.getRealSize(size)
|
||||
size.y.toFloat()
|
||||
}
|
||||
|
||||
private fun getNavigationBarHeight(): Int {
|
||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||
@@ -73,6 +83,11 @@ class BottomSheetView(
|
||||
private val onSnapPointChange by EventDispatcher()
|
||||
private val onStateChange by EventDispatcher()
|
||||
|
||||
// Last canvas size (in dp) pushed into the shadow tree, so repeated layout
|
||||
// passes don't spam state updates
|
||||
private var lastPushedCanvasWidth: Float = -1f
|
||||
private var lastPushedCanvasHeight: Float = -1f
|
||||
|
||||
var disableDrag = false
|
||||
set(value) {
|
||||
field = value
|
||||
@@ -99,10 +114,11 @@ class BottomSheetView(
|
||||
field = if (value < 0) 0f else dpToPx(value)
|
||||
}
|
||||
|
||||
var maxHeight = this.screenHeight
|
||||
// Stored unclamped (in px) because screenHeight can change under us on rotation.
|
||||
// The clamp against the screen happens at use time, in getTargetHeight().
|
||||
var maxHeight = Float.MAX_VALUE
|
||||
set(value) {
|
||||
val px = dpToPx(value)
|
||||
field = if (px > this.screenHeight) this.screenHeight else px
|
||||
field = dpToPx(value)
|
||||
}
|
||||
|
||||
private var isOpen: Boolean = false
|
||||
@@ -140,6 +156,38 @@ class BottomSheetView(
|
||||
this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id)
|
||||
this.dialogRootViewGroup = DialogRootViewGroup(context)
|
||||
this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher
|
||||
|
||||
// The dialog container's measured WIDTH is the authoritative canvas width: it
|
||||
// already accounts for the window's horizontal insets, Material's max-width cap on
|
||||
// tablets and the current rotation. DialogRootViewGroup's own updateNodeSize() path
|
||||
// is a no-op on the new architecture (getNativeModule(UIManagerModule) returns null
|
||||
// under Fabric), so this state channel is what actually gets the width across there.
|
||||
//
|
||||
// Its measured HEIGHT is deliberately ignored - see canvasHeight.
|
||||
this.dialogRootViewGroup.setOnSizeChangeListener(
|
||||
object : DialogRootViewGroup.OnSizeChangeListener {
|
||||
override fun onSizeChange(
|
||||
width: Int,
|
||||
height: Int,
|
||||
) {
|
||||
val density = context.resources.displayMetrics.density
|
||||
pushCanvasSize(width / density, canvasHeight / density)
|
||||
|
||||
// onSizeChanged fires from inside a layout pass, so defer the reposition:
|
||||
// updateLayout() reads child heights that aren't final yet. This is what
|
||||
// makes the sheet settle back into place after a rotation. It no-ops for
|
||||
// fullHeight sheets, which is correct - those are pinned to the expanded
|
||||
// offset either way.
|
||||
if ((isOpen || isOpening) && !isClosing) {
|
||||
post {
|
||||
if ((isOpen || isOpening) && !isClosing) {
|
||||
updateLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
SheetManager.add(this)
|
||||
}
|
||||
@@ -151,9 +199,84 @@ class BottomSheetView(
|
||||
r: Int,
|
||||
b: Int,
|
||||
) {
|
||||
this.seedCanvasSize()
|
||||
this.present()
|
||||
}
|
||||
|
||||
/**
|
||||
* The height, in px, of the canvas the sheet content is laid out on. This is the whole
|
||||
* expanded frame (the behavior's expandedOffset is the status bar height), NOT the
|
||||
* sheet's current height.
|
||||
*
|
||||
* That distinction is the whole ballgame. The content's height is what drives the snap
|
||||
* points, so the canvas has to be room to grow *into*. Sizing the canvas from the
|
||||
* dialog's own measured height is circular - BottomSheetBehavior measures the dialog
|
||||
* container against the sheet, so the canvas collapses onto the content height, and from
|
||||
* then on the content is pinned: extra ScrollView padding (the Android keyboard path) or
|
||||
* a longer list just becomes scroll extent instead of a height change, the
|
||||
* OnLayoutChangeListener never fires, and the sheet stops responding to its content.
|
||||
*/
|
||||
private val canvasHeight: Float
|
||||
get() = screenHeight - getStatusBarHeight()
|
||||
|
||||
/**
|
||||
* JS renders the sheet content unsized, so before the first state commit it measures
|
||||
* 0x0 and present() has no content height to derive snap points from. Seed the canvas
|
||||
* here to kick that off - the dialog container reports the authoritative width later,
|
||||
* via its OnSizeChangeListener.
|
||||
*
|
||||
* Runs at most once per open cycle. It has to: each state commit re-fires onLayout, so
|
||||
* re-seeding would fight the width the dialog reported and the two would push each other
|
||||
* back and forth forever.
|
||||
*
|
||||
* stateWrapper is assigned while Fabric mounts the view, before the first layout pass,
|
||||
* so setViewSize() should already reach the shadow tree from here. On the old
|
||||
* architecture it is null and this no-ops, which is fine: DialogRootViewGroup's legacy
|
||||
* updateNodeSize() path still sizes the content there.
|
||||
*/
|
||||
private fun seedCanvasSize() {
|
||||
if (lastPushedCanvasWidth > 0f) return
|
||||
val density = context.resources.displayMetrics.density
|
||||
val widthPx =
|
||||
minOf(
|
||||
context.resources.displayMetrics.widthPixels
|
||||
.toFloat(),
|
||||
getMaxSheetWidth(),
|
||||
)
|
||||
this.pushCanvasSize(widthPx / density, canvasHeight / density)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size of this view's shadow node, which is the canvas the sheet content is
|
||||
* laid out on. Deduped because both onLayout and the dialog container's size changes
|
||||
* can re-report an unchanged size.
|
||||
*/
|
||||
private fun pushCanvasSize(
|
||||
widthDp: Float,
|
||||
heightDp: Float,
|
||||
) {
|
||||
if (widthDp <= 0f || heightDp <= 0f) return
|
||||
if (widthDp == lastPushedCanvasWidth && heightDp == lastPushedCanvasHeight) return
|
||||
lastPushedCanvasWidth = widthDp
|
||||
lastPushedCanvasHeight = heightDp
|
||||
this.shadowNodeProxy.setViewSize(widthDp.toDouble(), heightDp.toDouble())
|
||||
}
|
||||
|
||||
/**
|
||||
* Material caps the sheet frame at `material_bottom_sheet_max_width` (the
|
||||
* `android:maxWidth` on `Widget.MaterialComponents.BottomSheet`, which our dialog theme
|
||||
* inherits from) and centers it horizontally, so on tablets the sheet is narrower than
|
||||
* the display. Returns the cap in px.
|
||||
*/
|
||||
private fun getMaxSheetWidth(): Float =
|
||||
try {
|
||||
resources
|
||||
.getDimensionPixelSize(com.google.android.material.R.dimen.material_bottom_sheet_max_width)
|
||||
.toFloat()
|
||||
} catch (e: android.content.res.Resources.NotFoundException) {
|
||||
FALLBACK_MAX_SHEET_WIDTH_DP * context.resources.displayMetrics.density
|
||||
}
|
||||
|
||||
private fun destroy() {
|
||||
this.stopObservingContentHeight()
|
||||
this.isClosing = false
|
||||
@@ -178,6 +301,15 @@ class BottomSheetView(
|
||||
|
||||
val contentHeight = this.getContentHeight()
|
||||
|
||||
// The content is unsized until the canvas size we pushed lands in the shadow tree,
|
||||
// so bail and let this retry itself: the state commit resizes this view, that
|
||||
// re-fires onLayout, and onLayout re-enters present(). Full-height sheets don't
|
||||
// need a content measurement, so they can go ahead immediately.
|
||||
//
|
||||
// Only gate when there is a state channel to wait on. Without one (old architecture)
|
||||
// nothing would ever resize this view, and the sheet would never present.
|
||||
if (stateWrapper != null && !fullHeight && contentHeight <= 0f) return
|
||||
|
||||
var activityWindow: Window? = null
|
||||
var currentContext = context
|
||||
while (currentContext != null) {
|
||||
@@ -425,8 +557,10 @@ class BottomSheetView(
|
||||
|
||||
private fun getTargetHeight(): Float {
|
||||
val contentHeight = this.getContentHeight()
|
||||
// maxHeight is stored unclamped, so clamp it against the current screen here
|
||||
val effectiveMaxHeight = minOf(this.maxHeight, this.screenHeight)
|
||||
return when {
|
||||
contentHeight > maxHeight -> maxHeight
|
||||
contentHeight > effectiveMaxHeight -> effectiveMaxHeight
|
||||
contentHeight < minHeight -> minHeight
|
||||
else -> contentHeight
|
||||
}
|
||||
|
||||
@@ -34,10 +34,6 @@ const IS_IOS15 =
|
||||
Platform.OS === 'ios' &&
|
||||
// semvar - can be 3 segments, so can't use Number(Platform.Version)
|
||||
Number(Platform.Version.split('.').at(0)) < 16
|
||||
// older android versions (15 and below) aren't naturally edge-to-edge
|
||||
// and behave a little differently
|
||||
const IS_NON_E2E_ANDROID =
|
||||
Platform.OS === 'android' && Number(Platform.Version) < 35
|
||||
|
||||
export class BottomSheetNativeComponent extends Component<
|
||||
BottomSheetViewProps,
|
||||
@@ -148,24 +144,35 @@ function BottomSheetNativeComponentInner({
|
||||
const {height: screenHeight} = useWindowDimensions()
|
||||
const isHeightConstrained = maxHeight != null || rest.fullHeight === true
|
||||
|
||||
// sigh... on older Android versions, screenHeight does not include safe area insets
|
||||
// on newer Androids + iOS, it does. we need to find the inner bit + the bottom inset
|
||||
// for the sheet content
|
||||
const sheetHeight = IS_NON_E2E_ANDROID
|
||||
? screenHeight + insets.bottom
|
||||
: screenHeight - insets.top
|
||||
|
||||
return (
|
||||
<NativeView
|
||||
{...rest}
|
||||
maxHeight={maxHeight}
|
||||
onStateChange={onStateChange}
|
||||
ref={nativeViewRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: sheetHeight,
|
||||
width: '100%',
|
||||
}}
|
||||
/*
|
||||
* On Android the native side owns this view's size - the canvas the sheet
|
||||
* content is laid out on - and pushes it into the Fabric shadow tree through
|
||||
* ExpoView's `setViewSize` state channel. It knows the real sheet frame
|
||||
* (window insets, Material's max-width cap on tablets, rotation), which JS
|
||||
* can only guess at. `width` and `height` must stay unset there:
|
||||
* `ExpoViewComponentDescriptor::adopt()` only applies the state size on an
|
||||
* axis where the style leaves that dimension undefined, so a style dimension
|
||||
* would silently win and clip the content again.
|
||||
*
|
||||
* iOS still sizes the canvas from JS. Moving it onto the same state channel
|
||||
* needs on-device iteration on iOS 26 sheet geometry (large-detent and
|
||||
* floating-card metrics), so it is deferred.
|
||||
*/
|
||||
style={
|
||||
Platform.OS === 'ios'
|
||||
? {
|
||||
position: 'absolute',
|
||||
height: screenHeight - insets.top,
|
||||
width: '100%',
|
||||
}
|
||||
: {position: 'absolute'}
|
||||
}
|
||||
containerBackgroundColor={backgroundColor}>
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -8,7 +8,7 @@ import {Trans} from '@lingui/react/macro'
|
||||
import {EMBED_SCRIPT} from '#/lib/constants'
|
||||
import {niceDate} from '#/lib/strings/time'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as SegmentedControl from '#/components/forms/SegmentedControl'
|
||||
@@ -103,7 +103,7 @@ function EmbedDialogInner({
|
||||
}, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode])
|
||||
|
||||
return (
|
||||
<Dialog.Inner label={_(msg`Embed post`)} style={[{maxWidth: 500}]}>
|
||||
<Dialog.Inner label={_(msg`Embed post`)} style={[web({maxWidth: 500})]}>
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {Logo} from '#/view/icons/Logo'
|
||||
import {Logotype} from '#/view/icons/Logotype'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
@@ -45,7 +45,7 @@ function SigninDialogInner({}: {control: Dialog.DialogOuterProps['control']}) {
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Sign in to Bluesky or create a new account`)}
|
||||
style={[gtMobile ? {width: 'auto', maxWidth: 420} : a.w_full]}>
|
||||
style={[a.w_full, gtMobile && web({width: 'auto', maxWidth: 420})]}>
|
||||
<View style={[!IS_NATIVE && a.p_2xl]}>
|
||||
<View
|
||||
style={[
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {urls} from '#/lib/constants'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||
@@ -38,7 +38,8 @@ export function InitialVerificationAnnouncement() {
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Announcing verification on Bluesky`)}
|
||||
style={[
|
||||
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
|
||||
a.w_full,
|
||||
gtMobile && web({width: 'auto', maxWidth: 400, minWidth: 200}),
|
||||
]}>
|
||||
<View style={[a.align_start, a.gap_xl]}>
|
||||
<View
|
||||
|
||||
@@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {usePdsClient, useSession} from '#/state/session'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
@@ -62,7 +62,8 @@ function Inner({}: {control: DialogControlProps}) {
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Verify email dialog`)}
|
||||
style={[
|
||||
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
|
||||
a.w_full,
|
||||
gtMobile && web({width: 'auto', maxWidth: 400, minWidth: 200}),
|
||||
]}>
|
||||
<View style={[a.gap_xl]}>
|
||||
{status === 'loading' ? (
|
||||
|
||||
@@ -18,7 +18,7 @@ import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {useMyLabelersQuery} from '#/state/queries/preferences'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
import {atoms as a, useGutters, useTheme, web} from '#/alf'
|
||||
import * as Admonition from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -334,7 +334,7 @@ function Inner(
|
||||
testID="report:dialog"
|
||||
label={l`Report dialog`}
|
||||
ref={ref}
|
||||
style={[a.w_full, {maxWidth: 500}]}>
|
||||
style={[a.w_full, web({maxWidth: 500})]}>
|
||||
<View style={[a.gap_2xl, IS_NATIVE && a.pt_md]}>
|
||||
<StepOuter>
|
||||
<StepTitle
|
||||
|
||||
@@ -8,7 +8,7 @@ import {getUserDisplayName} from '#/lib/getUserDisplayName'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
@@ -78,7 +78,8 @@ function Inner({
|
||||
<Dialog.ScrollableInner
|
||||
label={label}
|
||||
style={[
|
||||
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
|
||||
a.w_full,
|
||||
gtMobile && web({width: 'auto', maxWidth: 400, minWidth: 200}),
|
||||
]}>
|
||||
<View style={[a.gap_sm, a.pb_lg]}>
|
||||
<Text style={[a.text_2xl, a.font_semi_bold, a.pr_4xl, a.leading_tight]}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Trans} from '@lingui/react/macro'
|
||||
import {urls} from '#/lib/constants'
|
||||
import {getUserDisplayName} from '#/lib/getUserDisplayName'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {VerifierCheck} from '#/components/icons/VerifierCheck'
|
||||
@@ -65,7 +65,8 @@ function Inner({
|
||||
<Dialog.ScrollableInner
|
||||
label={label}
|
||||
style={[
|
||||
gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
|
||||
a.w_full,
|
||||
gtMobile && web({width: 'auto', maxWidth: 400, minWidth: 200}),
|
||||
]}>
|
||||
<View style={[a.gap_lg]}>
|
||||
<View
|
||||
|
||||
@@ -395,11 +395,13 @@ export function CustomFeedHeader({
|
||||
) : null}
|
||||
</Layout.Header.Outer>
|
||||
</Layout.Center>
|
||||
<Dialog.Outer control={infoControl}>
|
||||
<Dialog.Outer
|
||||
control={infoControl}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<Dialog.ScrollableInner
|
||||
label={l`Feed menu`}
|
||||
style={[gtMobile ? {width: 'auto', minWidth: 450} : a.w_full]}>
|
||||
style={[a.w_full, gtMobile && web({width: 'auto', minWidth: 450})]}>
|
||||
<DialogInner
|
||||
info={info}
|
||||
likeUri={likeUri}
|
||||
|
||||
@@ -329,6 +329,7 @@ export function StepProfile() {
|
||||
{
|
||||
width: 'auto',
|
||||
maxWidth: 410,
|
||||
marginHorizontal: 'auto',
|
||||
},
|
||||
]}>
|
||||
<View style={[a.align_center, {paddingTop: 20}]}>
|
||||
|
||||
@@ -101,7 +101,7 @@ function DialogInner({
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Add a content warning`)}
|
||||
style={[{maxWidth: 500}, a.w_full]}>
|
||||
style={[a.w_full, web({maxWidth: 500})]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_2xl, a.font_semi_bold]}>
|
||||
|
||||
Reference in New Issue
Block a user