-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(Android): image pasting in message composer #6848
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
e0edb3d
3c27dd2
88b87f3
3a66e6a
ff68f0b
855ee28
2bac128
b9cfb96
9719e2d
bb91f3c
bf0781b
c437c36
70a0b99
c21e7dc
8b69178
25e792c
9735282
57b6564
4c7e354
cbd3295
6aa6785
7d48f67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this comment is marked for future PR's to include live markdown directly inside the TextInput itself |
||
|
|
||
| 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find ShareView component definition
fd -e tsx -e ts | xargs rg -l "ShareView" | head -20Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 539 🏁 Script executed: # Look for ShareView component and its type definition
rg "export.*ShareView|interface.*ShareView|type.*ShareView" -A 10Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 1423 🏁 Script executed: # Check the ComposerInput file to understand the types of thread and tmid variables
cat -n app/containers/MessageComposer/components/ComposerInput.tsx | sed -n '430,460p'Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 960 🏁 Script executed: # Get the function signature and variable declarations in ComposerInput.tsx
cat -n app/containers/MessageComposer/components/ComposerInput.tsx | sed -n '1,100p'Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 5708 🏁 Script executed: # Search for thread and tmid variable declarations and their types
rg "thread|tmid" app/containers/MessageComposer/components/ComposerInput.tsx -B 3 -A 1 | head -80Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 2336 🏁 Script executed: # Check the exact function containing line 443
ast-grep --pattern $'const $_ = ($_) => {
$$$
thread: thread || tmid
$$$
}'Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 60 🏁 Script executed: # Find getThreadById definition and its return type
rg "export.*getThreadById|function getThreadById" -A 5Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 498 🏁 Script executed: # Check useRoomContext return type
rg "useRoomContext" app/views/RoomView/context.tsx -A 30 | head -80Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 148 🏁 Script executed: # Look for the complete function context around line 443 to see the full picture
cat -n app/containers/MessageComposer/components/ComposerInput.tsx | sed -n '410,450p'Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 1140 🏁 Script executed: # Find RoomContext definition
fd -type f -name "*.tsx" -o -name "*.ts" | xargs rg -l "useRoomContext" | grep -i contextRepository: RocketChat/Rocket.Chat.ReactNative Length of output: 295 🏁 Script executed: # Check the collection find() return type and thread model type
rg "threadCollection.find|TThreadModel|IThread" app/lib/database/services/Thread.ts -A 3 -B 3Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 261 🏁 Script executed: # Look for the context definition
fd -type f \( -name "*context*" \) | grep -i roomRepository: RocketChat/Rocket.Chat.ReactNative Length of output: 248 🏁 Script executed: # Read RoomContext type definition
cat -n app/views/RoomView/context.ts | head -150Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 1099 🏁 Script executed: # Look for the context provider and useRoomContext hook definition
rg "useRoomContext|export.*RoomContext" app/views/RoomView/context.ts -A 15 | head -100Repository: RocketChat/Rocket.Chat.ReactNative Length of output: 797 🏁 Script executed: // Instead of:
thread: thread || tmid
// Use:
thread: thread || tmid || ''
// or guard against undefined earlierRepository: RocketChat/Rocket.Chat.ReactNative Length of output: 410 Remove the type union issue by ensuring a non-undefined value is always passed to ShareView. The code passes To fix this, ensure a valid value is always passed—either by providing a fallback value (e.g., 🤖 Prompt for AI Agents |
||
| attachments: [file], | ||
| action, | ||
| finishShareView, | ||
| startShareView | ||
| }); | ||
| } else { | ||
| handleError(canUploadResult.error); | ||
| } | ||
| }; | ||
divyanshu-patil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const handleError = (error?: string) => { | ||
| Alert.alert(I18n.t('Error_uploading'), error && I18n.isTranslated(error) ? I18n.t(error) : error); | ||
| }; | ||
|
|
||
| return ( | ||
| <TextInput | ||
| style={[styles.textInput, { color: colors.fontDefault }]} | ||
| placeholder={placeholder} | ||
| placeholderTextColor={colors.fontAnnotation} | ||
| ref={component => { | ||
| 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 ? ( | ||
| <TypeRichTextInput | ||
| style={[styles.textInput]} | ||
| color={colors.fontDefault} | ||
| placeholder={placeholder} | ||
| placeholderTextColor={colors.fontAnnotation} | ||
| ref={component => { | ||
| 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} | ||
| /> | ||
| ) : ( | ||
| <TextInput | ||
| style={[styles.textInput, { color: colors.fontDefault }]} | ||
| placeholder={placeholder} | ||
| placeholderTextColor={colors.fontAnnotation} | ||
| ref={component => { | ||
| 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 | ||
| } | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.