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; @@ -193,6 +195,8 @@ targetSendButtonContainerOpen: Value; sendButtonContainerStyle: AnimatedViewStyle; + clearFocusListener: () => void; + clearBlurListener: () => void; clearBeforeRemoveListener: () => void; constructor(props: Props) { @@ -202,7 +206,7 @@ textEdited: false, buttonsExpanded: true, selectionState: { text: props.draft, selection: { start: 0, end: 0 } }, - isExitingEditMode: false, + isExitingDuringEditMode: false, }; this.setUpActionIconAnimations(); @@ -337,14 +341,25 @@ } componentDidMount() { - if (this.props.isActive) { + const { isActive, navigation } = this.props; + if (isActive) { this.addEditInputMessageListener(); } - if (this.props.navigation) { - this.clearBeforeRemoveListener = this.props.navigation.addListener( + if (navigation) { + this.clearBeforeRemoveListener = navigation.addListener( 'beforeRemove', this.onNavigationBeforeRemove, ); + invariant(navigation, 'navigation must be defined'); + this.clearBlurListener = navigation.addListener( + 'blur', + this.onNavigationBlur, + ); + invariant(navigation, 'navigation must be defined'); + this.clearFocusListener = navigation.addListener( + 'focus', + this.onNavigationFocus, + ); } } @@ -355,6 +370,12 @@ if (this.clearBeforeRemoveListener) { this.clearBeforeRemoveListener(); } + if (this.clearBlurListener) { + this.clearBlurListener(); + } + if (this.clearFocusListener) { + this.clearFocusListener(); + } } componentDidUpdate(prevProps: Props, prevState: State) { @@ -408,6 +429,13 @@ this.expandButtons(); this.setIOSKeyboardHeight(); } + + if ( + this.props.inputState?.editState.editedMessage && + !prevProps.inputState?.editState.editedMessage + ) { + this.blockNavigation(); + } } addEditInputMessageListener() { @@ -733,9 +761,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 +882,41 @@ return text !== originalText; }; + unblockNavigation = () => { + const { navigation } = this.props; + if (!navigation) { + return; + } + navigation.setParams({ removeEditMode: null }); + }; + + removeEditMode: RemoveEditMode = action => { + const { navigation } = this.props; + if (!navigation || this.state.isExitingDuringEditMode) { + return 'ignore_action'; + } + if (!this.isMessageEdited()) { + this.unblockNavigation(); + return 'reduce_action'; + } + const unblockAndDispatch = () => { + this.unblockNavigation(); + navigation.dispatch(action); + }; + exitEditAlert(unblockAndDispatch); + return 'ignore_action'; + }; + + 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,12 +962,27 @@ exitEditMode = () => { this.props.inputState?.setEditedMessage(null, () => { + this.unblockNavigation(); this.updateText(this.props.draft); this.focusAndUpdateButtonsVisibility(); this.updateSendButton(this.props.draft); }); }; + onNavigationFocus = () => { + this.setState({ isExitingDuringEditMode: false }); + }; + + onNavigationBlur = () => { + if (!this.isEditMode()) { + return; + } + this.setState( + { text: this.props.draft, isExitingDuringEditMode: true }, + this.exitEditMode, + ); + }; + onNavigationBeforeRemove = e => { if (!this.isEditMode()) { return; @@ -910,7 +991,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 +1188,7 @@ +onInputBarLayout?: (event: LayoutEvent) => mixed, +openCamera: () => mixed, +navigation?: ChatNavigationProp<'MessageList'>, + +isFocused?: boolean, }; function ConnectedChatInputBarBase(props: ConnectedChatInputBarBaseProps) { const navContext = React.useContext(NavContext); @@ -1297,12 +1379,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,22 @@ ); return baseGetStateForAction(clearedState, navigateAction, options); } else { - return baseGetStateForAction(lastState, action, options); + const result = baseGetStateForAction(lastState, action, options); + const removeEditMode = getRemoveEditMode(lastState); + + // We prevent navigating if the user is in edit mode. We don't block + // navigating back here because it is handled by the `beforeRemove` + // listener in the `ChatInputBar` component. + if ( + result !== null && + result?.index && + result.index > lastState.index && + removeEditMode && + removeEditMode(action) === 'ignore_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,13 @@ +threadInfo: ThreadInfo, +pendingPersonalThreadUserInfo?: UserInfo, +searching?: boolean, + +removeEditMode?: ?RemoveEditMode, }; +export type RemoveEditMode = ( + action: ChatRouterNavigationAction, +) => 'ignore_action' | 'reduce_action'; + 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, }; diff --git a/native/utils/edit-messages-utils.js b/native/utils/edit-messages-utils.js --- a/native/utils/edit-messages-utils.js +++ b/native/utils/edit-messages-utils.js @@ -14,9 +14,7 @@ { text: 'Discard edit', style: 'destructive', - onPress: () => { - onDiscard(); - }, + onPress: onDiscard, }, ], );