From 4dffd079ebabd25e153bfd31a11758f2c99cda1a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 24 Apr 2026 02:54:37 +0700 Subject: [PATCH] chore(lint): memoize inline styles in message components Clears 5 react-perf/jsx-no-new-* warnings in message.tsx by memoizing image source objects, extracting an AssistantMessageBlockContainer for per-block spacing, and moving todo list item rendering into a TodoListItemRow subcomponent so each row memoizes its own style arrays. The remaining 8 warnings in react-native-markdown-display render rules are inherent to that library's rule API. --- packages/app/src/components/message.tsx | 137 +++++++++++++++--------- 1 file changed, 86 insertions(+), 51 deletions(-) diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 0175c9e61..910cfdf4b 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -251,7 +251,10 @@ function canScrollInsideDetailFromTarget( function shouldStopDetailWheelPropagation(detailRoot: HTMLElement, event: WheelEvent): boolean { const startElement = getWheelEventElementTarget(event, detailRoot); const verticalDelta = event.deltaY; - const horizontalDelta = event.deltaX !== 0 ? event.deltaX : event.shiftKey ? event.deltaY : 0; + let horizontalDelta: number; + if (event.deltaX !== 0) horizontalDelta = event.deltaX; + else if (event.shiftKey) horizontalDelta = event.deltaY; + else horizontalDelta = 0; const hasVerticalIntent = Math.abs(verticalDelta) > SCROLL_EDGE_EPSILON; const hasHorizontalIntent = Math.abs(horizontalDelta) > SCROLL_EDGE_EPSILON; @@ -352,16 +355,17 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({ function UserMessageAttachmentThumbnail({ image }: { image: UserMessageImageAttachment }) { const uri = useAttachmentPreviewUrl(image); + const imageSource = useMemo(() => ({ uri: uri ?? "" }), [uri]); if (!uri) { return ; } - return ; + return ; } export const UserMessage = memo(function UserMessage({ message, images = [], - timestamp, + timestamp: _timestamp, isFirstInGroup = true, isLastInGroup = true, disableOuterSpacing, @@ -582,12 +586,13 @@ const AssistantMarkdownResolvedImage = memo(function AssistantMarkdownResolvedIm () => [assistantMessageStylesheet.imageFrame, containerStyle], [containerStyle], ); + const imageSource = useMemo(() => ({ uri }), [uri]); return ( - - {query.error instanceof Error - ? query.error.message - : dataImageQuery.error instanceof Error - ? dataImageQuery.error.message - : "Unable to load image preview."} - + {errorText} ); } @@ -1164,6 +1168,22 @@ const NativeExpandableBadgeShimmer = memo(function NativeExpandableBadgeShimmer( ); }); +interface AssistantMessageBlockContainerProps { + marginBottom: number; + children: ReactNode; +} + +function AssistantMessageBlockContainer({ + marginBottom, + children, +}: AssistantMessageBlockContainerProps) { + const style = useMemo( + () => (marginBottom > 0 ? { marginBottom } : undefined), + [marginBottom], + ); + return {children}; +} + interface MemoizedMarkdownBlockProps { text: string; styles: ReturnType; @@ -1195,7 +1215,7 @@ const MemoizedMarkdownBlock = React.memo(function MemoizedMarkdownBlock({ export const AssistantMessage = memo(function AssistantMessage({ message, - timestamp, + timestamp: _timestamp, onInlinePathPress, workspaceRoot, serverId, @@ -1373,7 +1393,7 @@ export const AssistantMessage = memo(function AssistantMessage({ )} ), - image: (node: any, _children: ReactNode[], parent: any, styles: any) => { + image: (node: any, _children: ReactNode[], parent: any, _styles: any) => { const paragraphNode = Array.isArray(parent) ? parent.find((ancestor: any) => ancestor?.type === "paragraph") : null; @@ -1415,9 +1435,9 @@ export const AssistantMessage = memo(function AssistantMessage({ return ( {blocks.map((block, index) => ( - - + ))} ); @@ -1468,7 +1488,7 @@ const speakMessageStylesheet = StyleSheet.create((theme) => ({ export const SpeakMessage = memo(function SpeakMessage({ message, - timestamp, + timestamp: _timestamp, disableOuterSpacing, }: SpeakMessageProps) { const { theme } = useUnistyles(); @@ -1579,7 +1599,7 @@ const activityLogStylesheet = StyleSheet.create((theme) => ({ export const ActivityLog = memo(function ActivityLog({ type, message, - timestamp, + timestamp: _timestamp, metadata, artifactId, artifactType, @@ -1710,12 +1730,10 @@ export const CompactionMarker = memo(function CompactionMarker({ status, preTokens, }: CompactionMarkerProps) { - const label = - status === "loading" - ? "Compacting..." - : preTokens - ? `Context compacted (${Math.round(preTokens / 1000)}K tokens)` - : "Context compacted"; + let label: string; + if (status === "loading") label = "Compacting..."; + else if (preTokens) label = `Context compacted (${Math.round(preTokens / 1000)}K tokens)`; + else label = "Context compacted"; return ( @@ -1738,6 +1756,41 @@ interface TodoListCardProps { disableOuterSpacing?: boolean; } +interface TodoListItemRowProps { + text: string; + completed: boolean; +} + +function TodoListItemRow({ text, completed }: TodoListItemRowProps) { + const { theme: todoUnistylesTheme } = useUnistyles(); + const badgeStyle = useMemo( + () => [ + todoListCardStylesheet.radioBadge, + completed + ? todoListCardStylesheet.radioBadgeComplete + : todoListCardStylesheet.radioBadgeIncomplete, + ], + [completed], + ); + const textStyle = useMemo( + () => [ + todoListCardStylesheet.itemText, + completed && todoListCardStylesheet.itemTextCompleted, + ], + [completed], + ); + return ( + + + {completed ? ( + + ) : null} + + {text} + + ); +} + const todoListCardStylesheet = StyleSheet.create((theme) => ({ detailsWrapper: { padding: theme.spacing[2], @@ -1800,28 +1853,11 @@ export const TodoListCard = memo(function TodoListCard({ No tasks yet. ) : ( items.map((item, idx) => ( - - - {item.completed ? ( - - ) : null} - - - {item.text} - - + )) )} @@ -2104,11 +2140,10 @@ const ExpandableBadge = memo(function ExpandableBadge({ ); const IconComponent = icon; - const iconColor = isError - ? theme.colors.destructive - : isActive - ? theme.colors.foreground - : theme.colors.mutedForeground; + let iconColor: string; + if (isError) iconColor = theme.colors.destructive; + else if (isActive) iconColor = theme.colors.foreground; + else iconColor = theme.colors.mutedForeground; let iconNode: ReactNode = null; if (isError) {