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,10 @@ import { ChatContext } from './chat-context.js'; import type { ChatNavigationProp } from './chat.react.js'; +import { + MessageEditingContext, + type MessageEditingContextType, +} from './message-editing-context.react.js'; import type { RemoveEditMode } from './message-list-types.js'; import TypeaheadTooltip from './typeahead-tooltip.react.js'; import Button from '../components/button.react.js'; @@ -173,6 +177,7 @@ ) => Promise, +navigation: ?ChatNavigationProp<'MessageList'>, +overlayContext: ?OverlayContextType, + +messageEditingContext: ?MessageEditingContextType, }; type State = { +text: string, @@ -432,8 +437,8 @@ } if ( - this.props.inputState?.editState.editedMessage && - !prevProps.inputState?.editState.editedMessage + this.props.messageEditingContext?.editState.editedMessage && + !prevProps.messageEditingContext?.editState.editedMessage ) { this.blockNavigation(); } @@ -766,7 +771,9 @@ return; } this.setState({ text, textEdited: true }); - this.props.inputState?.setEditedMessageChanged(this.isMessageEdited(text)); + this.props.messageEditingContext?.setEditedMessageChanged( + this.isMessageEdited(text), + ); if (this.isEditMode()) { return; } @@ -870,7 +877,7 @@ }; isEditMode = () => { - const editState = this.props.inputState?.editState; + const editState = this.props.messageEditingContext?.editState; const isThisThread = editState?.editedMessage?.threadID === this.props.threadInfo.id; return editState && editState.editedMessage !== null && isThisThread; @@ -946,7 +953,7 @@ }; getEditedMessage = () => { - const editState = this.props.inputState?.editState; + const editState = this.props.messageEditingContext?.editState; return editState?.editedMessage; }; @@ -970,7 +977,7 @@ }; exitEditMode = () => { - this.props.inputState?.setEditedMessage(null, () => { + this.props.messageEditingContext?.setEditedMessage(null, () => { this.unblockNavigation(); this.updateText(this.props.draft); this.focusAndUpdateButtonsVisibility(); @@ -999,7 +1006,7 @@ const { action } = e.data; e.preventDefault(); const saveExit = () => { - this.props.inputState?.setEditedMessage(null, () => { + this.props.messageEditingContext?.setEditedMessage(null, () => { this.setState({ isExitingDuringEditMode: true }, () => { if (!this.props.navigation) { return; @@ -1251,7 +1258,9 @@ parentThreadInfo, ); - const editedMessageInfo = inputState?.editState.editedMessage; + const messageEditingContext = React.useContext(MessageEditingContext); + + const editedMessageInfo = messageEditingContext?.editState.editedMessage; const editedMessagePreview = useMessagePreview( editedMessageInfo, props.threadInfo, @@ -1285,6 +1294,7 @@ editMessage={editMessage} navigation={props.navigation} overlayContext={overlayContext} + messageEditingContext={messageEditingContext} /> ); } diff --git a/native/chat/composed-message.react.js b/native/chat/composed-message.react.js --- a/native/chat/composed-message.react.js +++ b/native/chat/composed-message.react.js @@ -23,6 +23,7 @@ import { useComposedMessageMaxWidth } from './composed-message-width.js'; import { FailedSend } from './failed-send.react.js'; import { InlineEngagement } from './inline-engagement.react.js'; +import { MessageEditingContext } from './message-editing-context.react.js'; import { MessageHeader } from './message-header.react.js'; import { useNavigateToSidebar } from './sidebar-navigation.js'; import SwipeableMessage from './swipeable-message.react.js'; @@ -298,15 +299,18 @@ const navigateToSidebar = useNavigateToSidebar(props.item); const contentAndHeaderOpacity = useContentAndHeaderOpacity(props.item); const deliveryIconOpacity = useDeliveryIconOpacity(props.item); + + const messageEditingContext = React.useContext(MessageEditingContext); const progress = useDerivedValue(() => { const isThisThread = - inputState?.editState.editedMessage?.threadID === + messageEditingContext?.editState.editedMessage?.threadID === props.item.threadInfo.id; const isHighlighted = - inputState?.editState.editedMessage?.id === props.item.messageInfo.id && - isThisThread; + messageEditingContext?.editState.editedMessage?.id === + props.item.messageInfo.id && isThisThread; return withTiming(isHighlighted ? 1 : 0); }); + const editedMessageStyle = useAnimatedStyle(() => { const backgroundColor = interpolateColor( progress.value, diff --git a/native/chat/message-editing-context-provider.react.js b/native/chat/message-editing-context-provider.react.js new file mode 100644 --- /dev/null +++ b/native/chat/message-editing-context-provider.react.js @@ -0,0 +1,74 @@ +// @flow + +import * as React from 'react'; + +import type { MessageInfo } from 'lib/types/message-types.js'; + +import { + type EditState, + MessageEditingContext, +} from './message-editing-context.react.js'; + +const defaultEditState = { + editedMessage: null, + isEditedMessageChanged: false, +}; + +type Props = { + +children: React.Node, +}; +function MessageEditingContextProvider(props: Props): React.Node { + const [editState, setEditState] = React.useState(defaultEditState); + + const pendingCallbacksRef = React.useRef([]); + + const setEditedMessage = React.useCallback( + (editedMessage: ?MessageInfo, callback?: () => void) => { + if (callback) { + pendingCallbacksRef.current.push(callback); + } + setEditState({ editedMessage, isEditedMessageChanged: false }); + }, + [], + ); + + React.useEffect(() => { + if (pendingCallbacksRef.current.length === 0) { + return; + } + for (const callback of pendingCallbacksRef.current) { + callback(); + } + pendingCallbacksRef.current = []; + }, [editState]); + + const setEditedMessageChanged = React.useCallback( + (isEditedMessageChanged: boolean) => { + setEditState(prevEditState => ({ + ...prevEditState, + isEditedMessageChanged, + })); + }, + [], + ); + + const contextValue = React.useMemo( + () => ({ + editState, + setEditedMessage, + setEditedMessageChanged, + }), + [editState, setEditedMessage, setEditedMessageChanged], + ); + + return ( + + {props.children} + + ); +} + +const MemoizedMessageEditingContextProvider: React.ComponentType = + React.memo(MessageEditingContextProvider); + +export default MemoizedMessageEditingContextProvider; diff --git a/native/chat/message-editing-context.react.js b/native/chat/message-editing-context.react.js new file mode 100644 --- /dev/null +++ b/native/chat/message-editing-context.react.js @@ -0,0 +1,24 @@ +// @flow + +import * as React from 'react'; + +import type { MessageInfo } from 'lib/types/message-types.js'; + +export type EditState = { + +editedMessage: ?MessageInfo, + +isEditedMessageChanged: boolean, +}; + +export type MessageEditingContextType = { + +editState: EditState, + +setEditedMessage: ( + editedMessage: ?MessageInfo, + callback?: () => void, + ) => void, + +setEditedMessageChanged: (isEditedMessageChanged: boolean) => void, +}; + +const MessageEditingContext: React.Context = + React.createContext(null); + +export { MessageEditingContext }; diff --git a/native/chat/text-message-tooltip-modal.react.js b/native/chat/text-message-tooltip-modal.react.js --- a/native/chat/text-message-tooltip-modal.react.js +++ b/native/chat/text-message-tooltip-modal.react.js @@ -6,6 +6,7 @@ import { createMessageReply } from 'lib/shared/message-utils.js'; +import { MessageEditingContext } from './message-editing-context.react.js'; import { useNavigateToThread } from './message-list-types.js'; import { useOnPressReport } from './message-report-utils.js'; import { useAnimatedNavigateToSidebar } from './sidebar-navigation.js'; @@ -66,11 +67,14 @@ [], ); + const messageEditingContext = React.useContext(MessageEditingContext); + const { messageInfo } = route.params.item; const onPressEdit = React.useCallback(() => { invariant( - inputState, - 'inputState should be set in TextMessageTooltipModal.onPressEdit', + inputState && messageEditingContext, + 'inputState and messageEditingContext should be set in ' + + 'TextMessageTooltipModal.onPressEdit', ); const updateInputBar = () => { inputState.editInputMessage({ @@ -79,9 +83,10 @@ }); }; const enterEditMode = () => { - inputState.setEditedMessage(messageInfo, updateInputBar); + messageEditingContext.setEditedMessage(messageInfo, updateInputBar); }; - const { editedMessage, isEditedMessageChanged } = inputState.editState; + const { editedMessage, isEditedMessageChanged } = + messageEditingContext.editState; if (isEditedMessageChanged && editedMessage) { exitEditAlert({ onDiscard: enterEditMode, @@ -89,7 +94,7 @@ } else { enterEditMode(); } - }, [inputState, messageInfo, text]); + }, [inputState, messageEditingContext, messageInfo, text]); const renderEditIcon = React.useCallback( style => , [], diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js --- a/native/chat/text-message.react.js +++ b/native/chat/text-message.react.js @@ -15,6 +15,7 @@ import type { ChatNavigationProp } from './chat.react.js'; import ComposedMessage from './composed-message.react.js'; import { InnerTextMessage } from './inner-text-message.react.js'; +import { MessageEditingContext } from './message-editing-context.react.js'; import { MessagePressResponderContext, type MessagePressResponderContextType, @@ -22,7 +23,6 @@ import textMessageSendFailed from './text-message-send-failed.js'; import { getMessageTooltipKey } from './utils.js'; import { ChatContext, type ChatContextType } from '../chat/chat-context.js'; -import { InputStateContext } from '../input/input-state.js'; import { MarkdownContext } from '../markdown/markdown-context.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import { @@ -249,7 +249,6 @@ const overlayContext = React.useContext(OverlayContext); const chatContext = React.useContext(ChatContext); const markdownContext = React.useContext(MarkdownContext); - const inputContext = React.useContext(InputStateContext); invariant(markdownContext, 'markdownContext should be set'); const { linkModalActive, clearMarkdownContextData } = markdownContext; @@ -266,8 +265,9 @@ props.item.messageInfo, ); - const isThisMessageEdited = - inputContext?.editState.editedMessage?.id === props.item.messageInfo.id; + const messageEditingContext = React.useContext(MessageEditingContext); + const editMessageID = messageEditingContext?.editState.editedMessage?.id; + const isThisMessageEdited = editMessageID === props.item.messageInfo.id; const canEditMessage = useCanEditMessage(props.item.threadInfo, props.item.messageInfo) && diff --git a/native/input/input-state-container.react.js b/native/input/input-state-container.react.js --- a/native/input/input-state-container.react.js +++ b/native/input/input-state-container.react.js @@ -65,7 +65,6 @@ type RawMultimediaMessageInfo, type SendMessageResult, type SendMessagePayload, - type MessageInfo, } from 'lib/types/message-types.js'; import type { RawImagesMessageInfo } from 'lib/types/messages/images.js'; import type { @@ -106,7 +105,6 @@ import { type EditInputBarMessageParameters, - type EditState, InputStateContext, type PendingMultimediaUploads, type MultimediaProcessingStep, @@ -168,15 +166,10 @@ }; type State = { +pendingUploads: PendingMultimediaUploads, - +editState: EditState, }; class InputStateContainer extends React.PureComponent { state: State = { pendingUploads: {}, - editState: { - editedMessage: null, - isEditedMessageChanged: false, - }, }; sendCallbacks: Array<() => void> = []; activeURIs = new Map(); @@ -407,8 +400,7 @@ inputStateSelector = createSelector( (state: State) => state.pendingUploads, - (state: State) => state.editState, - (pendingUploads: PendingMultimediaUploads, editState: EditState) => ({ + (pendingUploads: PendingMultimediaUploads) => ({ pendingUploads, sendTextMessage: this.sendTextMessage, sendMultimediaMessage: this.sendMultimediaMessage, @@ -422,9 +414,6 @@ uploadInProgress: this.uploadInProgress, reportURIDisplayed: this.reportURIDisplayed, setPendingThreadUpdateHandler: this.setPendingThreadUpdateHandler, - editState, - setEditedMessage: this.setEditedMessage, - setEditedMessageChanged: this.setEditedMessageChanged, scrollToMessage: this.scrollToMessage, addScrollToMessageListener: this.addScrollToMessageListener, removeScrollToMessageListener: this.removeScrollToMessageListener, @@ -1427,21 +1416,6 @@ this.editInputBarCallbacks.push(callbackEditInputBar); }; - setEditedMessage = (editedMessage: ?MessageInfo, callback?: () => void) => { - this.setState( - { - editState: { editedMessage, isEditedMessageChanged: false }, - }, - callback, - ); - }; - - setEditedMessageChanged = (isEditedMessageChanged: boolean) => { - this.setState(prevState => ({ - editState: { ...prevState.editState, isEditedMessageChanged }, - })); - }; - removeEditInputMessageListener = ( callbackEditInputBar: (params: EditInputBarMessageParameters) => void, ) => { diff --git a/native/input/input-state.js b/native/input/input-state.js --- a/native/input/input-state.js +++ b/native/input/input-state.js @@ -3,7 +3,6 @@ import * as React from 'react'; import type { NativeMediaSelection } from 'lib/types/media-types.js'; -import type { MessageInfo } from 'lib/types/message-types.js'; import type { RawTextMessageInfo } from 'lib/types/messages/text.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; @@ -23,11 +22,6 @@ [localMessageID: string]: MessagePendingUploads, }; -export type EditState = { - +editedMessage: ?MessageInfo, - +isEditedMessageChanged: boolean, -}; - export type EditInputBarMessageParameters = { +message: string, +mode: 'prepend' | 'replace', @@ -65,12 +59,6 @@ threadID: string, pendingThreadUpdateHandler: ?(ThreadInfo) => mixed, ) => void, - +editState: EditState, - +setEditedMessage: ( - editedMessage: ?MessageInfo, - callback?: () => void, - ) => void, - +setEditedMessageChanged: (isEditedMessageChanged: boolean) => void, +scrollToMessage: (messageKey: string) => void, +addScrollToMessageListener: ((messageKey: string) => void) => void, +removeScrollToMessageListener: ((messageKey: string) => void) => void, diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -26,6 +26,7 @@ import NativeEditThreadAvatarProvider from './avatars/native-edit-thread-avatar-provider.react.js'; import NativeEditUserAvatarProvider from './avatars/native-edit-user-avatar-provider.react.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; +import MessageEditingContextProvider from './chat/message-editing-context-provider.react.js'; import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js'; import PersistedStateGate from './components/persisted-state-gate.js'; import ConnectedStatusBar from './connected-status-bar.react.js'; @@ -260,41 +261,43 @@ - - - - - - - - - - - - - - {gated} - - - - - {navigation} - - - - - - - - - - + + + + + + + + + + + + + + + {gated} + + + + + {navigation} + + + + + + + + + + +