Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e0edb3d
wip: android image pasting
divyanshu-patil Dec 14, 2025
3c27dd2
fix: keep spacing while setting input and trim while sending
divyanshu-patil Dec 14, 2025
88b87f3
fix: message modal opening and user fast typing cursor jumps
divyanshu-patil Dec 14, 2025
3a66e6a
feat: image pasting with ShareView
divyanshu-patil Dec 14, 2025
ff68f0b
fix: autoComplete typo in ComposerInput
divyanshu-patil Dec 14, 2025
855ee28
fix: null check on room, forceUpdateDraftMessage param to ensure draf…
divyanshu-patil Dec 14, 2025
2bac128
refactor: IShareAttachment types, image paste handler name
divyanshu-patil Dec 14, 2025
b9cfb96
chore: upate react-native-typerich version to 0.1.10
divyanshu-patil Dec 14, 2025
9719e2d
cleanup: logs
divyanshu-patil Dec 15, 2025
bb91f3c
fix: ios app crash on opening app
divyanshu-patil Dec 15, 2025
bf0781b
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 15, 2025
c437c36
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 17, 2025
70a0b99
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 18, 2025
c21e7dc
fix: context menu text paste, draft saving, programmatic multiline se…
divyanshu-patil Dec 19, 2025
8b69178
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 22, 2025
25e792c
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 23, 2025
9735282
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 23, 2025
57b6564
chore: added api changes
divyanshu-patil Dec 23, 2025
4c7e354
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 23, 2025
cbd3295
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 24, 2025
6aa6785
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 30, 2025
7d48f67
Merge branch 'develop' into feat/android_image_pasting
divyanshu-patil Dec 31, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions app/containers/MessageComposer/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const MessageComposer = ({
}): ReactElement | null => {
'use memo';

const composerInputRef = useRef(null);
const composerInputRef = useRef<any>(null);
const composerInputComponentRef = useRef<IComposerInput>({
getTextAndClear: () => '',
getText: () => '',
Expand Down Expand Up @@ -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);
}
Expand Down
176 changes: 146 additions & 30 deletions app/containers/MessageComposer/components/ComposerInput.tsx
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 {
Expand All @@ -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';
Expand All @@ -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 };

Expand All @@ -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);
Expand Down Expand Up @@ -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
Copy link
Author

Choose a reason for hiding this comment

The 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: '@' });
});
});
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find ShareView component definition
fd -e tsx -e ts | xargs rg -l "ShareView" | head -20

Repository: 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 10

Repository: 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 -80

Repository: 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 5

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 498


🏁 Script executed:

# Check useRoomContext return type
rg "useRoomContext" app/views/RoomView/context.tsx -A 30 | head -80

Repository: 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 context

Repository: 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 3

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 261


🏁 Script executed:

# Look for the context definition
fd -type f \( -name "*context*" \) | grep -i room

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 248


🏁 Script executed:

# Read RoomContext type definition
cat -n app/views/RoomView/context.ts | head -150

Repository: 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 -100

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 797


🏁 Script executed:

// Instead of:
thread: thread || tmid

// Use:
thread: thread || tmid || ''
// or guard against undefined earlier

Repository: 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 thread: thread || tmid to ShareView, but tmid is typed as string | undefined in the RoomContext, and getThreadById() returns TThreadModel | null. This means the expression can evaluate to undefined when both thread and tmid are falsy. However, ShareView's type definition expects thread: TThreadModel | string (excluding undefined).

To fix this, ensure a valid value is always passed—either by providing a fallback value (e.g., thread: thread || tmid || '') or by adding a guard condition to prevent navigation when neither value is available.

🤖 Prompt for AI Agents
In app/containers/MessageComposer/components/ComposerInput.tsx around line 443,
the prop `thread: thread || tmid` can evaluate to undefined because tmid is
typed string | undefined and thread may be null, violating ShareView's expected
TThreadModel | string; fix by ensuring a non-undefined value is passed — either
provide a safe fallback like `thread || tmid || ''` or add a guard that prevents
calling/navigating to ShareView when both thread and tmid are falsy so ShareView
always receives a valid string or TThreadModel.

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 (
<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' : ''}`}
/>
)}
</>
);
})
);
Expand All @@ -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
}
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down