Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion app/containers/UIKit/MessageBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ export const messageBlockWithContext = (context: any) => (props: any) =>
</KitContext.Provider>
);

const MessageBlock = ({ blocks }: any) => UiKitMessage(blocks);
const MessageBlock = ({ blocks }: any) => {
if (!blocks) return null;
const renderedBlocks = UiKitMessage(blocks);
return Array.isArray(renderedBlocks)
? renderedBlocks.map(element => <React.Fragment key={element.props.blockId}>{element}</React.Fragment>)
Comment on lines +16 to +17

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

❓ Verification inconclusive

🏁 Script executed:

#!/bin/bash
# Search for UiKitMessage implementation and blockId assignment
rg -n "blockId" --type ts --type tsx -C 3 | head -100

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 212


🏁 Script executed:

#!/bin/bash
# Find UiKitMessage definition
rg -n "UiKitMessage" --type ts --type tsx -C 3 | head -80

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 212


🏁 Script executed:

#!/bin/bash
# Look at the actual MessageBlock file to see full context
cat -n app/containers/UIKit/MessageBlock.tsx

Repository: RocketChat/Rocket.Chat.ReactNative

Length of output: 212


The repository cannot be cloned in the sandbox environment. However, I can still provide a rewritten review comment based on the context provided and common React/TypeScript patterns.

Let me analyze the original review comment:

Key observations from the provided context:

  1. The code uses element.props.blockId as the React key without any fallback
  2. The AI summary mentioned a fallback mechanism, but the actual code doesn't show one
  3. The concern is valid: if blockId is undefined/null, multiple fragments could share the same key

Analysis:

  • The original review correctly identifies a potential issue: React will warn if multiple elements share undefined as a key
  • Whether this is actually a problem depends on whether UiKitMessage always provides blockId on rendered elements
  • Without access to the implementation, the safest approach is to flag this as requiring verification by the developer

Since I cannot verify the codebase directly, the review comment should be rewritten to be more precise about what needs verification while preserving the valid concern:


Consider adding a fallback for blockId if it can be undefined.

The current implementation sets the key to element.props.blockId without a fallback. If any rendered element lacks a blockId property, React will warn about duplicate keys. Verify that UiKitMessage guarantees blockId on all rendered elements; if not, add an index fallback:

-	return Array.isArray(renderedBlocks)
-		? renderedBlocks.map(element => <React.Fragment key={element.props.blockId}>{element}</React.Fragment>)
+	return Array.isArray(renderedBlocks)
+		? renderedBlocks.map((element, index) => (
+				<React.Fragment key={element.props.blockId ?? `block-${index}`}>{element}</React.Fragment>
+			))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return Array.isArray(renderedBlocks)
? renderedBlocks.map(element => <React.Fragment key={element.props.blockId}>{element}</React.Fragment>)
return Array.isArray(renderedBlocks)
? renderedBlocks.map((element, index) => (
<React.Fragment key={element.props.blockId ?? `block-${index}`}>{element}</React.Fragment>
))
🤖 Prompt for AI Agents
In app/containers/UIKit/MessageBlock.tsx around lines 16 to 17, the mapped
Fragment uses element.props.blockId as the React key with no fallback; ensure
keys are always stable by verifying UiKitMessage guarantees a non-empty blockId
or change the mapping to derive a unique key when blockId is missing (for
example use the map index or a deterministic fallback like
`${element.type}-${index}`), and update the code to use that fallback so React
never receives duplicate/undefined keys.

: renderedBlocks;
};

export const ModalBlockWithContext = (props: any) => (
<KitContext.Provider value={props}>
Expand Down
30 changes: 17 additions & 13 deletions app/containers/markdown/components/Inline.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useContext } from 'react';
import { Text } from 'react-native';
import { type Paragraph as ParagraphProps } from '@rocket.chat/message-parser';
import { type Inlines as InlinesType, type Paragraph as ParagraphProps } from '@rocket.chat/message-parser';

import styles from '../styles';
import { AtMention, Hashtag } from './mentions';
Expand All @@ -18,11 +18,14 @@ interface IParagraphProps {
forceTrim?: boolean;
}

type TInlineWithID = InlinesType & { _id: string };

const Inline = ({ value, forceTrim }: IParagraphProps): React.ReactElement | null => {
const { useRealName, username, navToRoomInfo, mentions, channels } = useContext(MarkdownContext);
return (
<Text style={styles.inline}>
{value.map((block, index) => {
{value.map((b, index) => {
const block = b as TInlineWithID;
Comment thread
divyanshu-patil marked this conversation as resolved.
Comment thread
divyanshu-patil marked this conversation as resolved.
// We are forcing trim when is a `[ ](https://https://open.rocket.chat/) plain_text`
// to clean the empty spaces
if (forceTrim) {
Expand All @@ -41,20 +44,21 @@ const Inline = ({ value, forceTrim }: IParagraphProps): React.ReactElement | nul

switch (block.type) {
case 'IMAGE':
return <Image value={block.value} />;
return <Image key={block._id} value={block.value} />;
case 'PLAIN_TEXT':
return <Plain value={block.value} />;
return <Plain key={block._id} value={block.value} />;
case 'BOLD':
return <Bold value={block.value} />;
return <Bold key={block._id} value={block.value} />;
case 'STRIKE':
return <Strike value={block.value} />;
return <Strike key={block._id} value={block.value} />;
case 'ITALIC':
return <Italic value={block.value} />;
return <Italic key={block._id} value={block.value} />;
case 'LINK':
return <Link value={block.value} />;
return <Link key={block._id} value={block.value} />;
case 'MENTION_USER':
return (
<AtMention
key={block._id}
mention={block.value.value}
useRealName={useRealName}
username={username}
Expand All @@ -63,16 +67,16 @@ const Inline = ({ value, forceTrim }: IParagraphProps): React.ReactElement | nul
/>
);
case 'EMOJI':
return <Emoji block={block} index={index} />;
return <Emoji key={block._id} block={block} index={index} />;
case 'MENTION_CHANNEL':
return <Hashtag hashtag={block.value.value} navToRoomInfo={navToRoomInfo} channels={channels} />;
return <Hashtag key={block._id} hashtag={block.value.value} navToRoomInfo={navToRoomInfo} channels={channels} />;
case 'INLINE_CODE':
return <InlineCode value={block.value} />;
return <InlineCode key={block._id} value={block.value} />;
case 'INLINE_KATEX':
// return <InlineKaTeX value={block.value} />;
return <Text>{block.value}</Text>;
return <Text key={block._id}>{block.value}</Text>;
case 'TIMESTAMP':
return <Timestamp value={block.value} />;
return <Timestamp key={block._id} value={block.value} />;
default:
return null;
}
Expand Down
11 changes: 7 additions & 4 deletions app/containers/markdown/components/Quote.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { View } from 'react-native';
import { type Quote as QuoteProps } from '@rocket.chat/message-parser';
import { type Paragraph as ParagraphType, type Quote as QuoteProps } from '@rocket.chat/message-parser';

import { themes } from '../../../lib/constants/colors';
import { useTheme } from '../../../theme';
Expand All @@ -11,15 +11,18 @@ interface IQuoteProps {
value: QuoteProps['value'];
}

type TParagraphWithID = ParagraphType & { _id: string };

const Quote = ({ value }: IQuoteProps) => {
const { theme } = useTheme();
return (
<View style={styles.container}>
<View style={[styles.quote, { backgroundColor: themes[theme].strokeLight }]} />
<View style={styles.childContainer}>
{value.map(item => (
<Paragraph value={item.value} />
))}
{value.map(i => {
const item = i as TParagraphWithID;
return <Paragraph key={item._id} value={item.value} />;
})}
</View>
</View>
);
Expand Down
9 changes: 6 additions & 3 deletions app/containers/markdown/components/code/Code.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { View } from 'react-native';
import { type Code as CodeProps } from '@rocket.chat/message-parser';
import { type CodeLine as CodeLineType, type Code as CodeProps } from '@rocket.chat/message-parser';

import styles from '../../styles';
import { useTheme } from '../../../../theme';
Expand All @@ -10,6 +10,8 @@ interface ICodeProps {
value: CodeProps['value'];
}

type TCodeLineWithID = CodeLineType & { _id: string };

const Code = ({ value }: ICodeProps): React.ReactElement => {
const { colors } = useTheme();

Expand All @@ -22,10 +24,11 @@ const Code = ({ value }: ICodeProps): React.ReactElement => {
borderColor: colors.strokeLight
}
]}>
{value.map(block => {
{value.map(b => {
const block = b as TCodeLineWithID;
switch (block.type) {
case 'CODE_LINE':
return <CodeLine value={block.value} />;
return <CodeLine key={block._id} value={block.value} />;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

where is _id coming from?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

in app/containers/markdown/index.tsx
after the tokens are parsed we add _id field in it

assigning ids here

generating ids

i also tested in my local _id field always contains unique 16 char value

@divyanshu-patil divyanshu-patil Nov 27, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

image

ids are assigned to nested blocks also, so even if the block internally maps the tokens again like BigEmoji works perfectly with this keys

default:
return null;
}
Expand Down
11 changes: 7 additions & 4 deletions app/containers/markdown/components/emoji/BigEmoji.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { type BigEmoji as BigEmojiProps } from '@rocket.chat/message-parser';
import { type Emoji as EmojiType, type BigEmoji as BigEmojiProps } from '@rocket.chat/message-parser';

import Emoji from './Emoji';

interface IBigEmojiProps {
value: BigEmojiProps['value'];
}

type TEmojiWithId = EmojiType & { _id: string };

const styles = StyleSheet.create({
container: {
flexDirection: 'row'
Expand All @@ -16,9 +18,10 @@ const styles = StyleSheet.create({

const BigEmoji = ({ value }: IBigEmojiProps) => (
<View style={styles.container}>
{value.map(block => (
<Emoji block={block} isBigEmoji />
))}
{value.map(b => {
const block = b as TEmojiWithId;
return <Emoji key={block._id} block={block} isBigEmoji />;
})}
</View>
);

Expand Down
16 changes: 9 additions & 7 deletions app/containers/markdown/components/inline/Bold.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,27 @@ const styles = StyleSheet.create({
}
});

type TBoldWithID<T> = T & { _id: string };

const Bold = ({ value }: IBoldProps) => (
<Text style={styles.text}>
{value.map(block => {
{value.map(b => {
const block = b as TBoldWithID<typeof b>;
switch (block.type) {
case 'LINK':
return <Link value={block.value} />;
return <Link key={block._id} value={block.value} />;
case 'PLAIN_TEXT':
return <Plain value={block.value} />;
return <Plain key={block._id} value={block.value} />;
case 'STRIKE':
return <Strike value={block.value} />;
return <Strike key={block._id} value={block.value} />;
case 'ITALIC':
return <Italic value={block.value} />;
return <Italic key={block._id} value={block.value} />;
case 'MENTION_CHANNEL':
return <Plain value={`#${block.value.value}`} />;
return <Plain key={block._id} value={`#${block.value.value}`} />;
default:
return null;
}
})}
</Text>
);

export default Bold;
15 changes: 9 additions & 6 deletions app/containers/markdown/components/inline/Italic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ const styles = StyleSheet.create({
}
});

type TItalicWithID<T> = T & { _id: string };

const Italic = ({ value }: IItalicProps) => (
<Text style={styles.text}>
{value.map(block => {
{value.map(b => {
const block = b as TItalicWithID<typeof b>;
switch (block.type) {
case 'LINK':
return <Link value={block.value} />;
return <Link key={block._id} value={block.value} />;
case 'PLAIN_TEXT':
return <Plain value={block.value} />;
return <Plain key={block._id} value={block.value} />;
case 'STRIKE':
return <Strike value={block.value} />;
return <Strike key={block._id} value={block.value} />;
case 'BOLD':
return <Bold value={block.value} />;
return <Bold key={block._id} value={block.value} />;
case 'MENTION_CHANNEL':
return <Plain value={`#${block.value.value}`} />;
return <Plain key={block._id} value={`#${block.value.value}`} />;
default:
return null;
}
Expand Down
15 changes: 9 additions & 6 deletions app/containers/markdown/components/inline/Strike.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ const styles = StyleSheet.create({
}
});

type TStrikeWithID<T> = T & { _id: string };

const Strike = ({ value }: IStrikeProps) => (
<Text style={styles.text}>
{value.map(block => {
{value.map(b => {
const block = b as TStrikeWithID<typeof b>;
switch (block.type) {
case 'LINK':
return <Link value={block.value} />;
return <Link key={block._id} value={block.value} />;
case 'PLAIN_TEXT':
return <Plain value={block.value} />;
return <Plain key={block._id} value={block.value} />;
case 'BOLD':
return <Bold value={block.value} />;
return <Bold key={block._id} value={block.value} />;
case 'ITALIC':
return <Italic value={block.value} />;
return <Italic key={block._id} value={block.value} />;
case 'MENTION_CHANNEL':
return <Plain value={`#${block.value.value}`} />;
return <Plain key={block._id} value={`#${block.value.value}`} />;
default:
return null;
}
Expand Down
35 changes: 20 additions & 15 deletions app/containers/markdown/components/list/TaskList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { Text, View } from 'react-native';
import { type Tasks as TasksProps } from '@rocket.chat/message-parser';
import { type Task as TaskType, type Tasks as TasksProps } from '@rocket.chat/message-parser';

import Inline from '../Inline';
import styles from '../../styles';
Expand All @@ -11,24 +11,29 @@ interface ITasksProps {
value: TasksProps['value'];
}

type TTaskWithID = TaskType & { _id: string };

const TaskList = ({ value = [] }: ITasksProps) => {
const { colors } = useTheme();
return (
<View>
{value.map(item => (
<View style={styles.row}>
<Text style={[styles.text, { color: colors.fontDefault }]}>
<CustomIcon
testID={item.status ? 'task-list-checked' : 'task-list-unchecked'}
name={item.status ? 'checkbox-checked' : 'checkbox-unchecked'}
size={24}
/>
</Text>
<Text style={[styles.inline, { color: colors.fontDefault }]}>
<Inline value={item.value} />
</Text>
</View>
))}
{value.map(i => {
const item = i as TTaskWithID;
return (
<View key={item._id} style={styles.row}>
<Text style={[styles.text, { color: colors.fontDefault }]}>
<CustomIcon
testID={item.status ? 'task-list-checked' : 'task-list-unchecked'}
name={item.status ? 'checkbox-checked' : 'checkbox-unchecked'}
size={24}
/>
</Text>
<Text style={[styles.inline, { color: colors.fontDefault }]}>
<Inline value={item.value} />
</Text>
</View>
);
})}
</View>
);
};
Expand Down
23 changes: 14 additions & 9 deletions app/containers/markdown/components/list/UnorderedList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { type UnorderedList as UnorderedListProps } from '@rocket.chat/message-parser';
import { type ListItem, type UnorderedList as UnorderedListProps } from '@rocket.chat/message-parser';
import { View, Text } from 'react-native';

import Inline from '../Inline';
Expand All @@ -11,18 +11,23 @@ interface IUnorderedListProps {
value: UnorderedListProps['value'];
}

type TListItemWithID = ListItem & { _id: string };

const UnorderedList = ({ value }: IUnorderedListProps) => {
const { theme } = useTheme();
return (
<View>
{value.map(item => (
<View style={styles.row}>
<Text style={[styles.text, { color: themes[theme].fontDefault }]}>{'\u2022 '}</Text>
<Text style={[styles.inline, { color: themes[theme].fontDefault }]}>
<Inline value={item.value} />
</Text>
</View>
))}
{value.map(i => {
const item = i as TListItemWithID;
return (
<View key={item._id} style={styles.row}>
<Text style={[styles.text, { color: themes[theme].fontDefault }]}>{'\u2022 '}</Text>
<Text style={[styles.inline, { color: themes[theme].fontDefault }]}>
<Inline value={item.value} />
</Text>
</View>
);
})}
</View>
);
};
Expand Down
Loading