allow swipe dismissal on iOS

This commit is contained in:
Samuel Newman
2025-09-11 15:07:12 +03:00
parent 60d826c4f0
commit fa52da8628
2 changed files with 38 additions and 15 deletions
+26 -11
View File
@@ -173,8 +173,10 @@ export const ComposePost = ({
imageUris: initImageUris, imageUris: initImageUris,
videoUri: initVideoUri, videoUri: initVideoUri,
cancelRef, cancelRef,
setIsDirty,
}: Props & { }: Props & {
cancelRef?: React.RefObject<CancelRef | null> cancelRef?: React.RefObject<CancelRef | null>
setIsDirty?: React.Dispatch<React.SetStateAction<boolean>>
}) => { }) => {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const agent = useAgent() const agent = useAgent()
@@ -304,24 +306,37 @@ export const ComposePost = ({
[insets, isKeyboardVisible], [insets, isKeyboardVisible],
) )
const onPressCancel = useCallback(() => { const isDirty = thread.posts.some(
if (textInput.current?.maybeClosePopup()) { post =>
post.shortenedGraphemeLength > 0 || post.embed.media || post.embed.link,
)
// very unfortunate, but we need to pass state back up to the parent on iOS
//
// WARNING - if the Modal on iOS thinks it's not dirty,
// `allowSwipeDismissal` will be true, and if we don't then close the composer
// when `onPressCancel` is called it will be bad (might even softlock)
// so we need to keep the parent state and the behaviour of onPressCancel in
// tight sync. do NOT force the modal to stay open without marking it as dirty! -sfn
useEffect(() => {
if (isIOS) {
setIsDirty?.(isDirty)
}
}, [isDirty, setIsDirty])
const onPressCancel = useNonReactiveCallback(() => {
// web only, so it's fine w.r.t. the Modal
const didCloseAutocomplete = textInput.current?.maybeClosePopup()
if (isWeb && didCloseAutocomplete) {
return return
} else if ( } else if (isDirty) {
thread.posts.some(
post =>
post.shortenedGraphemeLength > 0 ||
post.embed.media ||
post.embed.link,
)
) {
closeAllDialogs() closeAllDialogs()
Keyboard.dismiss() Keyboard.dismiss()
discardPromptControl.open() discardPromptControl.open()
} else { } else {
onClose() onClose()
} }
}, [thread, closeAllDialogs, discardPromptControl, onClose]) })
useImperativeHandle(cancelRef, () => ({onPressCancel})) useImperativeHandle(cancelRef, () => ({onPressCancel}))
+12 -4
View File
@@ -1,4 +1,4 @@
import React from 'react' import {useEffect, useRef, useState} from 'react'
import {Modal, View} from 'react-native' import {Modal, View} from 'react-native'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
@@ -11,11 +11,17 @@ export function Composer({}: {winHeight: number}) {
const t = useTheme() const t = useTheme()
const state = useComposerState() const state = useComposerState()
const ref = useComposerCancelRef() const ref = useComposerCancelRef()
const [isDirty, setIsDirty] = useState(
!!state?.text ||
!!state?.imageUris ||
!!state?.videoUri ||
!!state?.mention,
)
const open = !!state const open = !!state
const prevOpen = React.useRef(open) const prevOpen = useRef(open)
React.useEffect(() => { useEffect(() => {
if (open && !prevOpen.current) { if (open && !prevOpen.current) {
setFullyExpandedCount(c => c + 1) setFullyExpandedCount(c => c + 1)
} else if (!open && prevOpen.current) { } else if (!open && prevOpen.current) {
@@ -31,7 +37,8 @@ export function Composer({}: {winHeight: number}) {
visible={open} visible={open}
presentationStyle="pageSheet" presentationStyle="pageSheet"
animationType="slide" animationType="slide"
onRequestClose={() => ref.current?.onPressCancel()}> onRequestClose={() => ref.current?.onPressCancel()}
allowSwipeDismissal={!isDirty}>
<View style={[t.atoms.bg, a.flex_1]}> <View style={[t.atoms.bg, a.flex_1]}>
<ComposePost <ComposePost
cancelRef={ref} cancelRef={ref}
@@ -43,6 +50,7 @@ export function Composer({}: {winHeight: number}) {
text={state?.text} text={state?.text}
imageUris={state?.imageUris} imageUris={state?.imageUris}
videoUri={state?.videoUri} videoUri={state?.videoUri}
setIsDirty={setIsDirty}
/> />
</View> </View>
</Modal> </Modal>