diff --git a/web/input/input-state-container.react.js b/web/input/input-state-container.react.js --- a/web/input/input-state-container.react.js +++ b/web/input/input-state-container.react.js @@ -80,7 +80,12 @@ import InvalidUploadModal from '../modals/chat/invalid-upload.react'; import { useSelector } from '../redux/redux-utils'; import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; -import { type PendingMultimediaUpload, InputStateContext } from './input-state'; +import { + type PendingMultimediaUpload, + type InputState, + type TypeaheadState, + InputStateContext, +} from './input-state'; const browser = detectBrowser(); const exifRotate = @@ -126,6 +131,7 @@ [threadID: string]: { [localUploadID: string]: PendingMultimediaUpload }, }, +textCursorPositions: { [threadID: string]: number }, + +typeaheadState: TypeaheadState, }; type PropsAndState = { @@ -136,6 +142,13 @@ state: State = { pendingUploads: {}, textCursorPositions: {}, + typeaheadState: { + canBeVisible: false, + moveChoiceUp: null, + moveChoiceDown: null, + close: null, + accept: null, + }, }; replyCallbacks: Array<(message: string) => void> = []; pendingThreadCreations = new Map>(); @@ -460,7 +473,7 @@ return threadCreationPromise; } - inputStateSelector = _memoize((threadID: string) => + inputBaseStateSelector = _memoize((threadID: string) => createSelector( (propsAndState: PropsAndState) => propsAndState.pendingUploads[threadID], (propsAndState: PropsAndState) => @@ -526,6 +539,23 @@ ), ); + typeaheadStateSelector = createSelector( + (propsAndState: PropsAndState) => propsAndState.typeaheadState, + (typeaheadState: TypeaheadState) => ({ + typeaheadState, + setTypeaheadState: this.setTypeaheadState, + }), + ); + + inputStateSelector = createSelector( + state => state.inputBaseState, + state => state.typeaheadState, + (inputBaseState, typeaheadState) => ({ + ...inputBaseState, + ...typeaheadState, + }), + ); + getRealizedOrPendingThreadID(threadID: string): string { return this.props.pendingRealizedThreadIDs.get(threadID) ?? threadID; } @@ -1097,6 +1127,15 @@ }); } + setTypeaheadState = (newState: $Shape) => { + this.setState(prevState => ({ + typeaheadState: { + ...prevState.typeaheadState, + ...newState, + }, + })); + }; + setProgress( threadID: string, localUploadID: string, @@ -1242,12 +1281,27 @@ render() { const { activeChatThreadID } = this.props; - const inputState = activeChatThreadID - ? this.inputStateSelector(activeChatThreadID)({ - ...this.state, - ...this.props, - }) - : null; + + // we're going with two selectors as we want to avoid + // recreation of chat state setter functions on typeahead state updates + let inputState: ?InputState = null; + if (activeChatThreadID) { + const inputBaseState = this.inputBaseStateSelector(activeChatThreadID)({ + ...this.state, + ...this.props, + }); + + const typeaheadState = this.typeaheadStateSelector({ + ...this.state, + ...this.props, + }); + + inputState = this.inputStateSelector({ + inputBaseState, + typeaheadState, + }); + } + return ( {this.props.children} diff --git a/web/input/input-state.js b/web/input/input-state.js --- a/web/input/input-state.js +++ b/web/input/input-state.js @@ -34,6 +34,14 @@ selectTime: number, }; +export type TypeaheadState = { + +canBeVisible: boolean, + +moveChoiceUp: ?() => void, + +moveChoiceDown: ?() => void, + +close: ?() => void, + +accept: ?() => void, +}; + // This type represents the input state for a particular thread export type InputState = { +pendingUploads: $ReadOnlyArray, @@ -42,6 +50,7 @@ }, +draft: string, +textCursorPosition: number, + +typeaheadState: TypeaheadState, +appendFiles: (files: $ReadOnlyArray) => Promise, +cancelPendingUpload: (localUploadID: string) => void, +sendTextMessage: ( @@ -51,6 +60,7 @@ +createMultimediaMessage: (localID: number, threadInfo: ThreadInfo) => void, +setDraft: (draft: string) => void, +setTextCursorPosition: (newPosition: number) => void, + +setTypeaheadState: ($Shape) => void, +messageHasUploadFailure: (localMessageID: string) => boolean, +retryMultimediaMessage: ( localMessageID: string,