chore(lint): no-array-index-key in app

This commit is contained in:
Mohamed Boudra
2026-04-24 03:32:57 +07:00
parent ee7a43726f
commit 2eed4a3f60
11 changed files with 78 additions and 50 deletions

View File

@@ -95,7 +95,7 @@ export function CalloutCard({
<View style={styles.actionRow} testID={testID ? `${testID}-actions` : undefined}>
{visibleActions.map((action, index) => (
<CalloutActionButton
key={`${action.label}-${index}`}
key={action.label}
action={action}
testID={action.testID ?? (testID ? `${testID}-action-${index}` : undefined)}
/>

View File

@@ -1156,7 +1156,7 @@ export function Composer({
if (attachment.kind === "image") {
return (
<ImageAttachmentPill
key={`${attachment.metadata.id}-${index}`}
key={attachment.metadata.id}
attachment={attachment}
index={index}
disabled={isComposerLocked}

View File

@@ -46,8 +46,12 @@ function DiffLineRow({ line }: { line: DiffLine }) {
<Text style={line.type === "add" ? styles.addText : styles.removeText}>
{line.content[0]}
</Text>
{line.segments.map((segment, segIdx) => (
<DiffSegment key={segIdx} segment={segment} lineType={line.type} />
{line.segments.map((segment) => (
<DiffSegment
key={`${segment.changed ? "c" : "u"}:${segment.text}`}
segment={segment}
lineType={line.type}
/>
))}
</Text>
) : (
@@ -101,6 +105,10 @@ export function DiffViewer({
() => [styles.linesContainer, scrollViewWidth > 0 && { minWidth: scrollViewWidth }],
[scrollViewWidth],
);
const keyedDiffLines = React.useMemo(
() => diffLines.map((line, index) => ({ key: `${index}-${line.type}-${line.content}`, line })),
[diffLines],
);
if (!diffLines.length) {
return (
@@ -126,8 +134,8 @@ export function DiffViewer({
onLayout={handleInnerLayout}
>
<View style={linesContainerStyle}>
{diffLines.map((line, index) => (
<DiffLineRow key={`${line.type}-${index}`} line={line} />
{keyedDiffLines.map(({ key, line }) => (
<DiffLineRow key={key} line={line} />
))}
</View>
</ScrollView>

View File

@@ -105,15 +105,19 @@ const CodeLine = React.memo(function CodeLine({
() => [codeLineStyles.gutterText, { color: baseColor }],
[baseColor],
);
const keyedTokens = useMemo(
() => tokens.map((token, index) => ({ key: `${index}-${token.text}`, token })),
[tokens],
);
return (
<View style={codeLineStyles.line}>
<View style={gutterStyle}>
<Text style={gutterTextStyle}>{String(lineNumber)}</Text>
</View>
<Text selectable style={codeLineStyles.lineText}>
{tokens.map((token, index) => (
{keyedTokens.map(({ key, token }) => (
<CodeLineToken
key={index}
key={key}
color={token.style ? (colorMap[token.style] ?? baseColor) : baseColor}
text={token.text}
/>
@@ -238,13 +242,18 @@ function FilePreviewBody({
}
const lines = highlightedLines ?? [[{ text: preview.content ?? "", style: null }]];
const keyedLines = lines.map((tokens, index) => ({
key: `line-${index}`,
tokens,
lineNumber: index + 1,
}));
const codeLines = (
<View>
{lines.map((tokens, index) => (
{keyedLines.map(({ key, tokens, lineNumber }) => (
<CodeLine
key={index}
key={key}
tokens={tokens}
lineNumber={index + 1}
lineNumber={lineNumber}
gutterWidth={gutterWidth}
colorMap={colorMap}
baseColor={baseColor}

View File

@@ -162,11 +162,16 @@ function HighlightedText({ tokens, wrapLines = false }: HighlightedTextProps) {
[lineHeight, wrapLines],
);
const keyedTokens = useMemo(
() => tokens.map((token, index) => ({ key: `${index}-${token.text}`, token })),
[tokens],
);
return (
<Text style={containerStyle}>
{tokens.map((token, index) => (
{keyedTokens.map(({ key, token }) => (
<HighlightedToken
key={index}
key={key}
text={token.text}
color={getTokenColor(token.style)}
lineHeight={lineHeight}
@@ -422,21 +427,23 @@ function SplitDiffColumn({
[scrollWidth],
);
const keyedRows = useMemo(() => rows.map((row, i) => ({ key: `row-${i}`, row })), [rows]);
if (wrapLines) {
return (
<View style={wrapCellStyle}>
<View style={styles.linesContainer}>
{rows.map((row, i) => {
{keyedRows.map(({ key, row }) => {
if (row.kind === "header") {
return (
<View key={`header-${i}`} style={styles.splitHeaderRow}>
<View key={key} style={styles.splitHeaderRow}>
<Text style={HEADER_LINE_TEXT_STYLE}>{row.content}</Text>
</View>
);
}
return (
<SplitDiffLine
key={`line-${i}`}
key={key}
line={side === "left" ? row.left : row.right}
gutterWidth={gutterWidth}
wrapLines={wrapLines}
@@ -451,21 +458,16 @@ function SplitDiffColumn({
return (
<View style={rowCellStyle}>
<View style={styles.gutterColumn}>
{rows.map((row, i) => {
{keyedRows.map(({ key, row }) => {
if (row.kind === "header") {
return (
<DiffGutterCell
key={`g-${i}`}
lineNumber={null}
type="header"
gutterWidth={gutterWidth}
/>
<DiffGutterCell key={key} lineNumber={null} type="header" gutterWidth={gutterWidth} />
);
}
const line = side === "left" ? row.left : row.right;
return (
<DiffGutterCell
key={`g-${i}`}
key={key}
lineNumber={line?.lineNumber ?? null}
type={line?.type}
gutterWidth={gutterWidth}
@@ -480,17 +482,17 @@ function SplitDiffColumn({
contentContainerStyle={styles.diffContentInner}
>
<View style={linesContainerRowStyle}>
{rows.map((row, i) => {
{keyedRows.map(({ key, row }) => {
if (row.kind === "header") {
return (
<View key={`t-${i}`} style={styles.splitHeaderRow}>
<View key={key} style={styles.splitHeaderRow}>
<Text style={HEADER_LINE_TEXT_STYLE}>{row.content}</Text>
</View>
);
}
return (
<SplitTextLine
key={`t-${i}`}
key={key}
line={side === "left" ? row.left : row.right}
wrapLines={false}
/>

View File

@@ -421,8 +421,8 @@ export const UserMessage = memo(function UserMessage({
<View style={userMessageStylesheet.bubble}>
{hasImages ? (
<View style={imagePreviewContainerStyle}>
{images.map((image, index) => (
<View key={`${image.id}-${index}`} style={userMessageStylesheet.imagePill}>
{images.map((image) => (
<View key={image.id} style={userMessageStylesheet.imagePill}>
<UserMessageAttachmentThumbnail image={image} />
</View>
))}
@@ -1416,6 +1416,10 @@ export const AssistantMessage = memo(function AssistantMessage({
}, [client, handleLinkPress, markdownParser, onInlinePathPress, serverId, workspaceRoot]);
const blocks = useMemo(() => splitMarkdownBlocks(message), [message]);
const keyedBlocks = useMemo(
() => blocks.map((block, index) => ({ key: `${index}:${block.slice(0, 32)}`, block })),
[blocks],
);
const assistantContainerStyle = useMemo(
() => [
@@ -1431,10 +1435,10 @@ export const AssistantMessage = memo(function AssistantMessage({
return (
<View testID="assistant-message" style={assistantContainerStyle}>
{blocks.map((block, index) => (
{keyedBlocks.map(({ key, block }, index) => (
<AssistantMessageBlockContainer
key={index}
marginBottom={index < blocks.length - 1 ? theme.spacing[3] : 0}
key={key}
marginBottom={index < keyedBlocks.length - 1 ? theme.spacing[3] : 0}
>
<MemoizedMarkdownBlock
text={block}
@@ -1844,12 +1848,8 @@ export const TodoListCard = memo(function TodoListCard({
{items.length === 0 ? (
<Text style={todoListCardStylesheet.emptyText}>No tasks yet.</Text>
) : (
items.map((item, idx) => (
<TodoListItemRow
key={`${item.text}-${idx}`}
text={item.text}
completed={item.completed}
/>
items.map((item) => (
<TodoListItemRow key={item.text} text={item.text} completed={item.completed} />
))
)}
</View>

View File

@@ -67,6 +67,10 @@ export function PrPane({ data }: { data: PrPaneData }) {
const StateIcon = getStateIcon(data.state);
const stateLabel = getStateLabel(data.state);
const stateLabelStyle = useMemo(() => [styles.stateLabel, { color: stateColor }], [stateColor]);
const keyedActivity = useMemo(
() => data.activity.map((item, idx) => ({ key: `${item.author}-${item.kind}-${idx}`, item })),
[data.activity],
);
return (
<View style={styles.root}>
@@ -116,8 +120,8 @@ export function PrPane({ data }: { data: PrPaneData }) {
</>
}
>
{data.checks.map((check, idx) => (
<CheckRow key={`${check.name}-${idx}`} check={check} />
{data.checks.map((check) => (
<CheckRow key={check.name} check={check} />
))}
</Section>
@@ -147,8 +151,8 @@ export function PrPane({ data }: { data: PrPaneData }) {
</>
}
>
{data.activity.map((item, idx) => (
<ActivityRow key={`${item.author}-${idx}`} item={item} />
{keyedActivity.map(({ key, item }) => (
<ActivityRow key={key} item={item} />
))}
</Section>
</View>

View File

@@ -356,7 +356,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
const otherText = otherTexts[qIndex] ?? "";
return (
<View key={qIndex} style={styles.questionBlock}>
<View key={q.question} style={styles.questionBlock}>
<View style={styles.questionHeader}>
<Text style={questionTextStyle}>{q.question}</Text>
<CircleHelp size={14} color={theme.colors.foregroundMuted} />
@@ -364,7 +364,7 @@ export function QuestionFormCard({ permission, onRespond, isResponding }: Questi
<View style={styles.optionsWrap}>
{q.options.map((opt, optIndex) => (
<QuestionOptionRow
key={optIndex}
key={opt.label}
qIndex={qIndex}
optIndex={optIndex}
option={opt}

View File

@@ -3,6 +3,10 @@ import { Animated, View, type StyleProp, type ViewStyle } from "react-native";
import { StyleSheet } from "react-native-unistyles";
const SECTION_OPACITIES: readonly number[] = [1, 0.7, 0.4];
const SECTION_KEYS = SECTION_OPACITIES.map((_, i) => `skeleton-section-${i}`);
const ROW_KEYS_BY_SECTION: readonly (readonly string[])[] = SECTION_OPACITIES.map((_, sIdx) =>
[0, 1, 2].map((r) => `skeleton-row-${sIdx}-${r}`),
);
function SkeletonPulse({ pulse, style }: { pulse: Animated.Value; style: StyleProp<ViewStyle> }) {
const opacity = pulse.interpolate({
@@ -37,8 +41,8 @@ function SkeletonSection({
</View>
<View style={styles.rows}>
{Array.from({ length: 3 }).map((__, rowIdx) => (
<View key={`skeleton-row-${sectionIdx}-${rowIdx}`} style={styles.row}>
{ROW_KEYS_BY_SECTION[sectionIdx]?.map((key) => (
<View key={key} style={styles.row}>
<SkeletonPulse pulse={pulse} style={styles.rowDot} />
<SkeletonPulse pulse={pulse} style={styles.rowTitle} />
<SkeletonPulse pulse={pulse} style={styles.rowBadge} />
@@ -76,7 +80,7 @@ export function SidebarAgentListSkeleton() {
<View style={styles.container}>
{SECTION_OPACITIES.map((sectionOpacity, sectionIdx) => (
<SkeletonSection
key={`skeleton-section-${sectionIdx}`}
key={SECTION_KEYS[sectionIdx]}
pulse={pulse}
sectionOpacity={sectionOpacity}
sectionIdx={sectionIdx}

View File

@@ -16,6 +16,7 @@ const DOT_COUNT = DOT_SEQUENCE.length;
const GRID_COLUMNS = 2;
const SNAKE_SEGMENT_OFFSETS = [0, -1, -2, -3, -4] as const;
const SNAKE_OPACITIES = [1, 0.78, 0.56, 0.34, 0] as const;
const DOT_KEYS = Array.from({ length: DOT_COUNT }, (_, i) => `dot-${i}`);
const sharedStepProgress = makeMutable(0);
let sharedLoopStarted = false;
@@ -91,7 +92,7 @@ export function SyncedLoader({ size = 10, color }: { size?: number; color: strin
return (
<SpinnerDot
key={dotIndex}
key={DOT_KEYS[dotIndex]}
color={color}
dotSize={dotSize}
sequenceIndex={sequenceIndex}

View File

@@ -37,9 +37,9 @@ export function Shortcut({
return (
<View style={sequenceStyle}>
{displayChord.map(function (combo, index) {
{displayChord.map(function (combo) {
return (
<View key={`${combo.join("+")}-${index}`} style={styles.badge}>
<View key={combo.join("+")} style={styles.badge}>
<Text style={textCombinedStyle}>{formatShortcut(combo, shortcutOs)}</Text>
</View>
);