Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3508890
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
56 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js
index 7c242baa1..b5b199c31 100644
--- a/native/chat/chat-input-bar.react.js
+++ b/native/chat/chat-input-bar.react.js
@@ -1,1612 +1,1427 @@
// @flow
import Icon from '@expo/vector-icons/Ionicons.js';
import type { GenericNavigationAction } from '@react-navigation/core';
import invariant from 'invariant';
import _throttle from 'lodash/throttle.js';
import * as React from 'react';
import {
ActivityIndicator,
NativeAppEventEmitter,
Platform,
Text,
TextInput,
TouchableOpacity,
TouchableWithoutFeedback,
View,
} from 'react-native';
import { TextInputKeyboardMangerIOS } from 'react-native-keyboard-input';
import Animated, {
EasingNode,
FadeInDown,
FadeOutDown,
} from 'react-native-reanimated';
import {
moveDraftActionType,
updateDraftActionType,
} from 'lib/actions/draft-actions.js';
import {
joinThreadActionTypes,
newThreadActionTypes,
useJoinThread,
} from 'lib/actions/thread-actions.js';
-import type { UseJoinThreadInput } from 'lib/actions/thread-actions.js';
import {
useChatMentionContext,
useThreadChatMentionCandidates,
} from 'lib/hooks/chat-mention-hooks.js';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
import { colorIsDark } from 'lib/shared/color-utils.js';
import { useEditMessage } from 'lib/shared/edit-messages-utils.js';
import {
getTypeaheadRegexMatches,
type MentionTypeaheadSuggestionItem,
type Selection,
type TypeaheadMatchedStrings,
useMentionTypeaheadChatSuggestions,
useMentionTypeaheadUserSuggestions,
useUserMentionsCandidates,
} from 'lib/shared/mention-utils.js';
import {
messageKey,
- type MessagePreviewResult,
trimMessage,
useMessagePreview,
getNextLocalID,
} from 'lib/shared/message-utils.js';
-import SentencePrefixSearchIndex from 'lib/shared/sentence-prefix-search-index.js';
import {
checkIfDefaultMembersAreVoiced,
draftKeyFromThreadID,
threadActualMembers,
useThreadFrozenDueToViewerBlock,
useThreadHasPermission,
viewerIsMember,
} from 'lib/shared/thread-utils.js';
-import type { CalendarQuery } from 'lib/types/entry-types.js';
-import type { SetState } from 'lib/types/hook-types.js';
-import type { LoadingStatus } from 'lib/types/loading-types.js';
import type { PhotoPaste } from 'lib/types/media-types.js';
import { messageTypes } from 'lib/types/message-types-enum.js';
import type { MessageInfo } from 'lib/types/message-types.js';
-import type {
- RelativeMemberInfo,
- ThreadInfo,
- RawThreadInfo,
-} from 'lib/types/minimally-encoded-thread-permissions-types.js';
-import type { Dispatch } from 'lib/types/redux-types.js';
+import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js';
import { threadPermissions } from 'lib/types/thread-permission-types.js';
-import type {
- ChatMentionCandidates,
- ThreadJoinPayload,
-} from 'lib/types/thread-types.js';
-import {
- type DispatchActionPromise,
- useDispatchActionPromise,
-} from 'lib/utils/redux-promise-utils.js';
+import type { ThreadJoinPayload } from 'lib/types/thread-types.js';
+import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js';
import { useDispatch } from 'lib/utils/redux-utils.js';
import { ChatContext } from './chat-context.js';
import type { ChatNavigationProp } from './chat.react.js';
-import {
- MessageEditingContext,
- type MessageEditingContextType,
-} from './message-editing-context.react.js';
+import { MessageEditingContext } from './message-editing-context.react.js';
import type { RemoveEditMode } from './message-list-types.js';
import TypeaheadTooltip from './typeahead-tooltip.react.js';
import MentionTypeaheadTooltipButton from '../chat/mention-typeahead-tooltip-button.react.js';
import Button from '../components/button.react.js';
// eslint-disable-next-line import/extensions
import ClearableTextInput from '../components/clearable-text-input.react';
import type { SyncedSelectionData } from '../components/selectable-text-input.js';
// eslint-disable-next-line import/extensions
import SelectableTextInput from '../components/selectable-text-input.react';
import SingleLine from '../components/single-line.react.js';
import SWMansionIcon from '../components/swmansion-icon.react.js';
import {
type EditInputBarMessageParameters,
InputStateContext,
} from '../input/input-state.js';
import KeyboardInputHost from '../keyboard/keyboard-input-host.react.js';
-import {
- KeyboardContext,
- type KeyboardState,
-} from '../keyboard/keyboard-state.js';
+import { KeyboardContext } from '../keyboard/keyboard-state.js';
import { getKeyboardHeight } from '../keyboard/keyboard.js';
import { getDefaultTextMessageRules } from '../markdown/rules.react.js';
import {
activeThreadSelector,
nonThreadCalendarQuery,
} from '../navigation/nav-selectors.js';
import { NavContext } from '../navigation/navigation-context.js';
import { OverlayContext } from '../navigation/overlay-context.js';
import {
ChatCameraModalRouteName,
ImagePasteModalRouteName,
type NavigationRoute,
} from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
-import { type Colors, useColors, useStyles } from '../themes/colors.js';
+import { useColors, useStyles } from '../themes/colors.js';
import type { ImagePasteEvent, LayoutEvent } from '../types/react-native.js';
-import {
- AnimatedView,
- type AnimatedViewStyle,
- type ViewStyle,
-} from '../types/styles.js';
+import { AnimatedView, type ViewStyle } from '../types/styles.js';
import Alert from '../utils/alert.js';
import { runTiming } from '../utils/animation-utils.js';
import { exitEditAlert } from '../utils/edit-messages-utils.js';
import {
mentionTypeaheadTooltipActions,
nativeMentionTypeaheadRegex,
} from '../utils/typeahead-utils.js';
const {
Value,
Clock,
block,
set,
cond,
neq,
sub,
interpolateNode,
stopClock,
useValue,
} = Animated;
const expandoButtonsAnimationConfig = {
duration: 150,
easing: EasingNode.inOut(EasingNode.ease),
};
const sendButtonAnimationConfig = {
duration: 150,
easing: EasingNode.inOut(EasingNode.ease),
};
const unboundStyles = {
cameraIcon: {
paddingBottom: Platform.OS === 'android' ? 11 : 8,
paddingRight: 5,
},
cameraRollIcon: {
paddingBottom: Platform.OS === 'android' ? 11 : 8,
paddingRight: 5,
},
container: {
backgroundColor: 'listBackground',
paddingLeft: Platform.OS === 'android' ? 10 : 5,
},
expandButton: {
bottom: 0,
position: 'absolute',
right: 0,
},
expandIcon: {
paddingBottom: Platform.OS === 'android' ? 13 : 11,
paddingRight: 2,
},
expandoButtons: {
alignSelf: 'flex-end',
},
explanation: {
color: 'listBackgroundSecondaryLabel',
paddingBottom: 4,
paddingTop: 1,
textAlign: 'center',
},
innerExpandoButtons: {
alignItems: 'flex-end',
alignSelf: 'flex-end',
flexDirection: 'row',
},
inputContainer: {
flexDirection: 'row',
},
joinButton: {
borderRadius: 8,
flex: 1,
justifyContent: 'center',
marginHorizontal: 12,
marginVertical: 3,
},
joinButtonContainer: {
flexDirection: 'row',
height: 48,
marginBottom: 8,
},
editView: {
marginLeft: 20,
marginRight: 20,
padding: 10,
flexDirection: 'row',
justifyContent: 'space-between',
},
editViewContent: {
flex: 1,
paddingRight: 6,
},
exitEditButton: {
marginTop: 6,
},
editingLabel: {
paddingBottom: 4,
},
editingMessagePreview: {
color: 'listForegroundLabel',
},
joinButtonContent: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
joinButtonTextLight: {
color: 'white',
fontSize: 20,
marginHorizontal: 4,
},
joinButtonTextDark: {
color: 'black',
fontSize: 20,
marginHorizontal: 4,
},
joinThreadLoadingIndicator: {
paddingVertical: 2,
},
sendButton: {
position: 'absolute',
bottom: 4,
left: 0,
},
sendIcon: {
paddingLeft: 9,
paddingRight: 8,
paddingVertical: 6,
},
textInput: {
backgroundColor: 'listInputBackground',
borderRadius: 8,
color: 'listForegroundLabel',
fontSize: 16,
marginLeft: 4,
marginRight: 4,
marginTop: 6,
marginBottom: 8,
maxHeight: 110,
paddingHorizontal: 10,
paddingVertical: 5,
},
};
type BaseProps = {
+threadInfo: ThreadInfo,
};
-type Props = {
- ...BaseProps,
- +rawThreadInfo: RawThreadInfo,
- +draft: string,
- +joinThreadLoadingStatus: LoadingStatus,
- +threadCreationInProgress: boolean,
- +calendarQuery: () => CalendarQuery,
- +colors: Colors,
- +styles: $ReadOnly<typeof unboundStyles>,
- +onInputBarLayout?: (event: LayoutEvent) => mixed,
- +openCamera: () => mixed,
- +isActive: boolean,
- +keyboardState: ?KeyboardState,
- +dispatch: Dispatch,
- +dispatchActionPromise: DispatchActionPromise,
- +joinThread: (input: UseJoinThreadInput) => Promise<ThreadJoinPayload>,
- +userMentionsCandidates: $ReadOnlyArray<RelativeMemberInfo>,
- +chatMentionSearchIndex: ?SentencePrefixSearchIndex,
- +chatMentionCandidates: ChatMentionCandidates,
- +editedMessagePreview: ?MessagePreviewResult,
- +editedMessageInfo: ?MessageInfo,
- +navigation: ?ChatNavigationProp<'MessageList'>,
- +messageEditingContext: ?MessageEditingContextType,
- +selectionState: SyncedSelectionData,
- +setSelectionState: SetState<SyncedSelectionData>,
- +suggestions: $ReadOnlyArray<MentionTypeaheadSuggestionItem>,
- +typeaheadMatchedStrings: ?TypeaheadMatchedStrings,
- +currentUserCanJoin: boolean,
- +threadFrozen: boolean,
- +text: string,
- +setText: (text: string) => void,
- +textEdited: boolean,
- +buttonsExpanded: boolean,
- +expandoButtonsStyle: AnimatedViewStyle,
- +cameraRollIconStyle: AnimatedViewStyle,
- +cameraIconStyle: AnimatedViewStyle,
- +expandIconStyle: AnimatedViewStyle,
- +sendButtonContainerStyle: AnimatedViewStyle,
- +shouldShowTextInput: () => boolean,
- +isEditMode: () => boolean,
- +updateSendButton: (currentText: string) => void,
- +expandButtons: () => void,
- +hideButtons: () => void,
- +textInputRef: { current: ?React.ElementRef<typeof TextInput> },
- +clearableTextInputRef: { current: ?ClearableTextInput },
- +selectableTextInputRef: {
- current: ?React.ElementRef<typeof SelectableTextInput>,
- },
- +setTextInputRef: (ref: ?React.ElementRef<typeof TextInput>) => void,
- +setClearableTextInputRef: (ref: ?ClearableTextInput) => void,
- +addEditInputMessageListener: () => void,
- +removeEditInputMessageListener: () => void,
- +focusAndUpdateTextAndSelection: (
- newText: string,
- selection: Selection,
- ) => void,
- +scrollToEditedMessage: () => void,
- +onPressExitEditMode: () => void,
- +updateText: (newText: string) => void,
- +onSend: () => Promise<void>,
- +isMessageEdited: (newText?: string) => boolean,
- +blockNavigation: () => void,
- +onPressJoin: () => void,
- +setIOSKeyboardHeight: () => void,
- +showMediaGallery: () => void,
- +dismissKeyboard: () => void,
-};
-
-class ChatInputBar extends React.PureComponent<Props> {
- render(): React.Node {
- const isMember = viewerIsMember(this.props.threadInfo);
- let joinButton = null;
- const threadColor = `#${this.props.threadInfo.color}`;
- const isEditMode = this.props.isEditMode();
- if (
- !isMember &&
- this.props.currentUserCanJoin &&
- !this.props.threadCreationInProgress
- ) {
- let buttonContent;
- if (this.props.joinThreadLoadingStatus === 'loading') {
- buttonContent = (
- <ActivityIndicator
- size="small"
- color="white"
- style={this.props.styles.joinThreadLoadingIndicator}
- />
- );
- } else {
- const textStyle = colorIsDark(this.props.threadInfo.color)
- ? this.props.styles.joinButtonTextLight
- : this.props.styles.joinButtonTextDark;
- buttonContent = (
- <View style={this.props.styles.joinButtonContent}>
- <SWMansionIcon name="plus" style={textStyle} />
- <Text style={textStyle}>Join Chat</Text>
- </View>
- );
- }
- joinButton = (
- <View style={this.props.styles.joinButtonContainer}>
- <Button
- onPress={this.props.onPressJoin}
- iosActiveOpacity={0.85}
- style={[
- this.props.styles.joinButton,
- { backgroundColor: threadColor },
- ]}
- >
- {buttonContent}
- </Button>
- </View>
- );
- }
-
- let typeaheadTooltip = null;
-
- if (
- this.props.suggestions.length > 0 &&
- this.props.typeaheadMatchedStrings &&
- !isEditMode
- ) {
- typeaheadTooltip = (
- <TypeaheadTooltip
- text={this.props.text}
- matchedStrings={this.props.typeaheadMatchedStrings}
- suggestions={this.props.suggestions}
- focusAndUpdateTextAndSelection={
- this.props.focusAndUpdateTextAndSelection
- }
- typeaheadTooltipActionsGetter={mentionTypeaheadTooltipActions}
- TypeaheadTooltipButtonComponent={MentionTypeaheadTooltipButton}
- />
- );
- }
-
- let content;
- const defaultMembersAreVoiced = checkIfDefaultMembersAreVoiced(
- this.props.threadInfo,
- );
- if (this.props.shouldShowTextInput()) {
- content = this.renderInput();
- } else if (
- this.props.threadFrozen &&
- threadActualMembers(this.props.threadInfo.members).length === 2
- ) {
- content = (
- <Text style={this.props.styles.explanation}>
- You can’t send messages to a user that you’ve blocked.
- </Text>
- );
- } else if (isMember) {
- content = (
- <Text style={this.props.styles.explanation}>
- You don’t have permission to send messages.
- </Text>
- );
- } else if (defaultMembersAreVoiced && this.props.currentUserCanJoin) {
- content = null;
- } else {
- content = (
- <Text style={this.props.styles.explanation}>
- You don’t have permission to send messages.
- </Text>
- );
- }
-
- const keyboardInputHost =
- Platform.OS === 'android' ? null : (
- <KeyboardInputHost textInputRef={this.props.textInputRef.current} />
- );
-
- let editedMessage;
- if (isEditMode && this.props.editedMessagePreview) {
- const { message } = this.props.editedMessagePreview;
- editedMessage = (
- <AnimatedView
- style={this.props.styles.editView}
- entering={FadeInDown}
- exiting={FadeOutDown}
- >
- <View style={this.props.styles.editViewContent}>
- <TouchableOpacity
- onPress={this.props.scrollToEditedMessage}
- activeOpacity={0.4}
- >
- <Text
- style={[{ color: threadColor }, this.props.styles.editingLabel]}
- >
- Editing message
- </Text>
- <SingleLine style={this.props.styles.editingMessagePreview}>
- {message.text}
- </SingleLine>
- </TouchableOpacity>
- </View>
- <SWMansionIcon
- style={this.props.styles.exitEditButton}
- name="cross"
- size={22}
- color={threadColor}
- onPress={this.props.onPressExitEditMode}
- />
- </AnimatedView>
- );
- }
-
- return (
- <AnimatedView
- style={this.props.styles.container}
- onLayout={this.props.onInputBarLayout}
- >
- {typeaheadTooltip}
- {joinButton}
- {editedMessage}
- {content}
- {keyboardInputHost}
- </AnimatedView>
- );
- }
-
- renderInput(): React.Node {
- const expandoButton = (
- <TouchableOpacity
- onPress={this.props.expandButtons}
- activeOpacity={0.4}
- style={this.props.styles.expandButton}
- >
- <AnimatedView style={this.props.expandIconStyle}>
- <SWMansionIcon
- name="chevron-right"
- size={22}
- color={`#${this.props.threadInfo.color}`}
- />
- </AnimatedView>
- </TouchableOpacity>
- );
- const threadColor = `#${this.props.threadInfo.color}`;
- const expandoButtonsViewStyle: Array<ViewStyle> = [
- this.props.styles.innerExpandoButtons,
- ];
- if (this.props.isEditMode()) {
- expandoButtonsViewStyle.push({ display: 'none' });
- }
- return (
- <TouchableWithoutFeedback onPress={this.props.dismissKeyboard}>
- <View style={this.props.styles.inputContainer}>
- <AnimatedView style={this.props.expandoButtonsStyle}>
- <View style={expandoButtonsViewStyle}>
- {this.props.buttonsExpanded ? expandoButton : null}
- <TouchableOpacity
- onPress={this.props.showMediaGallery}
- activeOpacity={0.4}
- >
- <AnimatedView style={this.props.cameraRollIconStyle}>
- <SWMansionIcon
- name="image-1"
- size={28}
- color={`#${this.props.threadInfo.color}`}
- />
- </AnimatedView>
- </TouchableOpacity>
- <TouchableOpacity
- onPress={this.props.openCamera}
- activeOpacity={0.4}
- disabled={!this.props.buttonsExpanded}
- >
- <AnimatedView style={this.props.cameraIconStyle}>
- <SWMansionIcon
- name="camera"
- size={28}
- color={`#${this.props.threadInfo.color}`}
- />
- </AnimatedView>
- </TouchableOpacity>
- {this.props.buttonsExpanded ? null : expandoButton}
- </View>
- </AnimatedView>
- <SelectableTextInput
- allowImagePasteForThreadID={this.props.threadInfo.id}
- value={this.props.text}
- onChangeText={this.props.updateText}
- selection={this.props.selectionState.selection}
- onUpdateSyncedSelectionData={this.props.setSelectionState}
- placeholder="Send a message..."
- placeholderTextColor={this.props.colors.listInputButton}
- multiline={true}
- style={this.props.styles.textInput}
- textInputRef={this.props.setTextInputRef}
- clearableTextInputRef={this.props.setClearableTextInputRef}
- ref={this.props.selectableTextInputRef}
- selectionColor={`#${this.props.threadInfo.color}`}
- />
- <AnimatedView style={this.props.sendButtonContainerStyle}>
- <TouchableOpacity
- onPress={this.props.onSend}
- activeOpacity={0.4}
- style={this.props.styles.sendButton}
- disabled={trimMessage(this.props.text) === ''}
- >
- <Icon
- name="md-send"
- size={25}
- style={this.props.styles.sendIcon}
- color={threadColor}
- />
- </TouchableOpacity>
- </AnimatedView>
- </View>
- </TouchableWithoutFeedback>
- );
- }
-}
const joinThreadLoadingStatusSelector = createLoadingStatusSelector(
joinThreadActionTypes,
);
const createThreadLoadingStatusSelector =
createLoadingStatusSelector(newThreadActionTypes);
type ConnectedChatInputBarBaseProps = {
...BaseProps,
+onInputBarLayout?: (event: LayoutEvent) => mixed,
+openCamera: () => mixed,
+navigation?: ChatNavigationProp<'MessageList'>,
};
function ConnectedChatInputBarBase(props: ConnectedChatInputBarBaseProps) {
const navContext = React.useContext(NavContext);
const keyboardState = React.useContext(KeyboardContext);
const inputState = React.useContext(InputStateContext);
const overlayContext = React.useContext(OverlayContext);
const viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const draft = useSelector(
state =>
state.draftStore.drafts[draftKeyFromThreadID(props.threadInfo.id)] ?? '',
);
const joinThreadLoadingStatus = useSelector(joinThreadLoadingStatusSelector);
const createThreadLoadingStatus = useSelector(
createThreadLoadingStatusSelector,
);
const threadCreationInProgress = createThreadLoadingStatus === 'loading';
const calendarQuery = useSelector(state =>
nonThreadCalendarQuery({
redux: state,
navContext,
}),
);
const userInfos = useSelector(state => state.userStore.userInfos);
const styles = useStyles(unboundStyles);
const colors = useColors();
const isActive = React.useMemo(
() => props.threadInfo.id === activeThreadSelector(navContext),
[props.threadInfo.id, navContext],
);
const dispatch = useDispatch();
const dispatchActionPromise = useDispatchActionPromise();
const rawThreadInfo = useSelector(
state => state.threadStore.threadInfos[props.threadInfo.id],
);
const { getChatMentionSearchIndex } = useChatMentionContext();
const chatMentionSearchIndex = getChatMentionSearchIndex(props.threadInfo);
const { parentThreadID, community } = props.threadInfo;
const parentThreadInfo = useSelector(state =>
parentThreadID ? threadInfoSelector(state)[parentThreadID] : null,
);
const communityThreadInfo = useSelector(state =>
community ? threadInfoSelector(state)[community] : null,
);
const threadFrozen = useThreadFrozenDueToViewerBlock(
props.threadInfo,
communityThreadInfo,
viewerID,
userInfos,
);
const userMentionsCandidates = useUserMentionsCandidates(
props.threadInfo,
parentThreadInfo,
);
const chatMentionCandidates = useThreadChatMentionCandidates(
props.threadInfo,
);
const messageEditingContext = React.useContext(MessageEditingContext);
const editedMessageInfo = messageEditingContext?.editState.editedMessage;
const editedMessagePreview = useMessagePreview(
editedMessageInfo,
props.threadInfo,
getDefaultTextMessageRules(chatMentionCandidates).simpleMarkdownRules,
);
const editMessage = useEditMessage(props.threadInfo);
const [selectionState, setSelectionState] =
React.useState<SyncedSelectionData>({
text: draft,
selection: { start: 0, end: 0 },
});
const [text, setText] = React.useState(draft);
const [textEdited, setTextEdited] = React.useState(false);
const [buttonsExpanded, setButtonsExpanded] = React.useState(true);
const typeaheadRegexMatches = React.useMemo(
() =>
getTypeaheadRegexMatches(
selectionState.text,
selectionState.selection,
nativeMentionTypeaheadRegex,
),
[selectionState.text, selectionState.selection],
);
const typeaheadMatchedStrings: ?TypeaheadMatchedStrings =
React.useMemo(() => {
if (typeaheadRegexMatches === null) {
return null;
}
return {
textBeforeAtSymbol: typeaheadRegexMatches[1] ?? '',
query: typeaheadRegexMatches[4] ?? '',
};
}, [typeaheadRegexMatches]);
const suggestedUsers = useMentionTypeaheadUserSuggestions(
userMentionsCandidates,
typeaheadMatchedStrings,
);
const suggestedChats = useMentionTypeaheadChatSuggestions(
chatMentionSearchIndex,
chatMentionCandidates,
typeaheadMatchedStrings,
);
const suggestions: $ReadOnlyArray<MentionTypeaheadSuggestionItem> =
React.useMemo(
() => [...suggestedUsers, ...suggestedChats],
[suggestedUsers, suggestedChats],
);
const currentUserIsVoiced = useThreadHasPermission(
props.threadInfo,
threadPermissions.VOICED,
);
const currentUserCanJoin = useThreadHasPermission(
props.threadInfo,
threadPermissions.JOIN_THREAD,
);
const isExitingDuringEditModeRef = React.useRef(false);
const expandoButtonsOpen = useValue(1);
const targetExpandoButtonsOpen = useValue(1);
const initialSendButtonContainerOpen = trimMessage(draft) ? 1 : 0;
const sendButtonContainerOpen = useValue(initialSendButtonContainerOpen);
const targetSendButtonContainerOpen = useValue(
initialSendButtonContainerOpen,
);
const iconsOpacity = React.useMemo(() => {
const prevTargetExpandoButtonsOpen = new Value(1);
const expandoButtonClock = new Clock();
return block([
cond(neq(targetExpandoButtonsOpen, prevTargetExpandoButtonsOpen), [
stopClock(expandoButtonClock),
set(prevTargetExpandoButtonsOpen, targetExpandoButtonsOpen),
]),
cond(
neq(expandoButtonsOpen, targetExpandoButtonsOpen),
set(
expandoButtonsOpen,
runTiming(
expandoButtonClock,
expandoButtonsOpen,
targetExpandoButtonsOpen,
true,
expandoButtonsAnimationConfig,
),
),
),
expandoButtonsOpen,
]);
}, [expandoButtonsOpen, targetExpandoButtonsOpen]);
const expandoButtonsWidth = React.useMemo(
() =>
interpolateNode(iconsOpacity, {
inputRange: [0, 1],
outputRange: [26, 66],
}),
[iconsOpacity],
);
const expandOpacity = React.useMemo(
() => sub(1, iconsOpacity),
[iconsOpacity],
);
const sendButtonContainerWidth = React.useMemo(() => {
const prevTargetSendButtonContainerOpen = new Value(
initialSendButtonContainerOpen,
);
const sendButtonClock = new Clock();
const animatedSendButtonContainerOpen = block([
cond(
neq(targetSendButtonContainerOpen, prevTargetSendButtonContainerOpen),
[
stopClock(sendButtonClock),
set(prevTargetSendButtonContainerOpen, targetSendButtonContainerOpen),
],
),
cond(
neq(sendButtonContainerOpen, targetSendButtonContainerOpen),
set(
sendButtonContainerOpen,
runTiming(
sendButtonClock,
sendButtonContainerOpen,
targetSendButtonContainerOpen,
true,
sendButtonAnimationConfig,
),
),
),
sendButtonContainerOpen,
]);
return interpolateNode(animatedSendButtonContainerOpen, {
inputRange: [0, 1],
outputRange: [4, 38],
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sendButtonContainerOpen, targetSendButtonContainerOpen]);
const cameraRollIconStyle = React.useMemo(
() => ({
...unboundStyles.cameraRollIcon,
opacity: iconsOpacity,
}),
[iconsOpacity],
);
const cameraIconStyle = React.useMemo(
() => ({
...unboundStyles.cameraIcon,
opacity: iconsOpacity,
}),
[iconsOpacity],
);
const expandoButtonsStyle = React.useMemo(
() => ({
...unboundStyles.expandoButtons,
width: expandoButtonsWidth,
}),
[expandoButtonsWidth],
);
const expandIconStyle = React.useMemo(
() => ({
...unboundStyles.expandIcon,
opacity: expandOpacity,
}),
[expandOpacity],
);
const sendButtonContainerStyle = React.useMemo(
() => ({
width: sendButtonContainerWidth,
}),
[sendButtonContainerWidth],
);
const shouldShowTextInput = React.useCallback(() => {
if (currentUserIsVoiced) {
return true;
}
// If the thread is created by somebody else while the viewer is attempting
// to create it, the threadInfo might be modified in-place
// and won't list the viewer as a member,
// which will end up hiding the input.
// In this case, we will assume that our creation action
// will get translated into a join, and as long
// as members are voiced, we can show the input.
if (!threadCreationInProgress) {
return false;
}
return checkIfDefaultMembersAreVoiced(props.threadInfo);
}, [currentUserIsVoiced, props.threadInfo, threadCreationInProgress]);
const isEditMode = React.useCallback(() => {
const editState = messageEditingContext?.editState;
const isThisThread =
editState?.editedMessage?.threadID === props.threadInfo.id;
return editState?.editedMessage !== null && isThisThread;
}, [messageEditingContext?.editState, props.threadInfo.id]);
const immediatelyShowSendButton = React.useCallback(() => {
sendButtonContainerOpen.setValue(1);
targetSendButtonContainerOpen.setValue(1);
}, [sendButtonContainerOpen, targetSendButtonContainerOpen]);
const updateSendButton = React.useCallback(
(currentText: string) => {
const targetValue = currentText === '' ? 0 : 1;
targetSendButtonContainerOpen.setValue(targetValue);
if (!shouldShowTextInput()) {
sendButtonContainerOpen.setValue(targetValue);
}
},
[
sendButtonContainerOpen,
shouldShowTextInput,
targetSendButtonContainerOpen,
],
);
const expandButtons = React.useCallback(() => {
if (buttonsExpanded || isEditMode()) {
return;
}
targetExpandoButtonsOpen.setValue(1);
setButtonsExpanded(true);
}, [buttonsExpanded, isEditMode, targetExpandoButtonsOpen]);
const hideButtons = React.useCallback(() => {
if (
keyboardState?.mediaGalleryOpen ||
!keyboardState?.systemKeyboardShowing ||
!buttonsExpanded
) {
return;
}
targetExpandoButtonsOpen.setValue(0);
setButtonsExpanded(false);
}, [
buttonsExpanded,
keyboardState?.mediaGalleryOpen,
keyboardState?.systemKeyboardShowing,
targetExpandoButtonsOpen,
]);
const immediatelyHideButtons = React.useCallback(() => {
expandoButtonsOpen.setValue(0);
targetExpandoButtonsOpen.setValue(0);
setButtonsExpanded(false);
}, [expandoButtonsOpen, targetExpandoButtonsOpen]);
const textInputRef = React.useRef<?React.ElementRef<typeof TextInput>>();
const clearableTextInputRef = React.useRef<?ClearableTextInput>();
const selectableTextInputRef =
React.useRef<?React.ElementRef<typeof SelectableTextInput>>();
const setTextInputRef = React.useCallback(
(ref: ?React.ElementRef<typeof TextInput>) => {
textInputRef.current = ref;
},
[],
);
const setClearableTextInputRef = React.useCallback(
(ref: ?ClearableTextInput) => {
clearableTextInputRef.current = ref;
},
[],
);
const saveDraft = React.useMemo(
() =>
_throttle(newText => {
dispatch({
type: updateDraftActionType,
payload: {
key: draftKeyFromThreadID(props.threadInfo.id),
text: newText,
},
});
}, 400),
[dispatch, props.threadInfo.id],
);
const isMessageEdited = React.useCallback(
(newText?: string): boolean => {
let updatedText = newText ?? text;
updatedText = trimMessage(updatedText);
const originalText = editedMessageInfo?.text;
return updatedText !== originalText;
},
[editedMessageInfo?.text, text],
);
const updateText = React.useCallback(
(newText: string) => {
if (isExitingDuringEditModeRef.current) {
return;
}
setText(newText);
setTextEdited(true);
messageEditingContext?.setEditedMessageChanged(isMessageEdited(newText));
if (isEditMode()) {
return;
}
saveDraft(newText);
},
[isEditMode, isMessageEdited, messageEditingContext, saveDraft],
);
const focusAndUpdateButtonsVisibility = React.useCallback(() => {
const textInput = textInputRef.current;
if (!textInput) {
return;
}
immediatelyShowSendButton();
immediatelyHideButtons();
textInput.focus();
}, [immediatelyHideButtons, immediatelyShowSendButton]);
const unblockNavigation = React.useCallback(() => {
const { navigation } = props;
if (!navigation) {
return;
}
navigation.setParams({ removeEditMode: null });
}, [props]);
const removeEditMode: RemoveEditMode = React.useCallback(
action => {
const { navigation } = props;
if (!navigation || isExitingDuringEditModeRef.current) {
return 'ignore_action';
}
if (!isMessageEdited()) {
unblockNavigation();
return 'reduce_action';
}
const unblockAndDispatch = () => {
unblockNavigation();
navigation.dispatch(action);
};
const onContinueEditing = () => {
overlayContext?.resetScrollBlockingModalStatus();
};
exitEditAlert({
onDiscard: unblockAndDispatch,
onContinueEditing,
});
return 'ignore_action';
},
[isMessageEdited, overlayContext, props, unblockNavigation],
);
const blockNavigation = React.useCallback(() => {
const { navigation } = props;
if (!navigation || !navigation.isFocused()) {
return;
}
navigation.setParams({
removeEditMode: removeEditMode,
});
}, [props, removeEditMode]);
const exitEditMode = React.useCallback(() => {
messageEditingContext?.setEditedMessage(null, () => {
unblockNavigation();
updateText(draft);
focusAndUpdateButtonsVisibility();
updateSendButton(draft);
});
}, [
draft,
focusAndUpdateButtonsVisibility,
messageEditingContext,
unblockNavigation,
updateSendButton,
updateText,
]);
const editMessageInner = React.useCallback(
async (messageID: string, newText: string) => {
if (!isMessageEdited()) {
exitEditMode();
return;
}
newText = trimMessage(newText);
try {
await editMessage(messageID, newText);
exitEditMode();
} catch (error) {
Alert.alert(
'Couldn’t edit the message',
'Please try again later',
[{ text: 'OK' }],
{
cancelable: true,
},
);
}
},
[editMessage, exitEditMode, isMessageEdited],
);
const focusAndUpdateText = React.useCallback(
(params: EditInputBarMessageParameters) => {
const { message, mode } = params;
const currentText = text;
if (mode === 'replace') {
updateText(message);
} else if (!currentText.startsWith(message)) {
const prependedText = message.concat(currentText);
updateText(prependedText);
}
focusAndUpdateButtonsVisibility();
},
[focusAndUpdateButtonsVisibility, text, updateText],
);
const addEditInputMessageListener = React.useCallback(() => {
invariant(
inputState,
'inputState should be set in addEditInputMessageListener',
);
inputState.addEditInputMessageListener(focusAndUpdateText);
}, [focusAndUpdateText, inputState]);
const removeEditInputMessageListener = React.useCallback(() => {
invariant(
inputState,
'inputState should be set in removeEditInputMessageListener',
);
inputState.removeEditInputMessageListener(focusAndUpdateText);
}, [focusAndUpdateText, inputState]);
const focusAndUpdateTextAndSelection = React.useCallback(
(newText: string, selection: Selection) => {
selectableTextInputRef.current?.prepareForSelectionMutation(
newText,
selection,
);
setText(newText);
setTextEdited(true);
setSelectionState({ text: newText, selection });
saveDraft(newText);
focusAndUpdateButtonsVisibility();
},
[focusAndUpdateButtonsVisibility, saveDraft],
);
const getEditedMessage = React.useCallback((): ?MessageInfo => {
const editState = messageEditingContext?.editState;
return editState?.editedMessage;
}, [messageEditingContext?.editState]);
const onSend = React.useCallback(async () => {
if (!trimMessage(text)) {
return;
}
const editedMessage = getEditedMessage();
if (editedMessage && editedMessage.id) {
await editMessageInner(editedMessage.id, text);
return;
}
updateSendButton('');
const clearableTextInput = clearableTextInputRef.current;
invariant(
clearableTextInput,
'clearableTextInput should be sent in onSend',
);
let newText = await clearableTextInput.getValueAndReset();
newText = trimMessage(newText);
if (!newText) {
return;
}
const localID = getNextLocalID();
const creatorID = viewerID;
invariant(creatorID, 'should have viewer ID in order to send a message');
invariant(inputState, 'inputState should be set in ChatInputBar.onSend');
await inputState.sendTextMessage(
{
type: messageTypes.TEXT,
localID,
threadID: props.threadInfo.id,
text: newText,
creatorID,
time: Date.now(),
},
props.threadInfo,
parentThreadInfo,
);
}, [
editMessageInner,
getEditedMessage,
inputState,
parentThreadInfo,
props.threadInfo,
text,
updateSendButton,
viewerID,
]);
const onPressExitEditMode = React.useCallback(() => {
if (!isMessageEdited()) {
exitEditMode();
return;
}
exitEditAlert({
onDiscard: exitEditMode,
});
}, [exitEditMode, isMessageEdited]);
const scrollToEditedMessage = React.useCallback(() => {
const editedMessage = getEditedMessage();
if (!editedMessage) {
return;
}
const editedMessageKey = messageKey(editedMessage);
inputState?.scrollToMessage(editedMessageKey);
}, [getEditedMessage, inputState]);
const onNavigationFocus = React.useCallback(() => {
isExitingDuringEditModeRef.current = false;
}, []);
const onNavigationBlur = React.useCallback(() => {
if (!isEditMode()) {
return;
}
setText(draft);
isExitingDuringEditModeRef.current = true;
exitEditMode();
}, [draft, exitEditMode, isEditMode]);
const onNavigationBeforeRemove = React.useCallback(
(e: {
+data: { +action: GenericNavigationAction },
+preventDefault: () => void,
...
}) => {
if (!isEditMode()) {
return;
}
const { action } = e.data;
e.preventDefault();
const saveExit = () => {
messageEditingContext?.setEditedMessage(null, () => {
isExitingDuringEditModeRef.current = true;
props.navigation?.dispatch(action);
});
};
if (!isMessageEdited()) {
saveExit();
return;
}
exitEditAlert({
onDiscard: saveExit,
});
},
[isEditMode, isMessageEdited, messageEditingContext, props.navigation],
);
React.useEffect(() => {
if (isActive) {
addEditInputMessageListener();
}
return () => {
if (isActive) {
removeEditInputMessageListener();
}
};
}, [addEditInputMessageListener, isActive, removeEditInputMessageListener]);
React.useEffect(() => {
const { navigation } = props;
const clearBeforeRemoveListener = navigation?.addListener(
'beforeRemove',
onNavigationBeforeRemove,
);
const clearFocusListener = navigation?.addListener(
'focus',
onNavigationFocus,
);
const clearBlurListener = navigation?.addListener('blur', onNavigationBlur);
return () => {
clearBeforeRemoveListener?.();
clearFocusListener?.();
clearBlurListener?.();
};
}, [onNavigationBeforeRemove, onNavigationBlur, onNavigationFocus, props]);
const callJoinThread = useJoinThread();
const joinAction = React.useCallback(async (): Promise<ThreadJoinPayload> => {
let joinThreadInput;
if (rawThreadInfo.thick) {
joinThreadInput = {
thick: true,
rawThreadInfo: rawThreadInfo,
};
} else {
const query = calendarQuery();
joinThreadInput = {
thick: false,
threadID: props.threadInfo.id,
calendarQuery: {
startDate: query.startDate,
endDate: query.endDate,
filters: [
...query.filters,
{ type: 'threads', threadIDs: [props.threadInfo.id] },
],
},
};
}
return await callJoinThread(joinThreadInput);
}, [calendarQuery, callJoinThread, props.threadInfo.id, rawThreadInfo]);
const onPressJoin = React.useCallback(() => {
void dispatchActionPromise(joinThreadActionTypes, joinAction());
}, [dispatchActionPromise, joinAction]);
const setIOSKeyboardHeight = React.useCallback(() => {
if (Platform.OS !== 'ios') {
return;
}
const textInput = textInputRef.current;
if (!textInput) {
return;
}
const keyboardHeight = getKeyboardHeight();
if (keyboardHeight === null || keyboardHeight === undefined) {
return;
}
TextInputKeyboardMangerIOS.setKeyboardHeight(textInput, keyboardHeight);
}, []);
const showMediaGallery = React.useCallback(() => {
invariant(keyboardState, 'keyboardState should be initialized');
keyboardState.showMediaGallery(props.threadInfo);
}, [keyboardState, props.threadInfo]);
const dismissKeyboard = React.useCallback(() => {
keyboardState?.dismissKeyboard();
}, [keyboardState]);
const prevThreadInfoID = React.useRef<?string>();
const prevDraft = React.useRef<?string>();
React.useEffect(() => {
if (
textEdited &&
text &&
prevThreadInfoID.current &&
props.threadInfo.id !== prevThreadInfoID.current
) {
dispatch({
type: moveDraftActionType,
payload: {
oldKey: draftKeyFromThreadID(prevThreadInfoID.current),
newKey: draftKeyFromThreadID(props.threadInfo.id),
},
});
} else if (!textEdited && draft !== prevDraft.current) {
setText(draft);
}
prevThreadInfoID.current = props.threadInfo.id;
prevDraft.current = draft;
}, [dispatch, draft, props.threadInfo.id, text, textEdited]);
const prevIsActiveRef = React.useRef(isActive);
React.useEffect(() => {
if (isActive && !prevIsActiveRef.current) {
addEditInputMessageListener();
} else if (!isActive && prevIsActiveRef) {
removeEditInputMessageListener();
}
prevIsActiveRef.current = isActive;
}, [addEditInputMessageListener, isActive, removeEditInputMessageListener]);
const prevTextRef = React.useRef(text);
React.useEffect(() => {
const currentText = trimMessage(text);
const prevText = trimMessage(prevTextRef.current);
prevTextRef.current = currentText;
if (
(currentText === '' && prevText !== '') ||
(currentText !== '' && prevText === '')
) {
updateSendButton(currentText);
}
}, [text, updateSendButton]);
const systemKeyboardWasShowingRef = React.useRef<?boolean>();
React.useEffect(() => {
const systemKeyboardShowing = keyboardState?.systemKeyboardShowing;
if (systemKeyboardShowing && !systemKeyboardWasShowingRef.current) {
hideButtons();
} else if (!systemKeyboardShowing && systemKeyboardWasShowingRef.current) {
expandButtons();
}
systemKeyboardWasShowingRef.current = systemKeyboardShowing;
}, [expandButtons, hideButtons, keyboardState?.systemKeyboardShowing]);
const mediaGalleryWasOpenRef = React.useRef<?boolean>();
React.useEffect(() => {
const mediaGalleryOpen = keyboardState?.mediaGalleryOpen;
if (!mediaGalleryOpen && mediaGalleryWasOpenRef.current) {
hideButtons();
} else if (mediaGalleryOpen && !mediaGalleryWasOpenRef.current) {
expandButtons();
setIOSKeyboardHeight();
}
mediaGalleryWasOpenRef.current = mediaGalleryOpen;
}, [
expandButtons,
hideButtons,
keyboardState?.mediaGalleryOpen,
setIOSKeyboardHeight,
]);
const prevEditedMessage = React.useRef<?MessageInfo>();
React.useEffect(() => {
const editedMessage = messageEditingContext?.editState.editedMessage;
if (editedMessage && !prevEditedMessage.current) {
blockNavigation();
}
prevEditedMessage.current = editedMessage;
}, [blockNavigation, messageEditingContext?.editState.editedMessage]);
+ const renderInput = () => {
+ const expandoButton = (
+ <TouchableOpacity
+ onPress={expandButtons}
+ activeOpacity={0.4}
+ style={styles.expandButton}
+ >
+ <AnimatedView style={expandIconStyle}>
+ <SWMansionIcon
+ name="chevron-right"
+ size={22}
+ color={`#${props.threadInfo.color}`}
+ />
+ </AnimatedView>
+ </TouchableOpacity>
+ );
+ const threadColor = `#${props.threadInfo.color}`;
+ const expandoButtonsViewStyle: Array<ViewStyle> = [
+ styles.innerExpandoButtons,
+ ];
+ if (isEditMode()) {
+ expandoButtonsViewStyle.push({ display: 'none' });
+ }
+ return (
+ <TouchableWithoutFeedback onPress={dismissKeyboard}>
+ <View style={styles.inputContainer}>
+ <AnimatedView style={expandoButtonsStyle}>
+ <View style={expandoButtonsViewStyle}>
+ {buttonsExpanded ? expandoButton : null}
+ <TouchableOpacity onPress={showMediaGallery} activeOpacity={0.4}>
+ <AnimatedView style={cameraRollIconStyle}>
+ <SWMansionIcon
+ name="image-1"
+ size={28}
+ color={`#${props.threadInfo.color}`}
+ />
+ </AnimatedView>
+ </TouchableOpacity>
+ <TouchableOpacity
+ onPress={props.openCamera}
+ activeOpacity={0.4}
+ disabled={!buttonsExpanded}
+ >
+ <AnimatedView style={cameraIconStyle}>
+ <SWMansionIcon
+ name="camera"
+ size={28}
+ color={`#${props.threadInfo.color}`}
+ />
+ </AnimatedView>
+ </TouchableOpacity>
+ {buttonsExpanded ? null : expandoButton}
+ </View>
+ </AnimatedView>
+ <SelectableTextInput
+ allowImagePasteForThreadID={props.threadInfo.id}
+ value={text}
+ onChangeText={updateText}
+ selection={selectionState.selection}
+ onUpdateSyncedSelectionData={setSelectionState}
+ placeholder="Send a message..."
+ placeholderTextColor={colors.listInputButton}
+ multiline={true}
+ style={styles.textInput}
+ textInputRef={setTextInputRef}
+ clearableTextInputRef={setClearableTextInputRef}
+ ref={selectableTextInputRef}
+ selectionColor={`#${props.threadInfo.color}`}
+ />
+ <AnimatedView style={sendButtonContainerStyle}>
+ <TouchableOpacity
+ onPress={onSend}
+ activeOpacity={0.4}
+ style={styles.sendButton}
+ disabled={trimMessage(text) === ''}
+ >
+ <Icon
+ name="md-send"
+ size={25}
+ style={styles.sendIcon}
+ color={threadColor}
+ />
+ </TouchableOpacity>
+ </AnimatedView>
+ </View>
+ </TouchableWithoutFeedback>
+ );
+ };
+
+ const isMember = viewerIsMember(props.threadInfo);
+ let joinButton = null;
+ const threadColor = `#${props.threadInfo.color}`;
+
+ if (!isMember && currentUserCanJoin && !threadCreationInProgress) {
+ let buttonContent;
+ if (joinThreadLoadingStatus === 'loading') {
+ buttonContent = (
+ <ActivityIndicator
+ size="small"
+ color="white"
+ style={styles.joinThreadLoadingIndicator}
+ />
+ );
+ } else {
+ const textStyle = colorIsDark(props.threadInfo.color)
+ ? styles.joinButtonTextLight
+ : styles.joinButtonTextDark;
+ buttonContent = (
+ <View style={styles.joinButtonContent}>
+ <SWMansionIcon name="plus" style={textStyle} />
+ <Text style={textStyle}>Join Chat</Text>
+ </View>
+ );
+ }
+ joinButton = (
+ <View style={styles.joinButtonContainer}>
+ <Button
+ onPress={onPressJoin}
+ iosActiveOpacity={0.85}
+ style={[styles.joinButton, { backgroundColor: threadColor }]}
+ >
+ {buttonContent}
+ </Button>
+ </View>
+ );
+ }
+
+ let typeaheadTooltip = null;
+
+ if (suggestions.length > 0 && typeaheadMatchedStrings && !isEditMode()) {
+ typeaheadTooltip = (
+ <TypeaheadTooltip
+ text={text}
+ matchedStrings={typeaheadMatchedStrings}
+ suggestions={suggestions}
+ focusAndUpdateTextAndSelection={focusAndUpdateTextAndSelection}
+ typeaheadTooltipActionsGetter={mentionTypeaheadTooltipActions}
+ TypeaheadTooltipButtonComponent={MentionTypeaheadTooltipButton}
+ />
+ );
+ }
+
+ let content;
+ const defaultMembersAreVoiced = checkIfDefaultMembersAreVoiced(
+ props.threadInfo,
+ );
+ if (shouldShowTextInput()) {
+ content = renderInput();
+ } else if (
+ threadFrozen &&
+ threadActualMembers(props.threadInfo.members).length === 2
+ ) {
+ content = (
+ <Text style={styles.explanation}>
+ You can’t send messages to a user that you’ve blocked.
+ </Text>
+ );
+ } else if (isMember) {
+ content = (
+ <Text style={styles.explanation}>
+ You don’t have permission to send messages.
+ </Text>
+ );
+ } else if (defaultMembersAreVoiced && currentUserCanJoin) {
+ content = null;
+ } else {
+ content = (
+ <Text style={styles.explanation}>
+ You don’t have permission to send messages.
+ </Text>
+ );
+ }
+
+ const keyboardInputHost =
+ Platform.OS === 'android' ? null : (
+ <KeyboardInputHost textInputRef={textInputRef.current} />
+ );
+
+ let editedMessage;
+ if (isEditMode() && editedMessagePreview) {
+ const { message } = editedMessagePreview;
+ editedMessage = (
+ <AnimatedView
+ style={styles.editView}
+ entering={FadeInDown}
+ exiting={FadeOutDown}
+ >
+ <View style={styles.editViewContent}>
+ <TouchableOpacity onPress={scrollToEditedMessage} activeOpacity={0.4}>
+ <Text style={[{ color: threadColor }, styles.editingLabel]}>
+ Editing message
+ </Text>
+ <SingleLine style={styles.editingMessagePreview}>
+ {message.text}
+ </SingleLine>
+ </TouchableOpacity>
+ </View>
+ <SWMansionIcon
+ style={styles.exitEditButton}
+ name="cross"
+ size={22}
+ color={threadColor}
+ onPress={onPressExitEditMode}
+ />
+ </AnimatedView>
+ );
+ }
+
return (
- <ChatInputBar
- {...props}
- rawThreadInfo={rawThreadInfo}
- draft={draft}
- joinThreadLoadingStatus={joinThreadLoadingStatus}
- threadCreationInProgress={threadCreationInProgress}
- calendarQuery={calendarQuery}
- colors={colors}
- styles={styles}
- isActive={isActive}
- keyboardState={keyboardState}
- dispatch={dispatch}
- dispatchActionPromise={dispatchActionPromise}
- joinThread={callJoinThread}
- userMentionsCandidates={userMentionsCandidates}
- chatMentionSearchIndex={chatMentionSearchIndex}
- chatMentionCandidates={chatMentionCandidates}
- editedMessagePreview={editedMessagePreview}
- editedMessageInfo={editedMessageInfo}
- navigation={props.navigation}
- messageEditingContext={messageEditingContext}
- selectionState={selectionState}
- setSelectionState={setSelectionState}
- suggestions={suggestions}
- typeaheadMatchedStrings={typeaheadMatchedStrings}
- currentUserCanJoin={currentUserCanJoin}
- threadFrozen={threadFrozen}
- text={text}
- setText={setText}
- textEdited={textEdited}
- buttonsExpanded={buttonsExpanded}
- cameraRollIconStyle={cameraRollIconStyle}
- cameraIconStyle={cameraIconStyle}
- expandoButtonsStyle={expandoButtonsStyle}
- expandIconStyle={expandIconStyle}
- sendButtonContainerStyle={sendButtonContainerStyle}
- shouldShowTextInput={shouldShowTextInput}
- isEditMode={isEditMode}
- updateSendButton={updateSendButton}
- expandButtons={expandButtons}
- hideButtons={hideButtons}
- textInputRef={textInputRef}
- clearableTextInputRef={clearableTextInputRef}
- selectableTextInputRef={selectableTextInputRef}
- setTextInputRef={setTextInputRef}
- setClearableTextInputRef={setClearableTextInputRef}
- addEditInputMessageListener={addEditInputMessageListener}
- removeEditInputMessageListener={removeEditInputMessageListener}
- focusAndUpdateTextAndSelection={focusAndUpdateTextAndSelection}
- scrollToEditedMessage={scrollToEditedMessage}
- onPressExitEditMode={onPressExitEditMode}
- updateText={updateText}
- onSend={onSend}
- isMessageEdited={isMessageEdited}
- blockNavigation={blockNavigation}
- onPressJoin={onPressJoin}
- setIOSKeyboardHeight={setIOSKeyboardHeight}
- showMediaGallery={showMediaGallery}
- dismissKeyboard={dismissKeyboard}
- />
+ <AnimatedView style={styles.container} onLayout={props.onInputBarLayout}>
+ {typeaheadTooltip}
+ {joinButton}
+ {editedMessage}
+ {content}
+ {keyboardInputHost}
+ </AnimatedView>
);
}
type DummyChatInputBarProps = {
...BaseProps,
+onHeightMeasured: (height: number) => mixed,
};
const noop = () => {};
function DummyChatInputBar(props: DummyChatInputBarProps): React.Node {
const { onHeightMeasured, ...restProps } = props;
const onInputBarLayout = React.useCallback(
(event: LayoutEvent) => {
const { height } = event.nativeEvent.layout;
onHeightMeasured(height);
},
[onHeightMeasured],
);
return (
<View pointerEvents="none">
<ConnectedChatInputBarBase
{...restProps}
onInputBarLayout={onInputBarLayout}
openCamera={noop}
/>
</View>
);
}
type ChatInputBarProps = {
...BaseProps,
+navigation: ChatNavigationProp<'MessageList'>,
+route: NavigationRoute<'MessageList'>,
};
const ConnectedChatInputBar: React.ComponentType<ChatInputBarProps> =
React.memo<ChatInputBarProps>(function ConnectedChatInputBar(
props: ChatInputBarProps,
) {
const { navigation, route, ...restProps } = props;
const keyboardState = React.useContext(KeyboardContext);
const { threadInfo } = props;
const imagePastedCallback = React.useCallback(
(imagePastedEvent: ImagePasteEvent) => {
if (threadInfo.id !== imagePastedEvent.threadID) {
return;
}
const pastedImage: PhotoPaste = {
step: 'photo_paste',
dimensions: {
height: imagePastedEvent.height,
width: imagePastedEvent.width,
},
filename: imagePastedEvent.fileName,
uri: 'file://' + imagePastedEvent.filePath,
selectTime: 0,
sendTime: 0,
retries: 0,
};
navigation.navigate<'ImagePasteModal'>({
name: ImagePasteModalRouteName,
params: {
imagePasteStagingInfo: pastedImage,
thread: threadInfo,
},
});
},
[navigation, threadInfo],
);
React.useEffect(() => {
const imagePasteListener = NativeAppEventEmitter.addListener(
'imagePasted',
imagePastedCallback,
);
return () => imagePasteListener.remove();
}, [imagePastedCallback]);
const chatContext = React.useContext(ChatContext);
invariant(chatContext, 'should be set');
const { setChatInputBarHeight, deleteChatInputBarHeight } = chatContext;
const onInputBarLayout = React.useCallback(
(event: LayoutEvent) => {
const { height } = event.nativeEvent.layout;
setChatInputBarHeight(threadInfo.id, height);
},
[threadInfo.id, setChatInputBarHeight],
);
React.useEffect(() => {
return () => {
deleteChatInputBarHeight(threadInfo.id);
};
}, [deleteChatInputBarHeight, threadInfo.id]);
const openCamera = React.useCallback(() => {
keyboardState?.dismissKeyboard();
navigation.navigate<'ChatCameraModal'>({
name: ChatCameraModalRouteName,
params: {
presentedFrom: route.key,
thread: threadInfo,
},
});
}, [keyboardState, navigation, route.key, threadInfo]);
return (
<ConnectedChatInputBarBase
{...restProps}
onInputBarLayout={onInputBarLayout}
openCamera={openCamera}
navigation={navigation}
/>
);
});
export { ConnectedChatInputBar as ChatInputBar, DummyChatInputBar };
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 12:18 AM (2 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2689978
Default Alt Text
(56 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment