diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -84,6 +84,7 @@ import { ChatContext } from './chat-context.js'; import type { ChatNavigationProp } from './chat.react.js'; +import type { RemoveEditMode } from './message-list-types.js'; import TypeaheadTooltip from './typeahead-tooltip.react.js'; import Button from '../components/button.react.js'; // eslint-disable-next-line import/extensions @@ -169,13 +170,14 @@ text: string, ) => Promise, +navigation: ?ChatNavigationProp<'MessageList'>, + +isFocused?: boolean, }; type State = { +text: string, +textEdited: boolean, +buttonsExpanded: boolean, +selectionState: SyncedSelectionData, - +isExitingEditMode: boolean, + +isExitingDuringEditMode: boolean, }; class ChatInputBar extends React.PureComponent { textInput: ?React.ElementRef; @@ -202,7 +204,7 @@ textEdited: false, buttonsExpanded: true, selectionState: { text: props.draft, selection: { start: 0, end: 0 } }, - isExitingEditMode: false, + isExitingDuringEditMode: false, }; this.setUpActionIconAnimations(); @@ -408,6 +410,22 @@ this.expandButtons(); this.setIOSKeyboardHeight(); } + + if (!this.props.isFocused && prevProps.isFocused && this.isEditMode()) { + this.setState( + { text: this.props.draft, isExitingDuringEditMode: true }, + this.exitEditMode, + ); + } + if (this.props.isFocused && !prevProps.isFocused) { + this.setState({ isExitingDuringEditMode: false }); + } + if ( + this.props.inputState?.editState.editedMessage && + !prevProps.inputState?.editState.editedMessage + ) { + this.blockNavigation(); + } } addEditInputMessageListener() { @@ -733,9 +751,12 @@ }; updateText = (text: string) => { + if (this.state.isExitingDuringEditMode) { + return; + } this.setState({ text, textEdited: true }); this.props.inputState?.setEditedMessageChanged(this.isMessageEdited(text)); - if (this.isEditMode() || this.state.isExitingEditMode) { + if (this.isEditMode()) { return; } this.saveDraft(text); @@ -851,6 +872,42 @@ return text !== originalText; }; + unblockNavigation = () => { + const { navigation, isFocused } = this.props; + if (!navigation || !isFocused) { + return; + } + navigation.setParams({ removeEditMode: null }); + }; + + // It returns true if the action should be dispatched immediately. + removeEditMode: RemoveEditMode = action => { + const { navigation } = this.props; + if (!navigation || this.state.isExitingDuringEditMode) { + return false; + } + if (!this.isMessageEdited()) { + this.unblockNavigation(); + return true; + } + const unblockAndDispatch = () => { + this.unblockNavigation(); + navigation.dispatch(action); + }; + exitEditAlert(unblockAndDispatch); + return false; + }; + + blockNavigation = () => { + const { navigation, isFocused } = this.props; + if (!navigation || !isFocused) { + return; + } + navigation.setParams({ + removeEditMode: this.removeEditMode, + }); + }; + editMessage = async (messageID: string, text: string) => { if (!this.isMessageEdited()) { this.exitEditMode(); @@ -896,6 +953,7 @@ exitEditMode = () => { this.props.inputState?.setEditedMessage(null, () => { + this.unblockNavigation(); this.updateText(this.props.draft); this.focusAndUpdateButtonsVisibility(); this.updateSendButton(this.props.draft); @@ -910,7 +968,7 @@ e.preventDefault(); const saveExit = () => { this.props.inputState?.setEditedMessage(null, () => { - this.setState({ isExitingEditMode: true }, () => { + this.setState({ isExitingDuringEditMode: true }, () => { if (!this.props.navigation) { return; } @@ -1107,6 +1165,7 @@ +onInputBarLayout?: (event: LayoutEvent) => mixed, +openCamera: () => mixed, +navigation?: ChatNavigationProp<'MessageList'>, + +isFocused?: boolean, }; function ConnectedChatInputBarBase(props: ConnectedChatInputBarBaseProps) { const navContext = React.useContext(NavContext); @@ -1297,12 +1356,15 @@ }); }, [keyboardState, navigation, route.key, threadInfo]); + const isFocused = props.navigation.isFocused(); + return ( ); }); diff --git a/native/chat/chat-router.js b/native/chat/chat-router.js --- a/native/chat/chat-router.js +++ b/native/chat/chat-router.js @@ -20,6 +20,7 @@ clearThreadsActionType, pushNewThreadActionType, } from '../navigation/action-types.js'; +import { getRemoveEditMode } from '../navigation/nav-selectors.js'; import { removeScreensFromStack, getThreadIDFromRoute, @@ -128,7 +129,18 @@ ); return baseGetStateForAction(clearedState, navigateAction, options); } else { - return baseGetStateForAction(lastState, action, options); + const result = baseGetStateForAction(lastState, action, options); + const removeEditMode = getRemoveEditMode(lastState); + if ( + result !== null && + result?.index && + result.index > lastState.index && + removeEditMode && + !removeEditMode(action) + ) { + return lastState; + } + return result; } }, actionCreators: { diff --git a/native/chat/message-list-types.js b/native/chat/message-list-types.js --- a/native/chat/message-list-types.js +++ b/native/chat/message-list-types.js @@ -7,6 +7,7 @@ import type { ThreadInfo } from 'lib/types/thread-types.js'; import { type UserInfo } from 'lib/types/user-types.js'; +import type { ChatRouterNavigationAction } from './chat-router.js'; import type { MarkdownRules } from '../markdown/rules.react.js'; import { useTextMessageRulesFunc } from '../markdown/rules.react.js'; import { MessageListRouteName } from '../navigation/route-names.js'; @@ -15,8 +16,11 @@ +threadInfo: ThreadInfo, +pendingPersonalThreadUserInfo?: UserInfo, +searching?: boolean, + +removeEditMode?: ?RemoveEditMode, }; +export type RemoveEditMode = (action: ChatRouterNavigationAction) => boolean; + export type MessageListContextType = { +getTextMessageMarkdownRules: (useDarkStyle: boolean) => MarkdownRules, }; diff --git a/native/navigation/nav-selectors.js b/native/navigation/nav-selectors.js --- a/native/navigation/nav-selectors.js +++ b/native/navigation/nav-selectors.js @@ -31,6 +31,7 @@ threadRoutes, CommunityDrawerNavigatorRouteName, } from './route-names.js'; +import type { RemoveEditMode } from '../chat/message-list-types'; import { useSelector } from '../redux/redux-utils.js'; import type { NavPlusRedux } from '../types/selector-types.js'; import type { GlobalTheme } from '../types/themes.js'; @@ -316,6 +317,24 @@ }, ); +function getRemoveEditMode( + chatRouteState: ?PossiblyStaleNavigationState, +): ?RemoveEditMode { + if (!chatRouteState) { + return null; + } + const messageListRoute = + chatRouteState.routes[chatRouteState.routes.length - 1]; + if (messageListRoute.name !== MessageListRouteName) { + return null; + } + if (!messageListRoute || !messageListRoute.params) { + return null; + } + const removeEditMode: Function = messageListRoute.params.removeEditMode; + return removeEditMode; +} + function useCurrentLeafRouteName(): ?string { const navContext = React.useContext(NavContext); return React.useMemo(() => { @@ -342,4 +361,5 @@ useCalendarQuery, drawerSwipeEnabledSelector, useCurrentLeafRouteName, + getRemoveEditMode, };