diff --git a/app/containers/MessageComposer/MessageComposer.tsx b/app/containers/MessageComposer/MessageComposer.tsx index 0c39eea594..a71c8f7116 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 d1835ef4ab..390e6b4aea 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 { 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,11 @@ 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; + // subscribe to changes on mic state to update draft after a message is sent useMicOrSend(); const { saveMessageDraft } = useAutoSaveDraft(textRef.current); @@ -143,6 +153,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 +187,7 @@ export const ComposerInput = memo( saveMessageDraft(''); } - inputRef.current?.setNativeProps?.({ text }); + inputRef.current?.setText(text); if (selection) { // setSelection won't trigger onSelectionChange, so we need it to be ran after new text is set @@ -201,25 +213,27 @@ export const ComposerInput = memo( setInput(text); }; - const onSelectionChange: TextInputProps['onSelectionChange'] = e => { - 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 onTouchStart: TextInputProps['onTouchStart'] = () => { + setFocused(true); + }; + const onAutocompleteItemSelected: IAutocompleteItemProps['onPress'] = async item => { if (item.type === 'loading') { return null; @@ -364,28 +378,93 @@ export const ComposerInput = memo( dispatch(userTyping(rid, isTyping, tmid ? { tmid } : {})); }; + const startShareView = () => ({ + selectedMessages, + text: '' + }); + + const finishShareView = (text = '', quotes = []) => setQuotesAndText?.(text, quotes); + + const handleOnImagePaste = async (e: onPasteImageEventData) => { + console.log(e); + if (e.error?.message) { + handleError(e.error.message); + console.log('error detected'); + 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 { + console.log('can upload error'); + + 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' : ''}`} - /> + <> + { + 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} + /> + ); }) ); @@ -397,9 +476,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/ios/Podfile.lock b/ios/Podfile.lock index 99a6053f60..cef75bacfe 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -232,7 +232,7 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - MobileCrypto (0.2.0): + - MobileCrypto (0.2.1): - DoubleConversion - glog - hermes-engine @@ -2162,6 +2162,30 @@ PODS: - React-logger (= 0.79.4) - React-perflogger (= 0.79.4) - React-utils (= 0.79.4) + - ReactNativeTypeRich (2.2.4): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - RNBootSplash (6.3.8): - DoubleConversion - glog @@ -2736,6 +2760,7 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - ReactNativeTypeRich (from `../node_modules/react-native-typerich`) - RNBootSplash (from `../node_modules/react-native-bootsplash`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" @@ -2994,6 +3019,8 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" + ReactNativeTypeRich: + :path: "../node_modules/react-native-typerich" RNBootSplash: :path: "../node_modules/react-native-bootsplash" RNCAsyncStorage: @@ -3082,7 +3109,7 @@ SPEC CHECKSUMS: libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - MobileCrypto: 60a1e43e26a9d6851ae2aa7294b8041c9e9220b7 + MobileCrypto: a424494b2f45bec9dbe60e3f6d16a40aedefe7b7 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 @@ -3159,6 +3186,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: bf62814e0fde923f73fc64b7e82d76c63c284da9 ReactCodegen: 2f22969ab54e1aace69c9b5d3085e0a3b405a9a6 ReactCommon: 177fca841e97b2c0e288e86097b8be04c6e7ae36 + ReactNativeTypeRich: 4702fc21cb380789c8c66d5ee652aab0e6719588 RNBootSplash: 1280eeb18d887de0a45bb4923d4fc56f25c8b99c RNCAsyncStorage: edb872909c88d8541c0bfade3f86cd7784a7c6b3 RNCClipboard: 4fd4b093bd9d0be5ad62ea73884eda7745ad23d0 @@ -3185,7 +3213,7 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 WatermelonDB: 4c846c8cb94eef3cba90fa034d15310163226703 - Yoga: dfabf1234ccd5ac41d1b1d43179f024366ae9831 + Yoga: 2a3a4c38a8441b6359d5e5914d35db7b2b67aebd ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 PODFILE CHECKSUM: 199f6fbbe6fb415c822cca992e6152000ac55b3e diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index 29cc956324..5e7acce4ec 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -1825,7 +1825,7 @@ inputFileListPaths = ( ); inputPaths = ( - "$TARGET_BUILD_DIR/$INFOPLIST_PATH", + $TARGET_BUILD_DIR/$INFOPLIST_PATH, ); name = "Upload source maps to Bugsnag"; outputFileListPaths = ( @@ -1845,7 +1845,7 @@ inputFileListPaths = ( ); inputPaths = ( - "$TARGET_BUILD_DIR/$INFOPLIST_PATH", + $TARGET_BUILD_DIR/$INFOPLIST_PATH, ); name = "Upload source maps to Bugsnag"; outputFileListPaths = ( @@ -2593,7 +2593,7 @@ "$(inherited)", "$(SRCROOT)/../node_modules/rn-extensions-share/ios/**", "$(SRCROOT)/../node_modules/react-native-firebase/ios/RNFirebase/**", - "$PODS_CONFIGURATION_BUILD_DIR/Firebase", + $PODS_CONFIGURATION_BUILD_DIR/Firebase, ); INFOPLIST_FILE = ShareRocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; @@ -2669,7 +2669,7 @@ "$(inherited)", "$(SRCROOT)/../node_modules/rn-extensions-share/ios/**", "$(SRCROOT)/../node_modules/react-native-firebase/ios/RNFirebase/**", - "$PODS_CONFIGURATION_BUILD_DIR/Firebase", + $PODS_CONFIGURATION_BUILD_DIR/Firebase, ); INFOPLIST_FILE = ShareRocketChatRN/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.1; diff --git a/package.json b/package.json index 45c7b5e50e..17c24a0b32 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": "^2.2.4", "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 4a19e87183..8dd7b7fc4a 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@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/react-native-typerich/-/react-native-typerich-2.2.4.tgz#da51f5f3990a287ffad348c202f432957303363c" + integrity sha512-0TQbkpwDvbR0mQk4d/l5CcVXphvFo0VnKt4XhO5wYZ+6SlsVmBrTVuMZ0ASbD2xOfNI5pHgcRbW6YgWHbq+uBg== + 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"