diff --git a/app/containers/MessageComposer/MessageComposer.tsx b/app/containers/MessageComposer/MessageComposer.tsx index 6805a659507..0e881a59276 100644 --- a/app/containers/MessageComposer/MessageComposer.tsx +++ b/app/containers/MessageComposer/MessageComposer.tsx @@ -85,7 +85,7 @@ export const MessageComposer = ({ const closeEmojiKeyboardAndAction = (onClosed?: Function, params?: any) => { resetKeyboard(); - onClosed && onClosed(params); + onClosed?.(params); }; useImperativeHandle(forwardedRef, () => ({ diff --git a/app/containers/MessageComposer/MessageComposerContainer.tsx b/app/containers/MessageComposer/MessageComposerContainer.tsx index b24608a61b8..eb4645d384e 100644 --- a/app/containers/MessageComposer/MessageComposerContainer.tsx +++ b/app/containers/MessageComposer/MessageComposerContainer.tsx @@ -6,8 +6,10 @@ import { MessageComposer } from './MessageComposer'; import { EmojiKeyboardProvider } from './hooks/useEmojiKeyboard'; import { ComposerAttachments } from './components/Attachments/ComposerAttachments'; +const defaultChildren = ; + export const MessageComposerContainer = forwardRef( - ({ children = }, ref): ReactElement => { + ({ children = defaultChildren }, ref): ReactElement => { return ( diff --git a/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.ts b/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.ts index 30dadfa9d1f..ee547191239 100644 --- a/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.ts +++ b/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.ts @@ -4,6 +4,8 @@ import { AccessibilityInfo } from 'react-native'; import I18n from '../../../../i18n'; import { useIsAutocompleteVisible } from '../../../../views/RoomView/stores/ComposerStore'; +const DELAY_TO_AVOID_KEYBOARD_ANNOUNCEMENT_CONFLICT = 800; + export const useAutocompleteA11yAnnounce = (): void => { const isAutocompleteVisible = useIsAutocompleteVisible(); @@ -12,10 +14,9 @@ export const useAutocompleteA11yAnnounce = (): void => { return; } - // timeout to prevent conflict with default keyboard announcement. const timeout = setTimeout(() => { AccessibilityInfo.announceForAccessibility(I18n.t('The_autocomplete_options_are_available_above_the_input_composer')); - }, 800); + }, DELAY_TO_AVOID_KEYBOARD_ANNOUNCEMENT_CONFLICT); return () => clearTimeout(timeout); }, [isAutocompleteVisible]); diff --git a/app/containers/MessageComposer/components/CancelEdit.tsx b/app/containers/MessageComposer/components/CancelEdit.tsx index f99ce062124..a065bfbe1fe 100644 --- a/app/containers/MessageComposer/components/CancelEdit.tsx +++ b/app/containers/MessageComposer/components/CancelEdit.tsx @@ -1,13 +1,13 @@ import { BaseButton } from './Buttons'; import { useEditCancel } from '../../../views/RoomView/stores/ComposerStore'; -import { useMessageAction } from '../../message/stores/MessageActionStore'; +import { useMessageActionKind } from '../../message/stores/MessageActionStore'; import { Gap } from './Gap'; export const CancelEdit = () => { const editCancel = useEditCancel(); - const action = useMessageAction(); + const actionKind = useMessageActionKind(); - if (action?.kind !== 'edit') { + if (actionKind !== 'edit') { return null; } return ( diff --git a/app/containers/MessageComposer/components/ComposerInput.tsx b/app/containers/MessageComposer/components/ComposerInput.tsx index 4df191d42e6..7fa78293e9b 100644 --- a/app/containers/MessageComposer/components/ComposerInput.tsx +++ b/app/containers/MessageComposer/components/ComposerInput.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; +import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react'; import { TextInput, StyleSheet, type TextInputProps, InteractionManager } from 'react-native'; import { useDebouncedCallback } from 'use-debounce'; import { useDispatch } from 'react-redux'; @@ -54,352 +54,356 @@ import { isExternalKeyboardConnected } from '../../../lib/methods/helpers/extern const defaultSelection: IInputSelection = { start: 0, end: 0 }; -export const ComposerInput = forwardRef(({ inputRef }, ref) => { - const { colors, theme } = useTheme(); - const rid = useComposerRid(); - const tmid = useComposerTmid(); - const sharing = useComposerSharing(); - const setQuotesAndText = useSetQuotesAndText(); - const room = useComposerRoom(); - const action = useMessageAction(); - const focused = useFocused(); - const { setFocused, setMicOrSend, setAutocompleteParams } = useMessageComposerApi(); - const autocompleteType = useAutocompleteParams()?.type; - const textRef = useRef(''); - const firstRender = useRef(true); - const selectionRef = useRef(defaultSelection); - const dispatch = useDispatch(); - const isMasterDetail = useMasterDetail(); - const altTextSupported = useAltTextSupported(); - let placeholder = tmid ? I18n.t('Add_thread_reply') : ''; - if (room && !tmid) { - placeholder = I18n.t('Message_roomname', { roomName: (room.t === 'd' ? '@' : '#') + getRoomTitle(room) }); - if (!isTablet && placeholder.length > COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH) { - placeholder = `${placeholder.slice(0, COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH)}...`; +export const ComposerInput = memo( + forwardRef(({ inputRef }, ref) => { + const { colors, theme } = useTheme(); + const rid = useComposerRid(); + const tmid = useComposerTmid(); + const sharing = useComposerSharing(); + const setQuotesAndText = useSetQuotesAndText(); + const room = useComposerRoom(); + const action = useMessageAction(); + const focused = useFocused(); + const { setFocused, setMicOrSend, setAutocompleteParams } = useMessageComposerApi(); + const autocompleteType = useAutocompleteParams()?.type; + const textRef = useRef(''); + const firstRender = useRef(true); + const selectionRef = useRef(defaultSelection); + const dispatch = useDispatch(); + const isMasterDetail = useMasterDetail(); + const altTextSupported = useAltTextSupported(); + let placeholder = tmid ? I18n.t('Add_thread_reply') : ''; + if (room && !tmid) { + placeholder = I18n.t('Message_roomname', { roomName: (room.t === 'd' ? '@' : '#') + getRoomTitle(room) }); + if (!isTablet && placeholder.length > COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH) { + placeholder = `${placeholder.slice(0, COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH)}...`; + } } - } - const route = useRoute>(); - const usedCannedResponse = route.params?.usedCannedResponse; - const prevAction = usePrevious(action); - - // subscribe to changes on mic state to update draft after a message is sent - useMicOrSend(); - const { saveMessageDraft } = useAutoSaveDraft(textRef.current); - - // workaround to handle issues with iOS back swipe navigation - const { iOSBackSwipe } = useIOSBackSwipeHandler(); - - // Draft/Canned Responses - useEffect(() => { - const setDraftMessage = async () => { - const draftMessage = await loadDraftMessage({ rid, tmid }); - if (draftMessage) { - const parsedDraft = parseJson(draftMessage); - if (parsedDraft?.msg || parsedDraft?.quotes) { - setQuotesAndText?.(parsedDraft.msg, parsedDraft.quotes); - } else { - setInput(draftMessage); + const route = useRoute>(); + const usedCannedResponse = route.params?.usedCannedResponse; + const prevAction = usePrevious(action); + + // subscribe to changes on mic state to update draft after a message is sent + useMicOrSend(); + const { saveMessageDraft } = useAutoSaveDraft(textRef.current); + + // workaround to handle issues with iOS back swipe navigation + const { iOSBackSwipe } = useIOSBackSwipeHandler(); + + // Draft/Canned Responses + useEffect(() => { + const setDraftMessage = async () => { + const draftMessage = await loadDraftMessage({ rid, tmid }); + if (draftMessage) { + const parsedDraft = parseJson(draftMessage); + if (parsedDraft?.msg || parsedDraft?.quotes) { + setQuotesAndText?.(parsedDraft.msg, parsedDraft.quotes); + } else { + setInput(draftMessage); + } } + }; + + if (action?.kind !== 'edit' && firstRender.current) { + firstRender.current = false; + setDraftMessage(); } - }; + if (sharing) return; + if (usedCannedResponse) setInput(usedCannedResponse); + }, [action?.kind, rid, tmid, usedCannedResponse]); + + // Edit/quote + useEffect(() => { + const fetchMessageAndSetInput = async (messageId: string) => { + const message = await getMessageById(messageId); + if (message) { + setInput(message?.msg || (altTextSupported ? '' : message?.attachments?.[0]?.description || '')); + } + }; - if (action?.kind !== 'edit' && firstRender.current) { - firstRender.current = false; - setDraftMessage(); - } - if (sharing) return; - if (usedCannedResponse) setInput(usedCannedResponse); - }, [action?.kind, rid, tmid, usedCannedResponse]); - - // Edit/quote - useEffect(() => { - const fetchMessageAndSetInput = async (messageId: string) => { - const message = await getMessageById(messageId); - if (message) { - setInput(message?.msg || (altTextSupported ? '' : message?.attachments?.[0]?.description || '')); + if (sharing) return; + + if (prevAction?.kind === 'edit' && action?.kind !== 'edit') { + setInput(''); + return; + } + if (action?.kind === 'edit') { + focus(); + fetchMessageAndSetInput(action.messageId); + return; + } + if (action?.kind === 'quote' && action.messageIds.length) { + focus(); + } + }, [action]); + + useFocusEffect( + useCallback(() => { + const task = InteractionManager.runAfterInteractions(() => { + emitter.on('addMarkdown', ({ style }) => { + const { start, end } = selectionRef.current; + const text = textRef.current; + const markdown = MARKDOWN_STYLES[style]; + const newText = `${text.substr(0, start)}${markdown}${text.substr(start, end - start)}${markdown}${text.substr(end)}`; + setInput(newText, { + start: start + markdown.length, + end: start === end ? start + markdown.length : end + markdown.length + }); + }); + emitter.on('toolbarMention', () => { + if (autocompleteType) { + return; + } + const { start, end } = selectionRef.current; + const text = textRef.current; + const newText = `${text.substr(0, start)}@${text.substr(start, end - start)}${text.substr(end)}`; + setInput(newText, { start: start + 1, end: start === end ? start + 1 : end + 1 }); + setAutocompleteParams({ text: '', type: '@' }); + }); + }); + return () => { + emitter.off('addMarkdown'); + emitter.off('toolbarMention'); + task?.cancel(); + }; + }, [rid, tmid, autocompleteType]) + ); + + useImperativeHandle(ref, () => ({ + getTextAndClear: () => { + const text = textRef.current; + setInput('', undefined, true); + return text; + }, + getText: () => textRef.current, + getSelection: () => selectionRef.current, + setInput, + onAutocompleteItemSelected, + focus + })); + + const setInput: TSetInput = (text, selection, forceUpdateDraftMessage) => { + const message = text.trim(); + textRef.current = message; + + if (forceUpdateDraftMessage) { + saveMessageDraft(''); } - }; - if (sharing) return; + inputRef.current?.setNativeProps?.({ text }); - if (prevAction?.kind === 'edit' && action?.kind !== 'edit') { - setInput(''); - return; - } - if (action?.kind === 'edit') { - focus(); - fetchMessageAndSetInput(action.messageId); - return; - } - if (action?.kind === 'quote' && action.messageIds.length) { - focus(); - } - }, [action]); + if (selection) { + // setSelection won't trigger onSelectionChange, so we need it to be ran after new text is set + setTimeout(() => { + inputRef.current?.setSelection?.(selection.start, selection.end); + selectionRef.current = selection; + }, 50); + } + setMicOrSend(message.length === 0 ? 'mic' : 'send'); + }; - useFocusEffect(() => { - const task = InteractionManager.runAfterInteractions(() => { - emitter.on('addMarkdown', ({ style }) => { - const { start, end } = selectionRef.current; - const text = textRef.current; - const markdown = MARKDOWN_STYLES[style]; - const newText = `${text.substr(0, start)}${markdown}${text.substr(start, end - start)}${markdown}${text.substr(end)}`; - setInput(newText, { - start: start + markdown.length, - end: start === end ? start + markdown.length : end + markdown.length - }); - }); - emitter.on('toolbarMention', () => { - if (autocompleteType) { - return; + const focus = () => { + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus(); } - const { start, end } = selectionRef.current; - const text = textRef.current; - const newText = `${text.substr(0, start)}@${text.substr(start, end - start)}${text.substr(end)}`; - setInput(newText, { start: start + 1, end: start === end ? start + 1 : end + 1 }); - setAutocompleteParams({ text: '', type: '@' }); - }); - }); - return () => { - emitter.off('addMarkdown'); - emitter.off('toolbarMention'); - task?.cancel(); + }, 300); }; - }); - useImperativeHandle(ref, () => ({ - getTextAndClear: () => { - const text = textRef.current; - setInput('', undefined, true); - return text; - }, - getText: () => textRef.current, - getSelection: () => selectionRef.current, - setInput, - onAutocompleteItemSelected, - focus - })); - - const setInput: TSetInput = (text, selection, forceUpdateDraftMessage) => { - const message = text.trim(); - textRef.current = message; - - if (forceUpdateDraftMessage) { - saveMessageDraft(''); - } + const onChangeText: TextInputProps['onChangeText'] = text => { + textRef.current = text; + debouncedOnChangeText(text); + setInput(text); + }; - inputRef.current?.setNativeProps?.({ text }); + const onSelectionChange: TextInputProps['onSelectionChange'] = e => { + selectionRef.current = e.nativeEvent.selection; + }; - if (selection) { - // setSelection won't trigger onSelectionChange, so we need it to be ran after new text is set - setTimeout(() => { - inputRef.current?.setSelection?.(selection.start, selection.end); - selectionRef.current = selection; - }, 50); - } - setMicOrSend(message.length === 0 ? 'mic' : 'send'); - }; + const onFocus: TextInputProps['onFocus'] = () => { + setFocused(true); + }; - const focus = () => { - setTimeout(() => { - if (inputRef.current) { - inputRef.current.focus(); + const onTouchStart: TextInputProps['onTouchStart'] = () => { + setFocused(true); + }; + + const onBlur: TextInputProps['onBlur'] = () => { + if (!iOSBackSwipe.current && !isExternalKeyboardConnected()) { + setFocused(false); + stopAutocomplete(); } - }, 300); - }; - - const onChangeText: TextInputProps['onChangeText'] = text => { - textRef.current = text; - debouncedOnChangeText(text); - setInput(text); - }; - - const onSelectionChange: TextInputProps['onSelectionChange'] = e => { - selectionRef.current = e.nativeEvent.selection; - }; - - const onFocus: TextInputProps['onFocus'] = () => { - setFocused(true); - }; - - const onTouchStart: TextInputProps['onTouchStart'] = () => { - setFocused(true); - }; - - const onBlur: TextInputProps['onBlur'] = () => { - if (!iOSBackSwipe.current && !isExternalKeyboardConnected()) { - setFocused(false); - stopAutocomplete(); - } - }; + }; - const onAutocompleteItemSelected: IAutocompleteItemProps['onPress'] = async item => { - if (item.type === 'loading') { - return null; - } + const onAutocompleteItemSelected: IAutocompleteItemProps['onPress'] = async item => { + if (item.type === 'loading') { + return null; + } - // If it's slash command preview, we need to execute the command - if (item.type === '/preview') { - try { - if (!rid) return; - const db = database.active; - const commandsCollection = db.get('slash_commands'); - const commandRecord = await commandsCollection.find(item.text); - const { appId } = commandRecord; - const triggerId = generateTriggerId(appId); - executeCommandPreview(item.text, item.params, rid, item.preview, triggerId, tmid); - } catch (e) { - log(e); + // If it's slash command preview, we need to execute the command + if (item.type === '/preview') { + try { + if (!rid) return; + const db = database.active; + const commandsCollection = db.get('slash_commands'); + const commandRecord = await commandsCollection.find(item.text); + const { appId } = commandRecord; + const triggerId = generateTriggerId(appId); + executeCommandPreview(item.text, item.params, rid, item.preview, triggerId, tmid); + } catch (e) { + log(e); + } + requestAnimationFrame(() => { + stopAutocomplete(); + setInput('', { start: 0, end: 0 }); + }); + return; } - requestAnimationFrame(() => { - stopAutocomplete(); - setInput('', { start: 0, end: 0 }); - }); - return; - } - // If it's canned response, but there's no canned responses, we open the canned responses view - if (item.type === '!' && item.id === NO_CANNED_RESPONSES) { - const params = { rid }; - if (isMasterDetail) { - Navigation.navigate('ModalStackNavigator', { screen: 'CannedResponsesListView', params }); - } else { - Navigation.navigate('CannedResponsesListView', params); + // If it's canned response, but there's no canned responses, we open the canned responses view + if (item.type === '!' && item.id === NO_CANNED_RESPONSES) { + const params = { rid }; + if (isMasterDetail) { + Navigation.navigate('ModalStackNavigator', { screen: 'CannedResponsesListView', params }); + } else { + Navigation.navigate('CannedResponsesListView', params); + } + stopAutocomplete(); + return; } - stopAutocomplete(); - return; - } - const text = textRef.current; - const { start, end } = selectionRef.current; - const cursor = Math.max(start, end); - const regexp = getMentionRegexp(); - let textBeforeMention = text.substr(0, cursor).replace(regexp, ''); - // Remove the ! after select the canned response - if (item.type === '!') { - const lastIndexOfExclamation = text.lastIndexOf('!', cursor); - textBeforeMention = text.substr(0, lastIndexOfExclamation).replace(regexp, ''); - } - let mention = ''; - switch (item.type) { - case '@': - mention = fetchIsAllOrHere(item) ? item.title : item.subtitle || item.title; - break; - case '#': - mention = item.subtitle ? item.subtitle : ''; - break; - case ':': - mention = `${typeof item.emoji === 'string' ? item.emoji : item.emoji.name}:`; - break; - case '/': - mention = item.title; - break; - case '!': - mention = item.subtitle ? item.subtitle : ''; - break; - default: - mention = ''; - } - const newText = `${textBeforeMention}${mention} ${text.slice(cursor)}`; + const text = textRef.current; + const { start, end } = selectionRef.current; + const cursor = Math.max(start, end); + const regexp = getMentionRegexp(); + let textBeforeMention = text.substr(0, cursor).replace(regexp, ''); + // Remove the ! after select the canned response + if (item.type === '!') { + const lastIndexOfExclamation = text.lastIndexOf('!', cursor); + textBeforeMention = text.substr(0, lastIndexOfExclamation).replace(regexp, ''); + } + let mention = ''; + switch (item.type) { + case '@': + mention = fetchIsAllOrHere(item) ? item.title : item.subtitle || item.title; + break; + case '#': + mention = item.subtitle ? item.subtitle : ''; + break; + case ':': + mention = `${typeof item.emoji === 'string' ? item.emoji : item.emoji.name}:`; + break; + case '/': + mention = item.title; + break; + case '!': + mention = item.subtitle ? item.subtitle : ''; + break; + default: + mention = ''; + } + const newText = `${textBeforeMention}${mention} ${text.slice(cursor)}`; - const newCursor = textBeforeMention.length + mention.length + 1; - setInput(newText, { start: newCursor, end: newCursor }); - focus(); - requestAnimationFrame(() => { - stopAutocomplete(); - }); - }; + const newCursor = textBeforeMention.length + mention.length + 1; + setInput(newText, { start: newCursor, end: newCursor }); + focus(); + requestAnimationFrame(() => { + stopAutocomplete(); + }); + }; - const stopAutocomplete = () => { - setAutocompleteParams({ text: '', type: null, params: '' }); - }; + const stopAutocomplete = () => { + setAutocompleteParams({ text: '', type: null, params: '' }); + }; - const debouncedOnChangeText = useDebouncedCallback(async (text: string) => { - const isTextEmpty = text.length === 0; - handleTyping(!isTextEmpty); - if (isTextEmpty || !focused) { - stopAutocomplete(); - return; - } - const { start, end } = selectionRef.current; - const cursor = Math.max(start, end); - const whiteSpaceOrBreakLineRegex = /[\s\n]+/; - const txt = - cursor < text.length ? text.substr(0, cursor).split(whiteSpaceOrBreakLineRegex) : text.split(whiteSpaceOrBreakLineRegex); - const lastWord = txt[txt.length - 1]; - const autocompleteText = lastWord.substring(1); - - if (!lastWord) { - stopAutocomplete(); - return; - } - if (!sharing && text.match(/^\//)) { - const commandParameter = text.match(/^\/([a-z0-9._-]+) (.+)/im); - if (commandParameter) { - const db = database.active; - const [, command, params] = commandParameter; - const commandsCollection = db.get('slash_commands'); - try { - const commandRecord = await commandsCollection.find(command); - if (commandRecord.providesPreview) { - setAutocompleteParams({ params, text: command, type: '/preview' }); + const debouncedOnChangeText = useDebouncedCallback(async (text: string) => { + const isTextEmpty = text.length === 0; + handleTyping(!isTextEmpty); + if (isTextEmpty || !focused) { + stopAutocomplete(); + return; + } + const { start, end } = selectionRef.current; + const cursor = Math.max(start, end); + const whiteSpaceOrBreakLineRegex = /[\s\n]+/; + const txt = + cursor < text.length ? text.substr(0, cursor).split(whiteSpaceOrBreakLineRegex) : text.split(whiteSpaceOrBreakLineRegex); + const lastWord = txt[txt.length - 1]; + const autocompleteText = lastWord.substring(1); + + if (!lastWord) { + stopAutocomplete(); + return; + } + if (!sharing && text.match(/^\//)) { + const commandParameter = text.match(/^\/([a-z0-9._-]+) (.+)/im); + if (commandParameter) { + const db = database.active; + const [, command, params] = commandParameter; + const commandsCollection = db.get('slash_commands'); + try { + const commandRecord = await commandsCollection.find(command); + if (commandRecord.providesPreview) { + setAutocompleteParams({ params, text: command, type: '/preview' }); + } + return; + } catch (e) { + // do nothing } - return; - } catch (e) { - // do nothing } + setAutocompleteParams({ text: autocompleteText, type: '/' }); + return; + } + if (lastWord.match(/^#/)) { + setAutocompleteParams({ text: autocompleteText, type: '#' }); + return; + } + if (lastWord.match(/^@/)) { + setAutocompleteParams({ text: autocompleteText, type: '@' }); + return; + } + if (lastWord.match(/^:/)) { + setAutocompleteParams({ text: autocompleteText, type: ':' }); + return; + } + if (lastWord.match(/^!/) && room?.t === 'l') { + setAutocompleteParams({ text: autocompleteText, type: '!' }); + return; } - setAutocompleteParams({ text: autocompleteText, type: '/' }); - return; - } - if (lastWord.match(/^#/)) { - setAutocompleteParams({ text: autocompleteText, type: '#' }); - return; - } - if (lastWord.match(/^@/)) { - setAutocompleteParams({ text: autocompleteText, type: '@' }); - return; - } - if (lastWord.match(/^:/)) { - setAutocompleteParams({ text: autocompleteText, type: ':' }); - return; - } - if (lastWord.match(/^!/) && room?.t === 'l') { - setAutocompleteParams({ text: autocompleteText, type: '!' }); - return; - } - stopAutocomplete(); - }, textInputDebounceTime); - - const handleTyping = (isTyping: boolean) => { - if (sharing || !rid) return; - dispatch(userTyping(rid, isTyping, tmid ? { tmid } : {})); - }; - - return ( - { - inputRef.current = component; - }} - blurOnSubmit={false} - onChangeText={onChangeText} - onTouchStart={onTouchStart} - onSelectionChange={onSelectionChange} - onFocus={onFocus} - onBlur={onBlur} - underlineColorAndroid='transparent' - defaultValue='' - multiline - {...(autocompleteType ? { autoComplete: 'off', autoCorrect: false, autoCapitalize: 'none' } : {})} - keyboardAppearance={theme === 'light' ? 'light' : 'dark'} - // eslint-disable-next-line no-nested-ternary - testID={`message-composer-input${tmid ? '-thread' : sharing ? '-share' : ''}`} - /> - ); -}); + stopAutocomplete(); + }, textInputDebounceTime); + + const handleTyping = (isTyping: boolean) => { + if (sharing || !rid) return; + dispatch(userTyping(rid, isTyping, tmid ? { tmid } : {})); + }; + + return ( + { + inputRef.current = component; + }} + blurOnSubmit={false} + onChangeText={onChangeText} + onTouchStart={onTouchStart} + onSelectionChange={onSelectionChange} + onFocus={onFocus} + onBlur={onBlur} + underlineColorAndroid='transparent' + defaultValue='' + multiline + {...(autocompleteType ? { autoComplete: 'off', autoCorrect: false, autoCapitalize: 'none' } : {})} + keyboardAppearance={theme === 'light' ? 'light' : 'dark'} + // eslint-disable-next-line no-nested-ternary + testID={`message-composer-input${tmid ? '-thread' : sharing ? '-share' : ''}`} + /> + ); + }) +); const styles = StyleSheet.create({ textInput: { diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx index a827fc792b0..bffc8304d2a 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx +++ b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx @@ -20,7 +20,7 @@ jest.mock('../../../views/RoomView/stores/ComposerStore', () => ({ })); jest.mock('../../message/stores/MessageActionStore', () => ({ - useMessageAction: jest.fn(), + useMessageActionKind: jest.fn(), useQuotedMessageIds: jest.fn(() => []) })); @@ -53,7 +53,7 @@ const mockUseAppSelector = require('../../../lib/hooks/useAppSelector').useAppSe const mockUseMessageComposerApi = require('../context').useMessageComposerApi as jest.Mock; const mockUseSetQuotesAndText = require('../../../views/RoomView/stores/ComposerStore').useSetQuotesAndText as jest.Mock; const mockUseGetText = require('../../../views/RoomView/stores/ComposerStore').useGetText as jest.Mock; -const mockUseMessageAction = require('../../message/stores/MessageActionStore').useMessageAction as jest.Mock; +const mockUseMessageActionKind = require('../../message/stores/MessageActionStore').useMessageActionKind as jest.Mock; const mockUseQuotedMessageIds = require('../../message/stores/MessageActionStore').useQuotedMessageIds as jest.Mock; const mockUseAltTextSupported = require('../../../lib/hooks/useAltTextSupported').useAltTextSupported as jest.Mock; const mockGetSubscriptionByRoomId = require('../../../lib/database/services/Subscription').getSubscriptionByRoomId as jest.Mock; @@ -77,7 +77,7 @@ describe('useChooseMedia', () => { mockUseMessageComposerApi.mockReturnValue({ addAttachments }); mockUseSetQuotesAndText.mockReturnValue(jest.fn()); mockUseGetText.mockReturnValue(jest.fn(() => 'draft')); - mockUseMessageAction.mockReturnValue(null); + mockUseMessageActionKind.mockReturnValue(null); mockGetSubscriptionByRoomId.mockResolvedValue({ rid: 'room-id', t: 'c' }); mockGetThreadById.mockResolvedValue({ id: 'thread-id' }); }); @@ -136,7 +136,7 @@ describe('useChooseMedia', () => { it('forwards quoted message ids to ShareView as selectedMessages', async () => { mockUseAltTextSupported.mockReturnValue(false); - mockUseMessageAction.mockReturnValue({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + mockUseMessageActionKind.mockReturnValue('quote'); mockUseQuotedMessageIds.mockReturnValue(['msg-1', 'msg-2']); mockGetDocumentAsync.mockResolvedValue({ canceled: false, @@ -155,7 +155,7 @@ describe('useChooseMedia', () => { it('does not quote the message when the action is edit', async () => { mockUseAltTextSupported.mockReturnValue(false); - mockUseMessageAction.mockReturnValue({ kind: 'edit', messageId: 'msg-1' }); + mockUseMessageActionKind.mockReturnValue('edit'); mockUseQuotedMessageIds.mockReturnValue([]); mockGetDocumentAsync.mockResolvedValue({ canceled: false, diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.ts b/app/containers/MessageComposer/hooks/useChooseMedia.ts index 0f723e86421..d953ad83916 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.ts +++ b/app/containers/MessageComposer/hooks/useChooseMedia.ts @@ -10,7 +10,7 @@ import { getThreadById } from '../../../lib/database/services/Thread'; import Navigation from '../../../lib/navigation/appNavigation'; import { useAppSelector } from '../../../lib/hooks/useAppSelector'; import { useGetText, useSetQuotesAndText } from '../../../views/RoomView/stores/ComposerStore'; -import { useMessageAction, useQuotedMessageIds } from '../../message/stores/MessageActionStore'; +import { useMessageActionKind, useQuotedMessageIds } from '../../message/stores/MessageActionStore'; import { type IShareAttachment } from '../../../definitions'; import ImagePicker, { type ImageOrVideo } from '../../../lib/methods/helpers/ImagePicker/ImagePicker'; import { useMessageComposerApi } from '../context'; @@ -32,7 +32,7 @@ export const useChooseMedia = ({ const { addAttachments } = useMessageComposerApi(); const setQuotesAndText = useSetQuotesAndText(); const getText = useGetText(); - const action = useMessageAction(); + const actionKind = useMessageActionKind(); const quotedMessageIds = useQuotedMessageIds(); const altTextSupported = useAltTextSupported(); const allowList = FileUpload_MediaTypeWhiteList as string; @@ -117,7 +117,7 @@ export const useChooseMedia = ({ room, thread: thread || tmid, attachments, - action: action?.kind ?? null, + action: actionKind, finishShareView, startShareView });