perf: reduce active-turn chat rerenders during streaming
- Keep older assistant messages in the active turn stable while new content streams - Prevent existing tool rows from rerendering when new tool activity is appended - Tighten message and tool memo comparisons to isolate render work to changed rows
This commit is contained in:
@@ -30,7 +30,7 @@ import type { TurnGroupingContext } from './lib/turns/types';
|
||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { FadeInOnReveal } from './message/FadeInOnReveal';
|
||||
import { streamPerfCount } from '@/stores/utils/streamDebug';
|
||||
import { areOptionalRenderRelevantMessagesEqual, areRenderRelevantMessageInfoEqual, areRenderRelevantPartsEqual } from './message/renderCompare';
|
||||
import { areOptionalRenderRelevantMessagesEqual, areRelevantTurnGroupingContextsEqual, areRenderRelevantMessageInfoEqual, areRenderRelevantPartsEqual } from './message/renderCompare';
|
||||
|
||||
const ToolOutputDialog = React.lazy(() => import('./message/ToolOutputDialog'));
|
||||
|
||||
@@ -1129,12 +1129,14 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
};
|
||||
|
||||
export default React.memo(ChatMessage, (prev, next) => {
|
||||
const prevRole = deriveMessageRole(prev.message.info);
|
||||
|
||||
return areRenderRelevantMessageInfoEqual(prev.message.info, next.message.info)
|
||||
&& areRenderRelevantPartsEqual(prev.message.parts, next.message.parts)
|
||||
&& areOptionalRenderRelevantMessagesEqual(prev.previousMessage, next.previousMessage)
|
||||
&& areOptionalRenderRelevantMessagesEqual(prev.nextMessage, next.nextMessage)
|
||||
&& prev.onContentChange === next.onContentChange
|
||||
&& prev.turnGroupingContext === next.turnGroupingContext
|
||||
&& areRelevantTurnGroupingContextsEqual(prev.turnGroupingContext, next.turnGroupingContext, prev.message.info.id, prevRole.isUser)
|
||||
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId
|
||||
&& prev.isInActiveTurn === next.isInActiveTurn
|
||||
&& prev.activeStreamingPhase === next.activeStreamingPhase
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Part } from '@opencode-ai/sdk/v2';
|
||||
import { measureElement as measureVirtualElement, type VirtualItem, useVirtualizer } from '@tanstack/react-virtual';
|
||||
|
||||
import ChatMessage from './ChatMessage';
|
||||
import { areOptionalRenderRelevantMessagesEqual, areRenderRelevantMessagesEqual } from './message/renderCompare';
|
||||
import { areOptionalRenderRelevantMessagesEqual, areRelevantTurnGroupingContextsEqual, areRenderRelevantMessagesEqual } from './message/renderCompare';
|
||||
import TurnItem from './components/TurnItem';
|
||||
import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatScrollManager';
|
||||
import { filterSyntheticParts } from '@/lib/messages/synthetic';
|
||||
@@ -480,15 +480,7 @@ const MessageRow = React.memo<MessageRowProps>(({
|
||||
&& prev.onUserAnimationConsumed === next.onUserAnimationConsumed
|
||||
&& prev.onContentChange === next.onContentChange
|
||||
&& prev.scrollToBottom === next.scrollToBottom
|
||||
&& prevTurn?.turnId === nextTurn?.turnId
|
||||
&& prevTurn?.isFirstAssistantInTurn === nextTurn?.isFirstAssistantInTurn
|
||||
&& prevTurn?.isLastAssistantInTurn === nextTurn?.isLastAssistantInTurn
|
||||
&& prevTurn?.activityOwnerMessageId === nextTurn?.activityOwnerMessageId
|
||||
&& prevTurn?.isWorking === nextTurn?.isWorking
|
||||
&& prevTurn?.isGroupExpanded === nextTurn?.isGroupExpanded
|
||||
&& prevTurn?.toggleGroup === nextTurn?.toggleGroup
|
||||
&& prevTurn?.activityGroupSegments === nextTurn?.activityGroupSegments
|
||||
&& prevTurn?.activityParts === nextTurn?.activityParts
|
||||
&& areRelevantTurnGroupingContextsEqual(prevTurn, nextTurn, prev.message.info.id, resolveMessageRole(prev.message) === 'user')
|
||||
&& prev.assistantHeaderMessageId === next.assistantHeaderMessageId
|
||||
&& prev.isInActiveTurn === next.isInActiveTurn
|
||||
&& prev.activeStreamingPhase === next.activeStreamingPhase
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import ReasoningPart from './ReasoningPart';
|
||||
import JustificationBlock from './JustificationBlock';
|
||||
import { areRenderRelevantPartsEqual } from '../renderCompare';
|
||||
|
||||
interface ProgressiveGroupProps {
|
||||
parts: TurnActivityPart[];
|
||||
@@ -380,6 +381,115 @@ type AggregatedRow =
|
||||
| { type: 'justification'; activity: TurnActivityPart }
|
||||
| { type: 'tool-fallback'; activity: TurnActivityPart };
|
||||
|
||||
interface ExpandableToolRowProps {
|
||||
activity: TurnActivityPart;
|
||||
isExpanded: boolean;
|
||||
syntaxTheme: Record<string, React.CSSProperties>;
|
||||
isMobile: boolean;
|
||||
onToggleTool: (toolId: string) => void;
|
||||
onShowPopup: (content: ToolPopupContent) => void;
|
||||
onContentChange?: (reason?: ContentChangeReason) => void;
|
||||
animateTailText: boolean;
|
||||
animateRows: boolean;
|
||||
}
|
||||
|
||||
const ExpandableToolRow: React.FC<ExpandableToolRowProps> = ({
|
||||
activity,
|
||||
isExpanded,
|
||||
syntaxTheme,
|
||||
isMobile,
|
||||
onToggleTool,
|
||||
onShowPopup,
|
||||
onContentChange,
|
||||
animateTailText,
|
||||
animateRows,
|
||||
}) => {
|
||||
const handleToggle = React.useCallback(() => {
|
||||
onToggleTool(activity.id);
|
||||
}, [activity.id, onToggleTool]);
|
||||
|
||||
const content = (
|
||||
<ToolPart
|
||||
part={activity.part as ToolPartType}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={handleToggle}
|
||||
syntaxTheme={syntaxTheme}
|
||||
isMobile={isMobile}
|
||||
onContentChange={onContentChange}
|
||||
onShowPopup={onShowPopup}
|
||||
animateTailText={animateTailText}
|
||||
/>
|
||||
);
|
||||
|
||||
const maybeWrapped = animateTailText ? (
|
||||
<ToolRevealOnMount animate={true} wipe>
|
||||
{content}
|
||||
</ToolRevealOnMount>
|
||||
) : content;
|
||||
|
||||
if (!animateRows) {
|
||||
return maybeWrapped;
|
||||
}
|
||||
|
||||
return <FadeInOnReveal>{maybeWrapped}</FadeInOnReveal>;
|
||||
};
|
||||
|
||||
const MemoExpandableToolRow = React.memo(ExpandableToolRow, (prev, next) => {
|
||||
return prev.isExpanded === next.isExpanded
|
||||
&& prev.syntaxTheme === next.syntaxTheme
|
||||
&& prev.isMobile === next.isMobile
|
||||
&& prev.onToggleTool === next.onToggleTool
|
||||
&& prev.onShowPopup === next.onShowPopup
|
||||
&& prev.onContentChange === next.onContentChange
|
||||
&& prev.animateTailText === next.animateTailText
|
||||
&& prev.animateRows === next.animateRows
|
||||
&& prev.activity.id === next.activity.id
|
||||
&& prev.activity.kind === next.activity.kind
|
||||
&& prev.activity.endedAt === next.activity.endedAt
|
||||
&& areRenderRelevantPartsEqual([prev.activity.part], [next.activity.part]);
|
||||
});
|
||||
|
||||
interface StaticGroupedToolRowProps {
|
||||
toolName: string;
|
||||
activities: TurnActivityPart[];
|
||||
animateTailText: boolean;
|
||||
animateRows: boolean;
|
||||
}
|
||||
|
||||
const StaticGroupedToolRow: React.FC<StaticGroupedToolRowProps> = ({
|
||||
toolName,
|
||||
activities,
|
||||
animateTailText,
|
||||
animateRows,
|
||||
}) => {
|
||||
const content = (
|
||||
<StaticToolRow
|
||||
toolName={toolName}
|
||||
activities={activities}
|
||||
animateTailText={animateTailText}
|
||||
/>
|
||||
);
|
||||
|
||||
const maybeWrapped = animateTailText ? (
|
||||
<ToolRevealOnMount animate={true} wipe>
|
||||
{content}
|
||||
</ToolRevealOnMount>
|
||||
) : content;
|
||||
|
||||
if (!animateRows) {
|
||||
return maybeWrapped;
|
||||
}
|
||||
|
||||
return <FadeInOnReveal>{maybeWrapped}</FadeInOnReveal>;
|
||||
};
|
||||
|
||||
const MemoStaticGroupedToolRow = React.memo(StaticGroupedToolRow, (prev, next) => {
|
||||
return prev.toolName === next.toolName
|
||||
&& prev.animateTailText === next.animateTailText
|
||||
&& prev.animateRows === next.animateRows
|
||||
&& areActivityListsEqual(prev.activities, next.activities);
|
||||
});
|
||||
|
||||
/**
|
||||
* Aggregate sorted activity parts into display rows.
|
||||
* Static tools are rendered as one row per call.
|
||||
@@ -440,7 +550,36 @@ const aggregateRows = (parts: TurnActivityPart[]): AggregatedRow[] => {
|
||||
* Render a static aggregated tool row.
|
||||
* Shows: [icon] DisplayName file1.tsx file2.tsx ...
|
||||
*/
|
||||
export const StaticToolRow: React.FC<{
|
||||
const areActivityListsEqual = (left: TurnActivityPart[], right: TurnActivityPart[]): boolean => {
|
||||
if (left === right) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < left.length; index += 1) {
|
||||
const leftActivity = left[index];
|
||||
const rightActivity = right[index];
|
||||
|
||||
if (leftActivity.id !== rightActivity.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (leftActivity.kind !== rightActivity.kind || leftActivity.endedAt !== rightActivity.endedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!areRenderRelevantPartsEqual([leftActivity.part], [rightActivity.part])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const StaticToolRowInner: React.FC<{
|
||||
toolName: string;
|
||||
activities: TurnActivityPart[];
|
||||
animateTailText: boolean;
|
||||
@@ -590,6 +729,12 @@ export const StaticToolRow: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export const StaticToolRow = React.memo(StaticToolRowInner, (prev, next) => {
|
||||
return prev.toolName === next.toolName
|
||||
&& prev.animateTailText === next.animateTailText
|
||||
&& areActivityListsEqual(prev.activities, next.activities);
|
||||
});
|
||||
|
||||
/**
|
||||
* Inline reasoning text block — rendered as dimmed italic markdown.
|
||||
*/
|
||||
@@ -683,18 +828,6 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
||||
return <FadeInOnReveal key={key}>{content}</FadeInOnReveal>;
|
||||
};
|
||||
|
||||
const renderToolRow = (key: string, content: React.ReactNode, animate: boolean) => {
|
||||
if (!animate) {
|
||||
return wrapRow(key, content);
|
||||
}
|
||||
return wrapRow(
|
||||
key,
|
||||
<ToolRevealOnMount animate={true} wipe>
|
||||
{content}
|
||||
</ToolRevealOnMount>
|
||||
);
|
||||
};
|
||||
|
||||
const renderedRows = shouldRenderRows
|
||||
? visibleRows.map((row, index) => {
|
||||
switch (row.type) {
|
||||
@@ -721,52 +854,46 @@ const ProgressiveGroup: React.FC<ProgressiveGroupProps> = ({
|
||||
);
|
||||
|
||||
case 'tool-expandable':
|
||||
return renderToolRow(
|
||||
row.activity.id,
|
||||
<>
|
||||
<ToolPart
|
||||
part={row.activity.part as ToolPartType}
|
||||
isExpanded={expandedTools.has(row.activity.id)}
|
||||
onToggle={() => onToggleTool(row.activity.id)}
|
||||
syntaxTheme={syntaxTheme}
|
||||
isMobile={isMobile}
|
||||
onContentChange={onContentChange}
|
||||
onShowPopup={onShowPopup}
|
||||
animateTailText={Boolean(animatedToolIds?.has(row.activity.id))}
|
||||
/>
|
||||
</>,
|
||||
Boolean(animatedToolIds?.has(row.activity.id))
|
||||
return (
|
||||
<MemoExpandableToolRow
|
||||
key={row.activity.id}
|
||||
activity={row.activity}
|
||||
isExpanded={expandedTools.has(row.activity.id)}
|
||||
syntaxTheme={syntaxTheme}
|
||||
isMobile={isMobile}
|
||||
onToggleTool={onToggleTool}
|
||||
onShowPopup={onShowPopup}
|
||||
onContentChange={onContentChange}
|
||||
animateTailText={Boolean(animatedToolIds?.has(row.activity.id))}
|
||||
animateRows={animateRows}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'tool-static-group':
|
||||
return renderToolRow(
|
||||
`static-${row.toolName}-${row.activities[0]?.id ?? index}`,
|
||||
<>
|
||||
<StaticToolRow
|
||||
toolName={row.toolName}
|
||||
activities={row.activities}
|
||||
animateTailText={row.activities.some((activity) => animatedToolIds?.has(activity.id))}
|
||||
/>
|
||||
</>,
|
||||
row.activities.some((activity) => animatedToolIds?.has(activity.id))
|
||||
return (
|
||||
<MemoStaticGroupedToolRow
|
||||
key={`static-${row.toolName}-${row.activities[0]?.id ?? index}`}
|
||||
toolName={row.toolName}
|
||||
activities={row.activities}
|
||||
animateTailText={row.activities.some((activity) => animatedToolIds?.has(activity.id))}
|
||||
animateRows={animateRows}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'tool-fallback':
|
||||
return renderToolRow(
|
||||
row.activity.id,
|
||||
<>
|
||||
<ToolPart
|
||||
part={row.activity.part as ToolPartType}
|
||||
isExpanded={expandedTools.has(row.activity.id)}
|
||||
onToggle={() => onToggleTool(row.activity.id)}
|
||||
syntaxTheme={syntaxTheme}
|
||||
isMobile={isMobile}
|
||||
onContentChange={onContentChange}
|
||||
onShowPopup={onShowPopup}
|
||||
animateTailText={Boolean(animatedToolIds?.has(row.activity.id))}
|
||||
/>
|
||||
</>,
|
||||
Boolean(animatedToolIds?.has(row.activity.id))
|
||||
return (
|
||||
<MemoExpandableToolRow
|
||||
key={row.activity.id}
|
||||
activity={row.activity}
|
||||
isExpanded={expandedTools.has(row.activity.id)}
|
||||
syntaxTheme={syntaxTheme}
|
||||
isMobile={isMobile}
|
||||
onToggleTool={onToggleTool}
|
||||
onShowPopup={onShowPopup}
|
||||
onContentChange={onContentChange}
|
||||
animateTailText={Boolean(animatedToolIds?.has(row.activity.id))}
|
||||
animateRows={animateRows}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
|
||||
@@ -40,6 +40,7 @@ import { ToolRevealOnMount } from './ToolRevealOnMount';
|
||||
import { getToolIcon } from './toolPresentation';
|
||||
import { useDurationTickerNow } from './useDurationTicker';
|
||||
import { resolveFallbackTaskSessionId } from './resolveFallbackTaskSessionId';
|
||||
import { areRenderRelevantPartsEqual } from '../renderCompare';
|
||||
|
||||
type ToolStateWithMetadata = ToolStateUnion & { metadata?: Record<string, unknown>; input?: Record<string, unknown>; output?: string; error?: string; time?: { start: number; end?: number } };
|
||||
|
||||
@@ -2472,4 +2473,12 @@ const ToolPart: React.FC<ToolPartProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolPart;
|
||||
export default React.memo(ToolPart, (prev, next) => {
|
||||
return areRenderRelevantPartsEqual([prev.part], [next.part])
|
||||
&& prev.isExpanded === next.isExpanded
|
||||
&& prev.syntaxTheme === next.syntaxTheme
|
||||
&& prev.isMobile === next.isMobile
|
||||
&& prev.onContentChange === next.onContentChange
|
||||
&& prev.onShowPopup === next.onShowPopup
|
||||
&& prev.animateTailText === next.animateTailText;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Message, Part } from '@opencode-ai/sdk/v2';
|
||||
import type { TurnActivityGroup, TurnActivityRecord, TurnDiffStats, TurnGroupingContext } from '../lib/turns/types';
|
||||
|
||||
type MessageRecord = {
|
||||
info: Message;
|
||||
@@ -113,3 +114,171 @@ export const areOptionalRenderRelevantMessagesEqual = (left?: MessageRecord, rig
|
||||
}
|
||||
return areRenderRelevantMessagesEqual(left, right);
|
||||
};
|
||||
|
||||
const areTurnDiffStatsEqual = (left?: TurnDiffStats, right?: TurnDiffStats): boolean => {
|
||||
if (!left || !right) {
|
||||
return left === right;
|
||||
}
|
||||
|
||||
return left.additions === right.additions
|
||||
&& left.deletions === right.deletions
|
||||
&& left.files === right.files;
|
||||
};
|
||||
|
||||
const areTurnActivityRecordsEqual = (left: TurnActivityRecord, right: TurnActivityRecord): boolean => {
|
||||
return left.id === right.id
|
||||
&& left.messageId === right.messageId
|
||||
&& left.kind === right.kind
|
||||
&& left.partIndex === right.partIndex
|
||||
&& left.endedAt === right.endedAt
|
||||
&& areRenderRelevantPartsEqual([left.part], [right.part]);
|
||||
};
|
||||
|
||||
const areRelevantActivityPartsEqual = (
|
||||
left: TurnActivityRecord[] | undefined,
|
||||
right: TurnActivityRecord[] | undefined,
|
||||
messageId: string,
|
||||
): boolean => {
|
||||
let leftIndex = 0;
|
||||
let rightIndex = 0;
|
||||
|
||||
while (true) {
|
||||
while (leftIndex < (left?.length ?? 0) && left?.[leftIndex]?.messageId !== messageId) {
|
||||
leftIndex += 1;
|
||||
}
|
||||
while (rightIndex < (right?.length ?? 0) && right?.[rightIndex]?.messageId !== messageId) {
|
||||
rightIndex += 1;
|
||||
}
|
||||
|
||||
const leftRecord = left?.[leftIndex];
|
||||
const rightRecord = right?.[rightIndex];
|
||||
|
||||
if (!leftRecord || !rightRecord) {
|
||||
return leftRecord === rightRecord;
|
||||
}
|
||||
|
||||
if (!areTurnActivityRecordsEqual(leftRecord, rightRecord)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
leftIndex += 1;
|
||||
rightIndex += 1;
|
||||
}
|
||||
};
|
||||
|
||||
const areTurnActivityGroupsEqual = (left: TurnActivityGroup, right: TurnActivityGroup): boolean => {
|
||||
if (left.id !== right.id || left.anchorMessageId !== right.anchorMessageId || left.afterToolPartId !== right.afterToolPartId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (left.parts.length !== right.parts.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < left.parts.length; index += 1) {
|
||||
if (!areTurnActivityRecordsEqual(left.parts[index], right.parts[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const hasRelevantActivitySegments = (segments: TurnActivityGroup[] | undefined, messageId: string): boolean => {
|
||||
return Boolean(segments?.some((segment) => segment.anchorMessageId === messageId));
|
||||
};
|
||||
|
||||
const areRelevantActivitySegmentsEqual = (
|
||||
left: TurnActivityGroup[] | undefined,
|
||||
right: TurnActivityGroup[] | undefined,
|
||||
messageId: string,
|
||||
): boolean => {
|
||||
let leftIndex = 0;
|
||||
let rightIndex = 0;
|
||||
|
||||
while (true) {
|
||||
while (leftIndex < (left?.length ?? 0) && left?.[leftIndex]?.anchorMessageId !== messageId) {
|
||||
leftIndex += 1;
|
||||
}
|
||||
while (rightIndex < (right?.length ?? 0) && right?.[rightIndex]?.anchorMessageId !== messageId) {
|
||||
rightIndex += 1;
|
||||
}
|
||||
|
||||
const leftSegment = left?.[leftIndex];
|
||||
const rightSegment = right?.[rightIndex];
|
||||
|
||||
if (!leftSegment || !rightSegment) {
|
||||
return leftSegment === rightSegment;
|
||||
}
|
||||
|
||||
if (!areTurnActivityGroupsEqual(leftSegment, rightSegment)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
leftIndex += 1;
|
||||
rightIndex += 1;
|
||||
}
|
||||
};
|
||||
|
||||
export const areRelevantTurnGroupingContextsEqual = (
|
||||
left: TurnGroupingContext | undefined,
|
||||
right: TurnGroupingContext | undefined,
|
||||
messageId: string,
|
||||
isUserMessage: boolean,
|
||||
): boolean => {
|
||||
if (left === right) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!left || !right) {
|
||||
return left === right;
|
||||
}
|
||||
|
||||
if (isUserMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (left.turnId !== right.turnId) return false;
|
||||
if (left.isFirstAssistantInTurn !== right.isFirstAssistantInTurn) return false;
|
||||
if (left.isLastAssistantInTurn !== right.isLastAssistantInTurn) return false;
|
||||
if (left.isWorking !== right.isWorking) return false;
|
||||
if (left.hasTools !== right.hasTools) return false;
|
||||
if (left.hasReasoning !== right.hasReasoning) return false;
|
||||
if (left.userMessageCreatedAt !== right.userMessageCreatedAt) return false;
|
||||
if (left.userMessageVariant !== right.userMessageVariant) return false;
|
||||
|
||||
const headerRelevant = left.headerMessageId === messageId || right.headerMessageId === messageId;
|
||||
if (headerRelevant && left.headerMessageId !== right.headerMessageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ownerRelevant = left.activityOwnerMessageId === messageId || right.activityOwnerMessageId === messageId;
|
||||
if (ownerRelevant && left.activityOwnerMessageId !== right.activityOwnerMessageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!areRelevantActivityPartsEqual(left.activityParts, right.activityParts, messageId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!areRelevantActivitySegmentsEqual(left.activityGroupSegments, right.activityGroupSegments, messageId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const segmentsRelevant = hasRelevantActivitySegments(left.activityGroupSegments, messageId)
|
||||
|| hasRelevantActivitySegments(right.activityGroupSegments, messageId);
|
||||
|
||||
if ((ownerRelevant || segmentsRelevant) && left.isGroupExpanded !== right.isGroupExpanded) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ownerRelevant || segmentsRelevant) && left.toggleGroup !== right.toggleGroup) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ownerRelevant || segmentsRelevant) && !areTurnDiffStatsEqual(left.diffStats, right.diffStats)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user