diff --git a/modules/bottom-sheet/android/build.gradle b/modules/bottom-sheet/android/build.gradle index 555d34ed1f..a1d423044b 100644 --- a/modules/bottom-sheet/android/build.gradle +++ b/modules/bottom-sheet/android/build.gradle @@ -40,17 +40,10 @@ android { lintOptions { abortOnError false } - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.8" - } } dependencies { implementation project(':expo-modules-core') - implementation "androidx.compose.material3:material3:1.3.0" - implementation "androidx.compose.material:material:1.7.2" + implementation 'com.google.android.material:material:1.12.0' implementation "com.facebook.react:react-native:+" } diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt index a34d1fdd43..4a9b2ccd4d 100644 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt @@ -31,11 +31,11 @@ class BottomSheetModule : Module() { } Prop("containerBackgroundColor") { view: BottomSheetView, prop: String -> - view.sheetState.value.containerBackgroundColor = Color.parseColor(prop) +// view.sheetState.value.containerBackgroundColor = Color.parseColor(prop) } Prop("cornerRadius") { view: BottomSheetView, prop: Float -> - view.sheetState.value.cornerRadius = prop +// view.sheetState.value.cornerRadius = prop } Prop("minHeight") { view: BottomSheetView, prop: Float -> @@ -51,7 +51,7 @@ class BottomSheetModule : Module() { } Prop("preventExpansion") { view: BottomSheetView, prop: Boolean -> - view.sheetState.value.preventExpansion = prop + view.preventExpansion = prop } } } diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt new file mode 100644 index 0000000000..1792c025eb --- /dev/null +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt @@ -0,0 +1,253 @@ +package expo.modules.bottomsheet + +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.core.view.allViews +import com.facebook.react.ReactRootView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView + +class BottomSheetView( + context: Context, + appContext: AppContext, +) : ExpoView(context, appContext) { + + private var reactRootView: ReactRootView? = null + private var innerView: View? = null + private var dialog: BottomSheetDialog? = null + + private val screenHeight = context.resources.displayMetrics.heightPixels.toFloat() + + private val onAttemptDismiss by EventDispatcher() + private val onSnapPointChange by EventDispatcher() + private val onStateChange by EventDispatcher() + + // Props + var preventDismiss = false + set(value) { + field = value + this.dialog?.setCancelable(!value) + } + var preventExpansion = false + + var minHeight = 0f + set (value) { + field = if (value < 0) { + 0f + } else { + value + } + } + + var maxHeight = this.screenHeight + set (value) { + field = if (value > this.screenHeight) { + this.screenHeight.toFloat() + } else { + value + } + } + + private var isOpen: Boolean = false + set(value) { + field = value + onStateChange( + mapOf( + "state" to if (value) "open" else "closed", + ), + ) + } + + private var isOpening: Boolean = false + set(value) { + field = value + if (value) { + onStateChange(mapOf( + "state" to "opening" + )) + } + } + + private var isClosing: Boolean = false + set(value) { + field = value + if (value) { + onStateChange(mapOf( + "state" to "closing" + )) + } + } + + private var selectedSnapPoint = 0 + set(value) { + if (field == value) return + + field = value + onSnapPointChange( + mapOf( + "snapPoint" to value, + ), + ) + } + + // Lifecycle + + init { + SheetManager.add(this) + } + + override fun addView( + child: View?, + index: Int, + ) { + this.innerView = child + } + + override fun onLayout( + changed: Boolean, + l: Int, + t: Int, + r: Int, + b: Int, + ) { + this.present() + } + + private fun destroy() { + this.isClosing = false + this.isOpen = false + this.dialog = null + this.reactRootView = null + this.innerView = null + SheetManager.remove(this) + } + + // Presentation + + private fun present() { + if (this.isOpen || this.isOpening || this.isClosing) return + + val innerView = this.innerView ?: return + val contentHeight = this.getContentHeight() + + // Needs to be a react root view for RNGH to work + val rootView = ReactRootView(context) + rootView.addView(innerView) + this.reactRootView = rootView + + val dialog = BottomSheetDialog(context) + dialog.setContentView(rootView) + dialog.setCancelable(!preventDismiss) + dialog.setOnShowListener { + val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) + bottomSheet?.let { + // Let the outside view handle the background color on its own, the default for this is + // white and we don't want that. + it.setBackgroundColor(0) + + val behavior = BottomSheetBehavior.from(it) + + behavior.isFitToContents = true + behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight) + if (contentHeight > this.screenHeight) { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + this.selectedSnapPoint = 2 + } else { + behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED + this.selectedSnapPoint = 1 + } + behavior.skipCollapsed = true + behavior.isDraggable = true + behavior.isHideable = true + + behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + when (newState) { + BottomSheetBehavior.STATE_EXPANDED -> { + selectedSnapPoint = 2 + } + BottomSheetBehavior.STATE_COLLAPSED -> { + selectedSnapPoint = 1 + } + BottomSheetBehavior.STATE_HALF_EXPANDED -> { + selectedSnapPoint = 1 + } + BottomSheetBehavior.STATE_HIDDEN -> { + selectedSnapPoint = 0 + } + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { } + }) + } + } + dialog.setOnDismissListener { + this.isClosing = true + this.destroy() + } + + this.isOpening = true + dialog.show() + this.dialog = dialog + } + + fun updateLayout() { + val dialog = this.dialog ?: return + val contentHeight = this.getContentHeight() + + val bottomSheet = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) + bottomSheet?.let { + val behavior = BottomSheetBehavior.from(it) + + behavior.halfExpandedRatio = this.clampRatio(this.getTargetHeight() / this.screenHeight) + + if (contentHeight > this.screenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } else if (contentHeight < this.screenHeight && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) { + behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED + } + } + } + + fun dismiss() { + this.dialog?.dismiss() + } + + // Util + + private fun getContentHeight(): Float { + val innerView = this.innerView ?: return 0f + innerView.allViews.forEach { + if (it.javaClass.simpleName == "RNGestureHandlerRootView") { + return it.height.toFloat() + 50f + } + } + return 0f + } + + private fun getTargetHeight(): Float { + val contentHeight = this.getContentHeight() + val height = if (contentHeight > maxHeight) { + maxHeight + } else if (contentHeight < minHeight) { + minHeight + } else { + contentHeight + } + return height + } + + private fun clampRatio(ratio: Float): Float { + if (ratio < 0.01) { + return 0.01f + } else if (ratio > 0.99) { + return 0.99f + } + return ratio + } +} diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/SheetManager.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/SheetManager.kt new file mode 100644 index 0000000000..ac6a820235 --- /dev/null +++ b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/SheetManager.kt @@ -0,0 +1,28 @@ +package expo.modules.bottomsheet + +import java.lang.ref.WeakReference + +class SheetManager { + companion object { + private val sheets = mutableSetOf>() + + fun add(view: BottomSheetView) { + sheets.add(WeakReference(view)) + } + + fun remove(view: BottomSheetView) { + sheets.forEach { + if (it.get() == view) { + sheets.remove(it) + return + } + } + } + + fun dismissAll() { + sheets.forEach { + it.get()?.dismiss() + } + } + } +} \ No newline at end of file diff --git a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/SheetView.kt b/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/SheetView.kt deleted file mode 100644 index cf12597590..0000000000 --- a/modules/bottom-sheet/android/src/main/java/expo/modules/bottomsheet/SheetView.kt +++ /dev/null @@ -1,70 +0,0 @@ -package expo.modules.bottomsheet - -import android.view.View -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetValue -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView - -data class SheetState( - var isOpen: Boolean = false, - var cornerRadius: Float? = null, - var containerBackgroundColor: Int? = null, - var preventExpansion: Boolean = false, -) - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SheetView( - state: MutableState, - innerView: View, - contentHeight: Float, - onDismissRequest: () -> Unit, - onExpanded: () -> Unit, - onHidden: () -> Unit, -) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) - - ModalBottomSheet( - sheetState = sheetState, - onDismissRequest = onDismissRequest, - shape = - RoundedCornerShape( - topStart = state.value.cornerRadius ?: 0f, - topEnd = state.value.cornerRadius ?: 0f, - ), - containerColor = Color(state.value.containerBackgroundColor ?: android.graphics.Color.TRANSPARENT), - ) { - Column( - Modifier - .fillMaxWidth() - .height(contentHeight.dp) - // Prevent covering up the handle - .padding(top = 34.dp), - ) { - AndroidView( - factory = { innerView }, - ) - } - } - - LaunchedEffect(sheetState.currentValue) { - if (sheetState.currentValue == SheetValue.PartiallyExpanded || sheetState.currentValue == SheetValue.Expanded) { - onExpanded() - } else if (sheetState.currentValue == SheetValue.Hidden) { - onHidden() - } - } -} diff --git a/modules/bottom-sheet/ios/SheetView.swift b/modules/bottom-sheet/ios/SheetView.swift index 54944030ff..be05dfa94c 100644 --- a/modules/bottom-sheet/ios/SheetView.swift +++ b/modules/bottom-sheet/ios/SheetView.swift @@ -111,6 +111,8 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate { func present() { guard !self.isOpen, + !self.isOpening, + !self.isClosing, let innerView = self.innerView, let contentHeight = innerView.subviews.first?.frame.height, let rvc = self.reactViewController() else { diff --git a/modules/bottom-sheet/ios/SheetViewController.swift b/modules/bottom-sheet/ios/SheetViewController.swift index 9caa5bab14..004e79a5c4 100644 --- a/modules/bottom-sheet/ios/SheetViewController.swift +++ b/modules/bottom-sheet/ios/SheetViewController.swift @@ -27,7 +27,7 @@ class SheetViewController: UIViewController { return } - if contentHeight > screenHeight { + if contentHeight > screenHeight - 100 { sheet.detents = [ .large() ] @@ -43,10 +43,10 @@ class SheetViewController: UIViewController { .medium() ] } - } - - if !preventExpansion { - sheet.detents.append(.large()) + + if !preventExpansion { + sheet.detents.append(.large()) + } } } diff --git a/modules/bottom-sheet/src/BottomSheet.tsx b/modules/bottom-sheet/src/BottomSheet.tsx index 7a402e7a8e..e7d19ae975 100644 --- a/modules/bottom-sheet/src/BottomSheet.tsx +++ b/modules/bottom-sheet/src/BottomSheet.tsx @@ -3,6 +3,7 @@ import { ColorValue, Dimensions, NativeSyntheticEvent, + Platform, StyleProp, StyleSheet, View, @@ -79,6 +80,7 @@ export class BottomSheet extends React.Component< const {children, ...rest} = this.props const topInset = rest.topInset ?? 0 const bottomInset = rest.bottomInset ?? 0 + const cornerRadius = rest.cornerRadius ?? 0 if (!this.state.open) { return null @@ -92,17 +94,23 @@ export class BottomSheet extends React.Component< ref={this.ref} style={{ position: 'absolute', - height: screenHeight - topInset - bottomInset, + height: screenHeight, width: '100%', }} containerBackgroundColor={backgroundColor}> + style={[ + { + flex: 1, + backgroundColor, + paddingTop: topInset, + paddingBottom: bottomInset, + }, + Platform.OS === 'android' && { + borderTopLeftRadius: cornerRadius, + borderTopRightRadius: cornerRadius, + }, + ]}> {children} diff --git a/src/components/BottomSheetLink.web.tsx b/src/components/BottomSheetLink.web.tsx index 6bfb275c25..eb21ca880e 100644 --- a/src/components/BottomSheetLink.web.tsx +++ b/src/components/BottomSheetLink.web.tsx @@ -1,3 +1 @@ -import {Link as BottomSheetLink} from './Link' - -export {BottomSheetLink} +export {Link as BottomSheetInlineLinkText} from './Link' diff --git a/src/components/Dialog/DialogTextInput.android.ts b/src/components/Dialog/DialogTextInput.android.ts new file mode 100644 index 0000000000..248eb2602d --- /dev/null +++ b/src/components/Dialog/DialogTextInput.android.ts @@ -0,0 +1,3 @@ +import {TextInput as DialogTextInput} from 'react-native-gesture-handler' + +export {DialogTextInput} diff --git a/src/components/Dialog/DialogTextInput.ts b/src/components/Dialog/DialogTextInput.ts new file mode 100644 index 0000000000..faad8d8675 --- /dev/null +++ b/src/components/Dialog/DialogTextInput.ts @@ -0,0 +1,3 @@ +import {TextInput as DialogTextInput} from 'react-native' + +export {DialogTextInput} diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index ae7f09e658..b9973b1a73 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -1,10 +1,6 @@ import React, {useImperativeHandle} from 'react' import {StyleProp, View, ViewStyle} from 'react-native' -import { - GestureHandlerRootView, - ScrollView, - TextInput as RNGHTextInput, -} from 'react-native-gesture-handler' +import {GestureHandlerRootView, ScrollView} from 'react-native-gesture-handler' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' @@ -14,6 +10,7 @@ import {useDialogStateControlContext} from '#/state/dialogs' import {List, ListMethods, ListProps} from '#/view/com/util/List' import {atoms as a, useTheme} from '#/alf' import {Context, useDialogContext} from '#/components/Dialog/context' +import {DialogTextInput} from '#/components/Dialog/DialogTextInput' import { DialogControlProps, DialogInnerProps, @@ -27,7 +24,7 @@ export {useDialogContext, useDialogControl} from '#/components/Dialog/context' export * from '#/components/Dialog/types' export * from '#/components/Dialog/utils' // @ts-ignore -export const Input = createInput(RNGHTextInput) +export const Input = createInput(DialogTextInput) export function Outer({ children, @@ -143,18 +140,23 @@ export function Inner({children, style}: DialogInnerProps) { } export const ScrollableInner = React.forwardRef( - function ScrollableInner({children, style}, ref) { + function ScrollableInner({children, style, ...props}, ref) { const insets = useSafeAreaInsets() const {nativeSnapPoint} = useDialogContext() return ( {children} - + ) }, diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 5a66767243..447833a239 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -241,45 +241,6 @@ export function Link({ ) } -export function BottomSheetLink({ - children, - to, - action = 'push', - onPress: outerOnPress, - download, - ...rest -}: LinkProps) { - const {href, isExternal, onPress} = useLink({ - to, - displayText: typeof children === 'string' ? children : '', - action, - onPress: outerOnPress, - }) - - return ( - - ) -} - export type InlineLinkProps = React.PropsWithChildren< BaseLinkProps & TextStyleProp & Pick > & diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index bfdcf4099d..4654d9f9b4 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -119,10 +119,8 @@ export function Item({children, label, style, onPress, ...rest}: ItemProps) { accessibilityLabel={label} onFocus={onFocus} onBlur={onBlur} - onPress={e => { - control?.close() - onPress(e) - + onPress={async e => { + await onPress(e) if (!e.defaultPrevented) { control?.close() } diff --git a/src/components/ReportDialog/SelectReportOptionView.tsx b/src/components/ReportDialog/SelectReportOptionView.tsx index 7e27cacf0c..169c07d732 100644 --- a/src/components/ReportDialog/SelectReportOptionView.tsx +++ b/src/components/ReportDialog/SelectReportOptionView.tsx @@ -5,7 +5,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {ReportOption, useReportOptions} from '#/lib/moderation/useReportOptions' -import {BottomSheetLink} from '#/components/Link' +import {Link} from '#/components/Link' import {DMCA_LINK} from '#/components/ReportDialog/const' export {useDialogControl as useReportDialogControl} from '#/components/Dialog' @@ -129,7 +129,7 @@ export function SelectReportOptionView({ ]}> Need to report a copyright violation? - View details - + )} diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx index 682e918c2d..5b2f527fdd 100644 --- a/src/components/ReportDialog/SubmitView.tsx +++ b/src/components/ReportDialog/SubmitView.tsx @@ -6,6 +6,7 @@ import {useLingui} from '@lingui/react' import {getLabelingServiceTitle} from '#/lib/moderation' import {ReportOption} from '#/lib/moderation/useReportOptions' +import {isAndroid} from '#/platform/detection' import {useAgent} from '#/state/session' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import * as Toast from '#/view/com/util/Toast' @@ -227,6 +228,8 @@ export function SubmitView({ {submitting && } + {/* Maybe fix this later -h */} + {isAndroid ? : null} ) } diff --git a/src/components/moderation/LabelsOnMeDialog.tsx b/src/components/moderation/LabelsOnMeDialog.tsx index fc30b004ad..acedf81754 100644 --- a/src/components/moderation/LabelsOnMeDialog.tsx +++ b/src/components/moderation/LabelsOnMeDialog.tsx @@ -16,7 +16,6 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {BottomSheetInlineLinkText} from '#/components/BottomSheetLink' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' -import {InlineLinkText} from '#/components/Link' import {Text} from '#/components/Typography' import {Divider} from '../Divider' import {Loader} from '../Loader' @@ -237,24 +236,24 @@ function AppealForm({ return ( <> - - Appeal "{strings.name}" label - - - - This appeal will be sent to{' '} - control.close()} - style={[a.text_md, a.leading_snug]}> - {sourceName} - - . - - + + + Appeal "{strings.name}" label + + + This appeal will be sent to{' '} + + control.close()} + style={[a.text_md, a.leading_snug]}> + {sourceName} + + . + { + async (url: string, override?: boolean) => { if (isBskyRSSUrl(url) && isRelativeUrl(url)) { url = createBskyAppAbsoluteUrl(url) } @@ -75,7 +75,7 @@ export function useOpenLink() { }) return } else if (override ?? enabled) { - WebBrowser.openBrowserAsync(url, { + await WebBrowser.openBrowserAsync(url, { presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, toolbarColor: pal.colors.backgroundLight, diff --git a/src/view/com/composer/GifAltText.tsx b/src/view/com/composer/GifAltText.tsx index b4ff731fbe..bc93084f50 100644 --- a/src/view/com/composer/GifAltText.tsx +++ b/src/view/com/composer/GifAltText.tsx @@ -12,6 +12,7 @@ import { parseEmbedPlayerFromUrl, } from '#/lib/strings/embed-player' import {enforceLen} from '#/lib/strings/helpers' +import {isAndroid} from '#/platform/detection' import {Gif} from '#/state/queries/tenor' import {atoms as a, native, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' @@ -184,6 +185,8 @@ function AltTextInner({ + {/* Maybe fix this later -h */} + {isAndroid ? : null} ) } diff --git a/src/view/com/composer/photos/ImageAltTextDialog.tsx b/src/view/com/composer/photos/ImageAltTextDialog.tsx index a5ef5bc031..a468db5376 100644 --- a/src/view/com/composer/photos/ImageAltTextDialog.tsx +++ b/src/view/com/composer/photos/ImageAltTextDialog.tsx @@ -5,7 +5,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {MAX_ALT_TEXT} from '#/lib/constants' -import {isWeb} from '#/platform/detection' +import {isAndroid, isWeb} from '#/platform/detection' import {ComposerImage} from '#/state/gallery' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' @@ -116,6 +116,8 @@ const ImageAltTextInner = ({ + {/* Maybe fix this later -h */} + {isAndroid ? : null} ) } diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index da1acfd256..cd1f2d3de6 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -240,8 +240,8 @@ let PostDropdownBtn = ({ Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') }, [_, richText]) - const onPressTranslate = React.useCallback(() => { - openLink(translatorUrl) + const onPressTranslate = React.useCallback(async () => { + await openLink(translatorUrl) }, [openLink, translatorUrl]) const onHidePost = React.useCallback(() => {