Changeset View
Changeset View
Standalone View
Standalone View
web/input/input-state-container.react.js
Show First 20 Lines • Show All 74 Lines • ▼ Show 20 Lines | |||||
} from 'lib/utils/action-utils'; | } from 'lib/utils/action-utils'; | ||||
import { getConfig } from 'lib/utils/config'; | import { getConfig } from 'lib/utils/config'; | ||||
import { getMessageForException, cloneError } from 'lib/utils/errors'; | import { getMessageForException, cloneError } from 'lib/utils/errors'; | ||||
import { validateFile, preloadImage } from '../media/media-utils'; | import { validateFile, preloadImage } from '../media/media-utils'; | ||||
import InvalidUploadModal from '../modals/chat/invalid-upload.react'; | import InvalidUploadModal from '../modals/chat/invalid-upload.react'; | ||||
import { useSelector } from '../redux/redux-utils'; | import { useSelector } from '../redux/redux-utils'; | ||||
import { nonThreadCalendarQuery } from '../selectors/nav-selectors'; | 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 browser = detectBrowser(); | ||||
const exifRotate = | const exifRotate = | ||||
!browser || (browser.name !== 'safari' && browser.name !== 'chrome'); | !browser || (browser.name !== 'safari' && browser.name !== 'chrome'); | ||||
type BaseProps = { | type BaseProps = { | ||||
+children: React.Node, | +children: React.Node, | ||||
}; | }; | ||||
Show All 29 Lines | type Props = { | ||||
+registerSendCallback: (() => mixed) => void, | +registerSendCallback: (() => mixed) => void, | ||||
+unregisterSendCallback: (() => mixed) => void, | +unregisterSendCallback: (() => mixed) => void, | ||||
}; | }; | ||||
type State = { | type State = { | ||||
+pendingUploads: { | +pendingUploads: { | ||||
[threadID: string]: { [localUploadID: string]: PendingMultimediaUpload }, | [threadID: string]: { [localUploadID: string]: PendingMultimediaUpload }, | ||||
}, | }, | ||||
+textCursorPositions: { [threadID: string]: number }, | +textCursorPositions: { [threadID: string]: number }, | ||||
+typeaheadState: TypeaheadState, | |||||
}; | }; | ||||
type PropsAndState = { | type PropsAndState = { | ||||
...Props, | ...Props, | ||||
...State, | ...State, | ||||
}; | }; | ||||
class InputStateContainer extends React.PureComponent<Props, State> { | class InputStateContainer extends React.PureComponent<Props, State> { | ||||
state: State = { | state: State = { | ||||
pendingUploads: {}, | pendingUploads: {}, | ||||
textCursorPositions: {}, | textCursorPositions: {}, | ||||
typeaheadState: { | |||||
canBeVisible: false, | |||||
moveChoiceUp: null, | |||||
moveChoiceDown: null, | |||||
close: null, | |||||
accept: null, | |||||
}, | |||||
}; | }; | ||||
replyCallbacks: Array<(message: string) => void> = []; | replyCallbacks: Array<(message: string) => void> = []; | ||||
pendingThreadCreations = new Map<string, Promise<string>>(); | pendingThreadCreations = new Map<string, Promise<string>>(); | ||||
static reassignToRealizedThreads<T>( | static reassignToRealizedThreads<T>( | ||||
state: { +[threadID: string]: T }, | state: { +[threadID: string]: T }, | ||||
props: Props, | props: Props, | ||||
): ?{ [threadID: string]: T } { | ): ?{ [threadID: string]: T } { | ||||
▲ Show 20 Lines • Show All 308 Lines • ▼ Show 20 Lines | if (!threadCreationPromise) { | ||||
viewerID: this.props.viewerID, | viewerID: this.props.viewerID, | ||||
calendarQuery, | calendarQuery, | ||||
}); | }); | ||||
this.pendingThreadCreations.set(threadInfo.id, threadCreationPromise); | this.pendingThreadCreations.set(threadInfo.id, threadCreationPromise); | ||||
} | } | ||||
return threadCreationPromise; | return threadCreationPromise; | ||||
} | } | ||||
inputStateSelector = _memoize((threadID: string) => | inputBaseStateSelector = _memoize((threadID: string) => | ||||
createSelector( | createSelector( | ||||
(propsAndState: PropsAndState) => propsAndState.pendingUploads[threadID], | (propsAndState: PropsAndState) => propsAndState.pendingUploads[threadID], | ||||
(propsAndState: PropsAndState) => | (propsAndState: PropsAndState) => | ||||
propsAndState.drafts[draftKeyFromThreadID(threadID)], | propsAndState.drafts[draftKeyFromThreadID(threadID)], | ||||
(propsAndState: PropsAndState) => | (propsAndState: PropsAndState) => | ||||
propsAndState.textCursorPositions[threadID], | propsAndState.textCursorPositions[threadID], | ||||
( | ( | ||||
pendingUploads: ?{ [localUploadID: string]: PendingMultimediaUpload }, | pendingUploads: ?{ [localUploadID: string]: PendingMultimediaUpload }, | ||||
▲ Show 20 Lines • Show All 49 Lines • ▼ Show 20 Lines | createSelector( | ||||
removeReplyListener: this.removeReplyListener, | removeReplyListener: this.removeReplyListener, | ||||
registerSendCallback: this.props.registerSendCallback, | registerSendCallback: this.props.registerSendCallback, | ||||
unregisterSendCallback: this.props.unregisterSendCallback, | unregisterSendCallback: this.props.unregisterSendCallback, | ||||
}; | }; | ||||
}, | }, | ||||
), | ), | ||||
); | ); | ||||
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 { | getRealizedOrPendingThreadID(threadID: string): string { | ||||
return this.props.pendingRealizedThreadIDs.get(threadID) ?? threadID; | return this.props.pendingRealizedThreadIDs.get(threadID) ?? threadID; | ||||
} | } | ||||
async appendFiles( | async appendFiles( | ||||
threadID: string, | threadID: string, | ||||
files: $ReadOnlyArray<File>, | files: $ReadOnlyArray<File>, | ||||
): Promise<boolean> { | ): Promise<boolean> { | ||||
▲ Show 20 Lines • Show All 555 Lines • ▼ Show 20 Lines | this.setState(prevState => { | ||||
textCursorPositions: { | textCursorPositions: { | ||||
...prevState.textCursorPositions, | ...prevState.textCursorPositions, | ||||
[newThreadID]: newPosition, | [newThreadID]: newPosition, | ||||
}, | }, | ||||
}; | }; | ||||
}); | }); | ||||
} | } | ||||
setTypeaheadState = (newState: $Shape<TypeaheadState>) => { | |||||
this.setState(prevState => ({ | |||||
typeaheadState: { | |||||
...prevState.typeaheadState, | |||||
...newState, | |||||
}, | |||||
})); | |||||
}; | |||||
setProgress( | setProgress( | ||||
threadID: string, | threadID: string, | ||||
localUploadID: string, | localUploadID: string, | ||||
progressPercent: number, | progressPercent: number, | ||||
) { | ) { | ||||
this.setState(prevState => { | this.setState(prevState => { | ||||
const newThreadID = this.getRealizedOrPendingThreadID(threadID); | const newThreadID = this.getRealizedOrPendingThreadID(threadID); | ||||
const pendingUploads = prevState.pendingUploads[newThreadID]; | const pendingUploads = prevState.pendingUploads[newThreadID]; | ||||
▲ Show 20 Lines • Show All 129 Lines • ▼ Show 20 Lines | |||||
removeReplyListener = (callbackReply: (message: string) => void) => { | removeReplyListener = (callbackReply: (message: string) => void) => { | ||||
this.replyCallbacks = this.replyCallbacks.filter( | this.replyCallbacks = this.replyCallbacks.filter( | ||||
candidate => candidate !== callbackReply, | candidate => candidate !== callbackReply, | ||||
); | ); | ||||
}; | }; | ||||
render() { | render() { | ||||
const { activeChatThreadID } = this.props; | const { activeChatThreadID } = this.props; | ||||
const inputState = activeChatThreadID | |||||
? this.inputStateSelector(activeChatThreadID)({ | // 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.state, | ||||
...this.props, | ...this.props, | ||||
}) | }); | ||||
: null; | |||||
inputState = this.inputStateSelector({ | |||||
inputBaseState, | |||||
typeaheadState, | |||||
}); | |||||
} | |||||
return ( | return ( | ||||
<InputStateContext.Provider value={inputState}> | <InputStateContext.Provider value={inputState}> | ||||
{this.props.children} | {this.props.children} | ||||
</InputStateContext.Provider> | </InputStateContext.Provider> | ||||
); | ); | ||||
} | } | ||||
} | } | ||||
▲ Show 20 Lines • Show All 68 Lines • Show Last 20 Lines |