Changeset View
Changeset View
Standalone View
Standalone View
native/chat/chat-input-bar.react.js
Show First 20 Lines • Show All 63 Lines • ▼ Show 20 Lines | |||||
import { | import { | ||||
type DispatchActionPromise, | type DispatchActionPromise, | ||||
useServerCall, | useServerCall, | ||||
useDispatchActionPromise, | useDispatchActionPromise, | ||||
} from 'lib/utils/action-utils'; | } from 'lib/utils/action-utils'; | ||||
import Button from '../components/button.react'; | import Button from '../components/button.react'; | ||||
import ClearableTextInput from '../components/clearable-text-input.react'; | import ClearableTextInput from '../components/clearable-text-input.react'; | ||||
import type { SyncedSelectionData } from '../components/selectable-text-input'; | |||||
import SelectableTextInput from '../components/selectable-text-input.react'; | |||||
import SWMansionIcon from '../components/swmansion-icon.react'; | import SWMansionIcon from '../components/swmansion-icon.react'; | ||||
import { type InputState, InputStateContext } from '../input/input-state'; | import { type InputState, InputStateContext } from '../input/input-state'; | ||||
import { getKeyboardHeight } from '../keyboard/keyboard'; | import { getKeyboardHeight } from '../keyboard/keyboard'; | ||||
import KeyboardInputHost from '../keyboard/keyboard-input-host.react'; | import KeyboardInputHost from '../keyboard/keyboard-input-host.react'; | ||||
import { | import { | ||||
type KeyboardState, | type KeyboardState, | ||||
KeyboardContext, | KeyboardContext, | ||||
} from '../keyboard/keyboard-state'; | } from '../keyboard/keyboard-state'; | ||||
import { | import { | ||||
nonThreadCalendarQuery, | nonThreadCalendarQuery, | ||||
activeThreadSelector, | activeThreadSelector, | ||||
} from '../navigation/nav-selectors'; | } from '../navigation/nav-selectors'; | ||||
import { NavContext } from '../navigation/navigation-context'; | import { NavContext } from '../navigation/navigation-context'; | ||||
import { | import { | ||||
type NavigationRoute, | type NavigationRoute, | ||||
CameraModalRouteName, | CameraModalRouteName, | ||||
ImagePasteModalRouteName, | ImagePasteModalRouteName, | ||||
} from '../navigation/route-names'; | } from '../navigation/route-names'; | ||||
import { useSelector } from '../redux/redux-utils'; | import { useSelector } from '../redux/redux-utils'; | ||||
import { type Colors, useStyles, useColors } from '../themes/colors'; | import { type Colors, useStyles, useColors } from '../themes/colors'; | ||||
import type { LayoutEvent, SelectionChangeEvent } from '../types/react-native'; | import type { LayoutEvent } from '../types/react-native'; | ||||
import { type AnimatedViewStyle, AnimatedView } from '../types/styles'; | import { type AnimatedViewStyle, AnimatedView } from '../types/styles'; | ||||
import { runTiming } from '../utils/animation-utils'; | import { runTiming } from '../utils/animation-utils'; | ||||
import { nativeTypeaheadRegex } from '../utils/typeahead-utils'; | import { nativeTypeaheadRegex } from '../utils/typeahead-utils'; | ||||
import { ChatContext } from './chat-context'; | import { ChatContext } from './chat-context'; | ||||
import type { ChatNavigationProp } from './chat.react'; | import type { ChatNavigationProp } from './chat.react'; | ||||
import TypeaheadTooltip from './typeahead-tooltip.react'; | import TypeaheadTooltip from './typeahead-tooltip.react'; | ||||
/* eslint-disable import/no-named-as-default-member */ | /* eslint-disable import/no-named-as-default-member */ | ||||
▲ Show 20 Lines • Show All 49 Lines • ▼ Show 20 Lines | type Props = { | ||||
+inputState: ?InputState, | +inputState: ?InputState, | ||||
+userSearchIndex: SearchIndex, | +userSearchIndex: SearchIndex, | ||||
+threadMembers: $ReadOnlyArray<RelativeMemberInfo>, | +threadMembers: $ReadOnlyArray<RelativeMemberInfo>, | ||||
}; | }; | ||||
type State = { | type State = { | ||||
+text: string, | +text: string, | ||||
+textEdited: boolean, | +textEdited: boolean, | ||||
+buttonsExpanded: boolean, | +buttonsExpanded: boolean, | ||||
+selection: Selection, | +selectionState: SyncedSelectionData, | ||||
}; | }; | ||||
class ChatInputBar extends React.PureComponent<Props, State> { | class ChatInputBar extends React.PureComponent<Props, State> { | ||||
textInput: ?React.ElementRef<typeof TextInput>; | textInput: ?React.ElementRef<typeof TextInput>; | ||||
clearableTextInput: ?ClearableTextInput; | clearableTextInput: ?ClearableTextInput; | ||||
selectableTextInput: ?React.ElementRef<typeof SelectableTextInput>; | |||||
expandoButtonsOpen: Value; | expandoButtonsOpen: Value; | ||||
targetExpandoButtonsOpen: Value; | targetExpandoButtonsOpen: Value; | ||||
expandoButtonsStyle: AnimatedViewStyle; | expandoButtonsStyle: AnimatedViewStyle; | ||||
cameraRollIconStyle: AnimatedViewStyle; | cameraRollIconStyle: AnimatedViewStyle; | ||||
cameraIconStyle: AnimatedViewStyle; | cameraIconStyle: AnimatedViewStyle; | ||||
expandIconStyle: AnimatedViewStyle; | expandIconStyle: AnimatedViewStyle; | ||||
sendButtonContainerOpen: Value; | sendButtonContainerOpen: Value; | ||||
targetSendButtonContainerOpen: Value; | targetSendButtonContainerOpen: Value; | ||||
sendButtonContainerStyle: AnimatedViewStyle; | sendButtonContainerStyle: AnimatedViewStyle; | ||||
constructor(props: Props) { | constructor(props: Props) { | ||||
super(props); | super(props); | ||||
this.state = { | this.state = { | ||||
text: props.draft, | text: props.draft, | ||||
textEdited: false, | textEdited: false, | ||||
buttonsExpanded: true, | buttonsExpanded: true, | ||||
selection: { start: 0, end: 0 }, | selectionState: { text: props.draft, selection: { start: 0, end: 0 } }, | ||||
}; | }; | ||||
this.setUpActionIconAnimations(); | this.setUpActionIconAnimations(); | ||||
this.setUpSendIconAnimations(); | this.setUpSendIconAnimations(); | ||||
} | } | ||||
setUpActionIconAnimations() { | setUpActionIconAnimations() { | ||||
this.expandoButtonsOpen = new Value(1); | this.expandoButtonsOpen = new Value(1); | ||||
▲ Show 20 Lines • Show All 276 Lines • ▼ Show 20 Lines | if (!isMember && canJoin && !this.props.threadCreationInProgress) { | ||||
> | > | ||||
{buttonContent} | {buttonContent} | ||||
</Button> | </Button> | ||||
</View> | </View> | ||||
); | ); | ||||
} | } | ||||
const typeaheadRegexMatches = getTypeaheadRegexMatches( | const typeaheadRegexMatches = getTypeaheadRegexMatches( | ||||
this.state.text, | this.state.selectionState.text, | ||||
this.state.selection, | this.state.selectionState.selection, | ||||
nativeTypeaheadRegex, | nativeTypeaheadRegex, | ||||
); | ); | ||||
let typeaheadTooltip = null; | let typeaheadTooltip = null; | ||||
if (typeaheadRegexMatches) { | if (typeaheadRegexMatches) { | ||||
const typeaheadMatchedStrings = { | const typeaheadMatchedStrings = { | ||||
textBeforeAtSymbol: typeaheadRegexMatches[1] ?? '', | textBeforeAtSymbol: typeaheadRegexMatches[1] ?? '', | ||||
▲ Show 20 Lines • Show All 118 Lines • ▼ Show 20 Lines | return ( | ||||
size={28} | size={28} | ||||
color={`#${this.props.threadInfo.color}`} | color={`#${this.props.threadInfo.color}`} | ||||
/> | /> | ||||
</AnimatedView> | </AnimatedView> | ||||
</TouchableOpacity> | </TouchableOpacity> | ||||
{this.state.buttonsExpanded ? null : expandoButton} | {this.state.buttonsExpanded ? null : expandoButton} | ||||
</View> | </View> | ||||
</AnimatedView> | </AnimatedView> | ||||
<ClearableTextInput | <SelectableTextInput | ||||
allowImagePasteForThreadID={this.props.threadInfo.id} | allowImagePasteForThreadID={this.props.threadInfo.id} | ||||
value={this.state.text} | value={this.state.text} | ||||
onChangeText={this.updateText} | onChangeText={this.updateText} | ||||
selection={this.state.selection} | selection={this.state.selectionState.selection} | ||||
onSelectionChange={this.updateSelection} | onUpdateSyncedSelectionData={this.updateSelectionState} | ||||
placeholder="Send a message..." | placeholder="Send a message..." | ||||
placeholderTextColor={this.props.colors.listInputButton} | placeholderTextColor={this.props.colors.listInputButton} | ||||
multiline={true} | multiline={true} | ||||
style={this.props.styles.textInput} | style={this.props.styles.textInput} | ||||
textInputRef={this.textInputRef} | textInputRef={this.textInputRef} | ||||
ref={this.clearableTextInputRef} | clearableTextInputRef={this.clearableTextInputRef} | ||||
ref={this.selectableTextInputRef} | |||||
selectionColor={`#${this.props.threadInfo.color}`} | selectionColor={`#${this.props.threadInfo.color}`} | ||||
/> | /> | ||||
<AnimatedView style={this.sendButtonContainerStyle}> | <AnimatedView style={this.sendButtonContainerStyle}> | ||||
<TouchableOpacity | <TouchableOpacity | ||||
onPress={this.onSend} | onPress={this.onSend} | ||||
activeOpacity={0.4} | activeOpacity={0.4} | ||||
style={this.props.styles.sendButton} | style={this.props.styles.sendButton} | ||||
disabled={trimMessage(this.state.text) === ''} | disabled={trimMessage(this.state.text) === ''} | ||||
Show All 14 Lines | class ChatInputBar extends React.PureComponent<Props, State> { | ||||
textInputRef = (textInput: ?React.ElementRef<typeof TextInput>) => { | textInputRef = (textInput: ?React.ElementRef<typeof TextInput>) => { | ||||
this.textInput = textInput; | this.textInput = textInput; | ||||
}; | }; | ||||
clearableTextInputRef = (clearableTextInput: ?ClearableTextInput) => { | clearableTextInputRef = (clearableTextInput: ?ClearableTextInput) => { | ||||
this.clearableTextInput = clearableTextInput; | this.clearableTextInput = clearableTextInput; | ||||
}; | }; | ||||
selectableTextInputRef = ( | |||||
selectableTextInput: ?React.ElementRef<typeof SelectableTextInput>, | |||||
) => { | |||||
this.selectableTextInput = selectableTextInput; | |||||
}; | |||||
updateText = (text: string) => { | updateText = (text: string) => { | ||||
this.setState({ text, textEdited: true }); | this.setState({ text, textEdited: true }); | ||||
this.saveDraft(text); | this.saveDraft(text); | ||||
}; | }; | ||||
updateSelection: (event: SelectionChangeEvent) => void = event => { | updateSelectionState: (data: SyncedSelectionData) => void = data => { | ||||
this.setState({ selection: event.nativeEvent.selection }); | this.setState({ selectionState: data }); | ||||
}; | }; | ||||
saveDraft = _throttle(text => { | saveDraft = _throttle(text => { | ||||
this.props.dispatch({ | this.props.dispatch({ | ||||
type: updateDraftActionType, | type: updateDraftActionType, | ||||
payload: { | payload: { | ||||
key: draftKeyFromThreadID(this.props.threadInfo.id), | key: draftKeyFromThreadID(this.props.threadInfo.id), | ||||
text, | text, | ||||
}, | }, | ||||
}); | }); | ||||
}, 400); | }, 400); | ||||
focusAndUpdateTextAndSelection = (text: string, selection: Selection) => { | focusAndUpdateTextAndSelection = (text: string, selection: Selection) => { | ||||
this.selectableTextInput?.prepareForSelectionMutation(text, selection); | |||||
this.setState({ | this.setState({ | ||||
text, | text, | ||||
textEdited: true, | textEdited: true, | ||||
selection, | selectionState: { text, selection }, | ||||
}); | }); | ||||
this.saveDraft(text); | this.saveDraft(text); | ||||
this.focusAndUpdateButtonsVisibility(); | this.focusAndUpdateButtonsVisibility(); | ||||
}; | }; | ||||
focusAndUpdateText = (text: string) => { | focusAndUpdateText = (text: string) => { | ||||
const currentText = this.state.text; | const currentText = this.state.text; | ||||
▲ Show 20 Lines • Show All 398 Lines • Show Last 20 Lines |