From fa52da862860a808dae3e41d2a6d280b7b22eb77 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 11 Sep 2025 15:07:12 +0300 Subject: [PATCH] allow swipe dismissal on iOS --- src/view/com/composer/Composer.tsx | 37 +++++++++++++++++++++--------- src/view/shell/Composer.ios.tsx | 16 +++++++++---- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 2a9635402f..3e33e86b67 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -173,8 +173,10 @@ export const ComposePost = ({ imageUris: initImageUris, videoUri: initVideoUri, cancelRef, + setIsDirty, }: Props & { cancelRef?: React.RefObject + setIsDirty?: React.Dispatch> }) => { const {currentAccount} = useSession() const agent = useAgent() @@ -304,24 +306,37 @@ export const ComposePost = ({ [insets, isKeyboardVisible], ) - const onPressCancel = useCallback(() => { - if (textInput.current?.maybeClosePopup()) { + const isDirty = thread.posts.some( + 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 - } else if ( - thread.posts.some( - post => - post.shortenedGraphemeLength > 0 || - post.embed.media || - post.embed.link, - ) - ) { + } else if (isDirty) { closeAllDialogs() Keyboard.dismiss() discardPromptControl.open() } else { onClose() } - }, [thread, closeAllDialogs, discardPromptControl, onClose]) + }) useImperativeHandle(cancelRef, () => ({onPressCancel})) diff --git a/src/view/shell/Composer.ios.tsx b/src/view/shell/Composer.ios.tsx index 393b8f80e6..8def80f91c 100644 --- a/src/view/shell/Composer.ios.tsx +++ b/src/view/shell/Composer.ios.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useEffect, useRef, useState} from 'react' import {Modal, View} from 'react-native' import {useDialogStateControlContext} from '#/state/dialogs' @@ -11,11 +11,17 @@ export function Composer({}: {winHeight: number}) { const t = useTheme() const state = useComposerState() const ref = useComposerCancelRef() + const [isDirty, setIsDirty] = useState( + !!state?.text || + !!state?.imageUris || + !!state?.videoUri || + !!state?.mention, + ) const open = !!state - const prevOpen = React.useRef(open) + const prevOpen = useRef(open) - React.useEffect(() => { + useEffect(() => { if (open && !prevOpen.current) { setFullyExpandedCount(c => c + 1) } else if (!open && prevOpen.current) { @@ -31,7 +37,8 @@ export function Composer({}: {winHeight: number}) { visible={open} presentationStyle="pageSheet" animationType="slide" - onRequestClose={() => ref.current?.onPressCancel()}> + onRequestClose={() => ref.current?.onPressCancel()} + allowSwipeDismissal={!isDirty}>