perf(renderer): gate timeline live lead props
This commit is contained in:
parent
a4ad5f6297
commit
ad2f602cba
2 changed files with 156 additions and 27 deletions
|
|
@ -53,7 +53,11 @@ import {
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import { ActivityTimeline, type TimelineViewport } from '../activity/ActivityTimeline';
|
import { ActivityTimeline, type TimelineViewport } from '../activity/ActivityTimeline';
|
||||||
import { getThoughtGroupKey, groupTimelineItems } from '../activity/LeadThoughtsGroup';
|
import {
|
||||||
|
getThoughtGroupKey,
|
||||||
|
groupTimelineItems,
|
||||||
|
isLeadThought,
|
||||||
|
} from '../activity/LeadThoughtsGroup';
|
||||||
import { MessageExpandDialog } from '../activity/MessageExpandDialog';
|
import { MessageExpandDialog } from '../activity/MessageExpandDialog';
|
||||||
import { CollapsibleTeamSection } from '../CollapsibleTeamSection';
|
import { CollapsibleTeamSection } from '../CollapsibleTeamSection';
|
||||||
import {
|
import {
|
||||||
|
|
@ -691,6 +695,15 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
searchQuery: messagesSearchQuery,
|
searchQuery: messagesSearchQuery,
|
||||||
});
|
});
|
||||||
}, [effectiveMessages, leadNames, messagesFilter, messagesSearchQuery, timeWindow]);
|
}, [effectiveMessages, leadNames, messagesFilter, messagesSearchQuery, timeWindow]);
|
||||||
|
const firstTimelineMessage = activityTimelineMessages[0];
|
||||||
|
const hasVisibleCurrentLeadThought =
|
||||||
|
firstTimelineMessage != null &&
|
||||||
|
isLeadThought(firstTimelineMessage) &&
|
||||||
|
(currentLeadSessionId ? firstTimelineMessage.leadSessionId === currentLeadSessionId : true);
|
||||||
|
const timelineLeadActivity = hasVisibleCurrentLeadThought ? leadActivity : undefined;
|
||||||
|
const timelineLeadContextUpdatedAt = hasVisibleCurrentLeadThought
|
||||||
|
? leadContextUpdatedAt
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const hasTrackedPendingReplies = useMemo(
|
const hasTrackedPendingReplies = useMemo(
|
||||||
() => Object.keys(pendingRepliesByMember).length > 0,
|
() => Object.keys(pendingRepliesByMember).length > 0,
|
||||||
|
|
@ -1175,8 +1188,8 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
onToggleExpandOverride={toggleExpandOverride}
|
onToggleExpandOverride={toggleExpandOverride}
|
||||||
currentLeadSessionId={currentLeadSessionId}
|
currentLeadSessionId={currentLeadSessionId}
|
||||||
isTeamAlive={isTeamAlive}
|
isTeamAlive={isTeamAlive}
|
||||||
leadActivity={leadActivity}
|
leadActivity={timelineLeadActivity}
|
||||||
leadContextUpdatedAt={leadContextUpdatedAt}
|
leadContextUpdatedAt={timelineLeadContextUpdatedAt}
|
||||||
teamNames={teamNames}
|
teamNames={teamNames}
|
||||||
teamColorByName={teamColorByName}
|
teamColorByName={teamColorByName}
|
||||||
onTeamClick={openTeamTab}
|
onTeamClick={openTeamTab}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ const readHookState = {
|
||||||
markRead: vi.fn(),
|
markRead: vi.fn(),
|
||||||
markAllRead: vi.fn(),
|
markAllRead: vi.fn(),
|
||||||
};
|
};
|
||||||
|
const activityTimelineRenderSpy = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
const expandedHookState = {
|
const expandedHookState = {
|
||||||
expandedSet: new Set<string>(),
|
expandedSet: new Set<string>(),
|
||||||
|
|
@ -161,37 +162,44 @@ vi.mock('@renderer/components/team/activity/ActivityTimeline', () => ({
|
||||||
loading,
|
loading,
|
||||||
revisionMessageId,
|
revisionMessageId,
|
||||||
onReviseMessage,
|
onReviseMessage,
|
||||||
|
leadActivity,
|
||||||
|
leadContextUpdatedAt,
|
||||||
}: {
|
}: {
|
||||||
messages: InboxMessage[];
|
messages: InboxMessage[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
revisionMessageId?: string | null;
|
revisionMessageId?: string | null;
|
||||||
onReviseMessage?: (message: InboxMessage) => void;
|
onReviseMessage?: (message: InboxMessage) => void;
|
||||||
|
leadActivity?: string;
|
||||||
|
leadContextUpdatedAt?: string;
|
||||||
}) =>
|
}) =>
|
||||||
React.createElement(
|
(() => {
|
||||||
'div',
|
activityTimelineRenderSpy({ leadActivity, leadContextUpdatedAt, messages });
|
||||||
{ 'data-testid': 'activity-timeline' },
|
return React.createElement(
|
||||||
loading ? React.createElement('div', null, 'timeline-loading') : null,
|
'div',
|
||||||
messages.map((message) =>
|
{ 'data-testid': 'activity-timeline' },
|
||||||
React.createElement(
|
loading ? React.createElement('div', null, 'timeline-loading') : null,
|
||||||
'div',
|
messages.map((message) =>
|
||||||
{
|
React.createElement(
|
||||||
key: message.messageId ?? `${message.from}-${message.timestamp}`,
|
'div',
|
||||||
'data-message-id': message.messageId ?? '',
|
{
|
||||||
},
|
key: message.messageId ?? `${message.from}-${message.timestamp}`,
|
||||||
`${message.messageId ?? 'no-id'}:${message.text}`,
|
'data-message-id': message.messageId ?? '',
|
||||||
message.messageId === revisionMessageId
|
},
|
||||||
? React.createElement(
|
`${message.messageId ?? 'no-id'}:${message.text}`,
|
||||||
'button',
|
message.messageId === revisionMessageId
|
||||||
{
|
? React.createElement(
|
||||||
type: 'button',
|
'button',
|
||||||
onClick: () => onReviseMessage?.(message),
|
{
|
||||||
},
|
type: 'button',
|
||||||
'Edit message'
|
onClick: () => onReviseMessage?.(message),
|
||||||
)
|
},
|
||||||
: null
|
'Edit message'
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
),
|
})(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@renderer/components/team/activity/MessageExpandDialog', () => ({
|
vi.mock('@renderer/components/team/activity/MessageExpandDialog', () => ({
|
||||||
|
|
@ -238,6 +246,7 @@ describe('MessagesPanel idle summary invariants', () => {
|
||||||
readHookState.readSet = new Set<string>();
|
readHookState.readSet = new Set<string>();
|
||||||
readHookState.markRead.mockReset();
|
readHookState.markRead.mockReset();
|
||||||
readHookState.markAllRead.mockReset();
|
readHookState.markAllRead.mockReset();
|
||||||
|
activityTimelineRenderSpy.mockClear();
|
||||||
expandedHookState.expandedSet = new Set<string>();
|
expandedHookState.expandedSet = new Set<string>();
|
||||||
expandedHookState.toggle.mockReset();
|
expandedHookState.toggle.mockReset();
|
||||||
storeState.sendTeamMessage.mockClear();
|
storeState.sendTeamMessage.mockClear();
|
||||||
|
|
@ -335,6 +344,113 @@ describe('MessagesPanel idle summary invariants', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not pass live lead status to timeline when newest visible item is not a current lead thought', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
storeState.teamMessagesByName['atlas-hq'] = {
|
||||||
|
canonicalMessages: [makeMessage({ messageId: 'm-1', text: 'ordinary message' })],
|
||||||
|
optimisticMessages: [],
|
||||||
|
feedRevision: 'rev-1',
|
||||||
|
nextCursor: null,
|
||||||
|
hasMore: false,
|
||||||
|
lastFetchedAt: Date.now(),
|
||||||
|
loadingHead: false,
|
||||||
|
loadingOlder: false,
|
||||||
|
headHydrated: true,
|
||||||
|
};
|
||||||
|
root.render(
|
||||||
|
React.createElement(MessagesPanel, {
|
||||||
|
teamName: 'atlas-hq',
|
||||||
|
position: 'sidebar',
|
||||||
|
onPositionChange: vi.fn(),
|
||||||
|
members: [],
|
||||||
|
tasks: [],
|
||||||
|
isTeamAlive: true,
|
||||||
|
leadActivity: 'active',
|
||||||
|
leadContextUpdatedAt: '2026-05-31T10:00:00.000Z',
|
||||||
|
currentLeadSessionId: 'lead-session-current',
|
||||||
|
timeWindow: null,
|
||||||
|
pendingRepliesByMember: {},
|
||||||
|
onPendingReplyChange: vi.fn(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activityTimelineRenderSpy).toHaveBeenCalled();
|
||||||
|
expect(activityTimelineRenderSpy.mock.lastCall?.[0]).toMatchObject({
|
||||||
|
leadActivity: undefined,
|
||||||
|
leadContextUpdatedAt: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes live lead status when newest visible item is the current lead thought', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
storeState.teamMessagesByName['atlas-hq'] = {
|
||||||
|
canonicalMessages: [
|
||||||
|
makeMessage({
|
||||||
|
from: 'lead',
|
||||||
|
to: undefined,
|
||||||
|
source: 'lead_session',
|
||||||
|
leadSessionId: 'lead-session-current',
|
||||||
|
messageId: 'lead-thought-1',
|
||||||
|
text: 'thinking',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
optimisticMessages: [],
|
||||||
|
feedRevision: 'rev-1',
|
||||||
|
nextCursor: null,
|
||||||
|
hasMore: false,
|
||||||
|
lastFetchedAt: Date.now(),
|
||||||
|
loadingHead: false,
|
||||||
|
loadingOlder: false,
|
||||||
|
headHydrated: true,
|
||||||
|
};
|
||||||
|
root.render(
|
||||||
|
React.createElement(MessagesPanel, {
|
||||||
|
teamName: 'atlas-hq',
|
||||||
|
position: 'sidebar',
|
||||||
|
onPositionChange: vi.fn(),
|
||||||
|
members: [],
|
||||||
|
tasks: [],
|
||||||
|
isTeamAlive: true,
|
||||||
|
leadActivity: 'active',
|
||||||
|
leadContextUpdatedAt: '2026-05-31T10:00:00.000Z',
|
||||||
|
currentLeadSessionId: 'lead-session-current',
|
||||||
|
timeWindow: null,
|
||||||
|
pendingRepliesByMember: {},
|
||||||
|
onPendingReplyChange: vi.fn(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(activityTimelineRenderSpy).toHaveBeenCalled();
|
||||||
|
expect(activityTimelineRenderSpy.mock.lastCall?.[0]).toMatchObject({
|
||||||
|
leadActivity: 'active',
|
||||||
|
leadContextUpdatedAt: '2026-05-31T10:00:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('persists sidebar scroll position after scroll settles', async () => {
|
it('persists sidebar scroll position after scroll settles', async () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue