import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { I18nManager, Platform, TextInput as RNTextInput, StyleSheet, TextInputProps, TextInputSelectionChangeEvent, } from 'react-native'; import Animated, { LinearTransition } from 'react-native-reanimated'; import { MessageComposerConfig, TextComposerState } from 'stream-chat'; import { ChannelContextValue, useChannelContext, } from '../../contexts/channelContext/ChannelContext'; import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer'; import { MessageInputContextValue, useMessageInputContext, } from '../../contexts/messageInputContext/MessageInputContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { TranslationContextValue, useTranslationContext, } from '../../contexts/translationContext/TranslationContext'; import { useStateStore } from '../../hooks/useStateStore'; import { useCooldownRemaining } from '../MessageInput/hooks/useCooldownRemaining'; export type TextInputOverrideComponent = | typeof RNTextInput | React.ComponentClass | React.ForwardRefExoticComponent>; type AnimatedTextInputRendererProps = TextInputProps & { TextInputComponent: TextInputOverrideComponent; }; const TextInputRenderer = React.forwardRef( ({ TextInputComponent: Component, ...props }, ref) => , ); const AnimatedTextInputRenderer = Animated.createAnimatedComponent(TextInputRenderer); type AutoCompleteInputPropsWithContext = TextInputProps & Pick & Pick & Pick & { /** * This is currently passed in from MessageComposer to avoid rerenders * that would happen if we put this in the MessageInputContext */ cooldownRemainingSeconds?: number; TextInputComponent?: TextInputOverrideComponent; }; type AutoCompleteInputProps = Partial; const textComposerStateSelector = (state: TextComposerState) => ({ command: state.command, text: state.text, }); const configStateSelector = (state: MessageComposerConfig) => ({ enabled: state.text.enabled, }); const MAX_NUMBER_OF_LINES = 5; const LINE_HEIGHT = 20; const INPUT_VERTICAL_PADDING = Platform.OS === 'ios' ? 7 : 12; const commandPlaceHolders: Record = { giphy: 'Search GIFs', ban: '@username', unban: '@username', mute: '@username', unmute: '@username', }; const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext) => { const styles = useStyles(); const { channel, cooldownRemainingSeconds, setInputBoxRef, t, TextInputComponent = RNTextInput, placeholder, ...rest } = props; const [localText, setLocalText] = useState(''); const messageComposer = useMessageComposer(); const { textComposer } = messageComposer; const { command, text } = useStateStore(textComposer.state, textComposerStateSelector); const { enabled } = useStateStore(messageComposer.configState, configStateSelector); const maxMessageLength = useMemo(() => { return channel.getConfig()?.max_message_length; }, [channel]); const numberOfLines = useMemo(() => { return props.numberOfLines ?? MAX_NUMBER_OF_LINES; }, [props.numberOfLines]); useEffect(() => { setLocalText(text); }, [text]); const handleSelectionChange = useCallback( (e: TextInputSelectionChangeEvent) => { const { selection } = e.nativeEvent; textComposer.setSelection(selection); }, [textComposer], ); const onChangeTextHandler = useCallback( (newText: string) => { setLocalText(newText); textComposer.handleChange({ selection: { end: newText.length, start: newText.length, }, text: newText, }); }, [textComposer], ); const { theme: { messageComposer: { inputBox }, semantics, }, } = useTheme(); const placeholderText = useMemo(() => { return placeholder ? placeholder : command ? commandPlaceHolders[command.name ?? ''] : cooldownRemainingSeconds ? t('Slow mode, wait {{seconds}}s...', { seconds: cooldownRemainingSeconds }) : t('Send a message'); }, [command, cooldownRemainingSeconds, t, placeholder]); return ( ); }; const areEqual = ( prevProps: AutoCompleteInputPropsWithContext, nextProps: AutoCompleteInputPropsWithContext, ) => { const { channel: prevChannel, cooldownRemainingSeconds: prevCooldownRemainingSeconds, t: prevT, } = prevProps; const { channel: nextChannel, cooldownRemainingSeconds: nextCooldownRemainingSeconds, t: nextT, } = nextProps; const tEqual = prevT === nextT; if (!tEqual) { return false; } const cooldownRemainingSecondsEqual = prevCooldownRemainingSeconds === nextCooldownRemainingSeconds; if (!cooldownRemainingSecondsEqual) { return false; } const channelEqual = prevChannel.cid === nextChannel.cid; if (!channelEqual) { return false; } return true; }; const MemoizedAutoCompleteInput = React.memo( AutoCompleteInputWithContext, areEqual, ) as typeof AutoCompleteInputWithContext; export const AutoCompleteInput = (props: AutoCompleteInputProps) => { const { setInputBoxRef } = useMessageInputContext(); const { t } = useTranslationContext(); const { channel } = useChannelContext(); const cooldownRemainingSeconds = useCooldownRemaining(); return ( ); }; const useStyles = () => { const { theme: { semantics }, } = useTheme(); return useMemo(() => { return StyleSheet.create({ inputBox: { color: semantics.inputTextDefault, flex: 1, fontSize: 16, includeFontPadding: false, // for android vertical text centering lineHeight: 20, paddingLeft: 16, paddingVertical: 12, textAlignVertical: 'center', // for android vertical text centering alignSelf: 'center', }, }); }, [semantics]); }; AutoCompleteInput.displayName = 'AutoCompleteInput{messageComposer{inputBox}}';