diff --git a/app/containers/MessageComposer/MessageComposer.tsx b/app/containers/MessageComposer/MessageComposer.tsx index 0c39eea5946..a71c8f7116e 100644 --- a/app/containers/MessageComposer/MessageComposer.tsx +++ b/app/containers/MessageComposer/MessageComposer.tsx @@ -33,7 +33,7 @@ export const MessageComposer = ({ }): ReactElement | null => { 'use memo'; - const composerInputRef = useRef(null); + const composerInputRef = useRef(null); const composerInputComponentRef = useRef({ getTextAndClear: () => '', getText: () => '', @@ -170,7 +170,10 @@ export const MessageComposer = ({ }; const accessibilityFocusOnInput = () => { - const node = findNodeHandle(composerInputRef.current); + const input = composerInputRef.current; + + const hostRef = input?.getNativeRef?.() ?? input; + const node = findNodeHandle(hostRef); if (node) { AccessibilityInfo.setAccessibilityFocus(node); } diff --git a/app/containers/MessageComposer/components/ComposerInput.tsx b/app/containers/MessageComposer/components/ComposerInput.tsx index 178ca3b58b1..71da7e5c7b3 100644 --- a/app/containers/MessageComposer/components/ComposerInput.tsx +++ b/app/containers/MessageComposer/components/ComposerInput.tsx @@ -1,9 +1,11 @@ import React, { forwardRef, memo, useCallback, useEffect, useImperativeHandle } from 'react'; -import { TextInput, StyleSheet, type TextInputProps, InteractionManager } from 'react-native'; +import { TextInput, Platform, StyleSheet, type TextInputProps, InteractionManager, Alert } from 'react-native'; import { useDebouncedCallback } from 'use-debounce'; import { useDispatch } from 'react-redux'; import { type RouteProp, useFocusEffect, useRoute } from '@react-navigation/native'; +import { type OnChangeSelectionEvent, type onPasteImageEventData, TypeRichTextInput } from 'react-native-typerich'; +import { canUploadFile } from '../../../lib/methods/helpers'; import { textInputDebounceTime } from '../../../lib/constants/debounceConfig'; import I18n from '../../../i18n'; import { @@ -15,7 +17,7 @@ import { } from '../interfaces'; import { useAutocompleteParams, useFocused, useMessageComposerApi, useMicOrSend } from '../context'; import { fetchIsAllOrHere, getMentionRegexp } from '../helpers'; -import { useAutoSaveDraft } from '../hooks'; +import { useAutoSaveDraft, useCanUploadFile } from '../hooks'; import sharedStyles from '../../../views/Styles'; import { useTheme } from '../../../theme'; import { userTyping } from '../../../actions/room'; @@ -42,6 +44,9 @@ import { usePrevious } from '../../../lib/hooks/usePrevious'; import { type ChatsStackParamList } from '../../../stacks/types'; import { loadDraftMessage } from '../../../lib/methods/draftMessage'; import useIOSBackSwipeHandler from '../hooks/useIOSBackSwipeHandler'; +import { getSubscriptionByRoomId } from '../../../lib/database/services/Subscription'; +import { getThreadById } from '../../../lib/database/services/Thread'; +import { type IShareAttachment } from '../../../definitions'; const defaultSelection: IInputSelection = { start: 0, end: 0 }; @@ -68,6 +73,13 @@ export const ComposerInput = memo( const usedCannedResponse = route.params?.usedCannedResponse; const prevAction = usePrevious(action); + const permissionToUpload = useCanUploadFile(rid); + const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = useAppSelector(state => state.settings); + const allowList = FileUpload_MediaTypeWhiteList as string; + const maxFileSize = FileUpload_MaxFileSize as number; + + const isAndroid = Platform.OS === 'android'; + // subscribe to changes on mic state to update draft after a message is sent useMicOrSend(); const { saveMessageDraft } = useAutoSaveDraft(textRef.current); @@ -143,6 +155,8 @@ export const ComposerInput = memo( 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 }); + // todo mention command here + setAutocompleteParams({ text: '', type: '@' }); }); }); @@ -175,7 +189,11 @@ export const ComposerInput = memo( saveMessageDraft(''); } - inputRef.current?.setNativeProps?.({ text }); + if (isAndroid) { + inputRef.current?.setText(text); + } else { + inputRef.current?.setNativeProps?.({ text }); // keep TextInput path + } if (selection) { // setSelection won't trigger onSelectionChange, so we need it to be ran after new text is set @@ -205,20 +223,33 @@ export const ComposerInput = memo( selectionRef.current = e.nativeEvent.selection; }; - const onFocus: TextInputProps['onFocus'] = () => { - setFocused(true); + const onChangeSelection = (e: OnChangeSelectionEvent) => { + const { start, end } = e; + const selection = { start, end }; + selectionRef.current = selection; }; - const onTouchStart: TextInputProps['onTouchStart'] = () => { + const handleFocus = () => { setFocused(true); }; - const onBlur: TextInputProps['onBlur'] = () => { + const handleBlur = () => { if (!iOSBackSwipe.current) { setFocused(false); stopAutocomplete(); } }; + const onFocus: TextInputProps['onFocus'] = () => { + handleFocus(); + }; + + const onTouchStart: TextInputProps['onTouchStart'] = () => { + setFocused(true); + }; + + const onBlur: TextInputProps['onBlur'] = () => { + handleBlur(); + }; const onAutocompleteItemSelected: IAutocompleteItemProps['onPress'] = async item => { if (item.type === 'loading') { @@ -364,28 +395,113 @@ export const ComposerInput = memo( dispatch(userTyping(rid, isTyping)); }; + const startShareView = () => ({ + selectedMessages, + text: '' + }); + + const finishShareView = (text = '', quotes = []) => setQuotesAndText?.(text, quotes); + + const handleOnImagePaste = async (e: onPasteImageEventData) => { + if (e.error) { + handleError(e.error.message); + return; + } + if (!rid) return; + + const room = await getSubscriptionByRoomId(rid); + + if (!room) { + handleError('Room not found'); + return; + } + + let thread; + if (tmid) { + thread = await getThreadById(tmid); + } + + const file = { + filename: e.fileName, + size: e.fileSize, + mime: e.type, + path: e.uri + } as IShareAttachment; + + const canUploadResult = canUploadFile({ + file, + allowList, + maxFileSize, + permissionToUploadFile: permissionToUpload + }); + if (canUploadResult.success) { + Navigation.navigate('ShareView', { + room, + thread: thread || tmid, + attachments: [file], + action, + finishShareView, + startShareView + }); + } else { + handleError(canUploadResult.error); + } + }; + + const handleError = (error?: string) => { + Alert.alert(I18n.t('Error_uploading'), error && I18n.isTranslated(error) ? I18n.t(error) : error); + }; + 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' : ''}`} - /> + <> + {isAndroid ? ( + { + inputRef.current = component; + }} + // blurOnSubmit={false} // not needed + onChangeText={onChangeText} + onTouchStart={onTouchStart} + onChangeSelection={onChangeSelection} + onFocus={handleFocus} // typerich onFocus / onBlur events doesn't pass any arguments to callbacks + onBlur={handleBlur} + // underlineColorAndroid='transparent' // by default behaiviour + 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' : ''}`} + onPasteImageData={handleOnImagePaste} + /> + ) : ( + { + 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' : ''}`} + /> + )} + ); }) ); @@ -397,9 +513,9 @@ const styles = StyleSheet.create({ maxHeight: MAX_HEIGHT, paddingTop: 12, paddingBottom: 12, - fontSize: 16, textAlignVertical: 'center', ...sharedStyles.textRegular, - lineHeight: 22 + lineHeight: 22, + fontSize: 16 } }); diff --git a/package.json b/package.json index 45c7b5e50e1..97f24a0966d 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "react-native-skeleton-placeholder": "5.2.4", "react-native-slowlog": "1.0.2", "react-native-svg": "^15.12.1", + "react-native-typerich": "^1.1.1", "react-native-url-polyfill": "2.0.0", "react-native-webview": "^13.15.0", "react-redux": "8.0.5", diff --git a/yarn.lock b/yarn.lock index 5f334cbad37..22931fffafb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12854,6 +12854,11 @@ react-native-svg@^15.12.1: css-tree "^1.1.3" warn-once "0.1.1" +react-native-typerich@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/react-native-typerich/-/react-native-typerich-1.1.1.tgz#4e391bf61b88a261a358154cc1ffd1f3a1ce477a" + integrity sha512-TeBufzO9T4EKPMougv+ssD59+UnFwWL0JM2FKqXhihn+3z4MFCfRDuD7qd6vP3Vh+WIyUINW4GKm9XlqYeC6Lw== + react-native-url-polyfill@2.0.0, react-native-url-polyfill@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-native-url-polyfill/-/react-native-url-polyfill-2.0.0.tgz#db714520a2985cff1d50ab2e66279b9f91ffd589"