refactor: streamline message filtering in TeamDetailView
- Replaced manual message filtering logic with a dedicated utility function, `filterTeamMessages`, to enhance readability and maintainability. - Removed the temporary lead member message filtering, simplifying the message handling process. - Updated dependencies in the filtering logic to accommodate new parameters for time window and search query.
This commit is contained in:
parent
1f4c550ed3
commit
b09c4e4fd0
4 changed files with 129 additions and 45 deletions
|
|
@ -27,9 +27,9 @@ import { formatProjectPath } from '@renderer/utils/pathDisplay';
|
||||||
import { buildTaskCountsByOwner, normalizePath } from '@renderer/utils/pathNormalize';
|
import { buildTaskCountsByOwner, normalizePath } from '@renderer/utils/pathNormalize';
|
||||||
import { nameColorSet } from '@renderer/utils/projectColor';
|
import { nameColorSet } from '@renderer/utils/projectColor';
|
||||||
import { resolveProjectIdByPath } from '@renderer/utils/projectLookup';
|
import { resolveProjectIdByPath } from '@renderer/utils/projectLookup';
|
||||||
|
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
||||||
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
||||||
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
|
||||||
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
|
||||||
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { deriveTaskDisplayId, formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
|
@ -600,51 +600,14 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
[data?.members]
|
[data?.members]
|
||||||
);
|
);
|
||||||
|
|
||||||
const leadMemberName = useMemo(
|
|
||||||
() => activeMembers.find((m) => m.agentType === 'team-lead')?.name,
|
|
||||||
[activeMembers]
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredMessages = useMemo(() => {
|
const filteredMessages = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
let list = data.messages;
|
return filterTeamMessages(data.messages, {
|
||||||
// Temporarily hide lead→user messages from the UI
|
timeWindow,
|
||||||
// (notifications and other processing still receive them via data.messages)
|
filter: messagesFilter,
|
||||||
if (leadMemberName) {
|
searchQuery: messagesSearchQuery,
|
||||||
list = list.filter((m) => !(m.to?.trim() === 'user' && m.from?.trim() === leadMemberName));
|
});
|
||||||
}
|
}, [data, timeWindow, messagesFilter, messagesSearchQuery]);
|
||||||
if (timeWindow) {
|
|
||||||
list = list.filter((m) => {
|
|
||||||
const ts = new Date(m.timestamp).getTime();
|
|
||||||
return ts >= timeWindow.start && ts < timeWindow.end;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!messagesFilter.showNoise) {
|
|
||||||
list = list.filter((m) => !isInboxNoiseMessage(typeof m.text === 'string' ? m.text : ''));
|
|
||||||
}
|
|
||||||
const hasFrom = messagesFilter.from.size > 0;
|
|
||||||
const hasTo = messagesFilter.to.size > 0;
|
|
||||||
if (hasFrom || hasTo) {
|
|
||||||
list = list.filter((m) => {
|
|
||||||
const fromMatch = hasFrom && m.from?.trim() && messagesFilter.from.has(m.from.trim());
|
|
||||||
const toMatch = hasTo && m.to?.trim() && messagesFilter.to.has(m.to.trim());
|
|
||||||
// When both filters active → OR (show messages matching either direction)
|
|
||||||
// When only one active → just that filter
|
|
||||||
return fromMatch || toMatch;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const q = messagesSearchQuery.trim().toLowerCase();
|
|
||||||
if (q) {
|
|
||||||
list = list.filter((m) => {
|
|
||||||
const text = (m.text ?? '').toLowerCase();
|
|
||||||
const summary = (m.summary ?? '').toLowerCase();
|
|
||||||
const from = (m.from ?? '').toLowerCase();
|
|
||||||
const to = (m.to ?? '').toLowerCase();
|
|
||||||
return text.includes(q) || summary.includes(q) || from.includes(q) || to.includes(q);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}, [data, timeWindow, messagesFilter, messagesSearchQuery, leadMemberName]);
|
|
||||||
|
|
||||||
const { readSet, markRead, markAllRead } = useTeamMessagesRead(teamName ?? '');
|
const { readSet, markRead, markAllRead } = useTeamMessagesRead(teamName ?? '');
|
||||||
const { expandedSet, toggle: toggleExpandOverride } = useTeamMessagesExpanded(teamName ?? '');
|
const { expandedSet, toggle: toggleExpandOverride } = useTeamMessagesExpanded(teamName ?? '');
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,14 @@ export const MessageComposer = ({
|
||||||
setTeamSelectorOpen(false);
|
setTeamSelectorOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArrowRightLeft size={11} className="shrink-0 text-purple-400" />
|
{target.color ? (
|
||||||
|
<span
|
||||||
|
className="inline-block size-2 shrink-0 rounded-full"
|
||||||
|
style={{ backgroundColor: target.color }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ArrowRightLeft size={11} className="shrink-0 text-purple-400" />
|
||||||
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="truncate text-[var(--color-text)]">
|
<div className="truncate text-[var(--color-text)]">
|
||||||
{target.displayName}
|
{target.displayName}
|
||||||
|
|
|
||||||
54
src/renderer/utils/teamMessageFiltering.ts
Normal file
54
src/renderer/utils/teamMessageFiltering.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
||||||
|
|
||||||
|
import type { InboxMessage } from '@shared/types';
|
||||||
|
|
||||||
|
export interface TeamMessagesFilter {
|
||||||
|
from: Set<string>;
|
||||||
|
to: Set<string>;
|
||||||
|
showNoise: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterTeamMessages(
|
||||||
|
messages: InboxMessage[],
|
||||||
|
options: {
|
||||||
|
timeWindow?: { start: number; end: number } | null;
|
||||||
|
filter: TeamMessagesFilter;
|
||||||
|
searchQuery: string;
|
||||||
|
}
|
||||||
|
): InboxMessage[] {
|
||||||
|
const { timeWindow, filter, searchQuery } = options;
|
||||||
|
|
||||||
|
let list = messages;
|
||||||
|
if (timeWindow) {
|
||||||
|
list = list.filter((m) => {
|
||||||
|
const ts = new Date(m.timestamp).getTime();
|
||||||
|
return ts >= timeWindow.start && ts < timeWindow.end;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!filter.showNoise) {
|
||||||
|
list = list.filter((m) => !isInboxNoiseMessage(typeof m.text === 'string' ? m.text : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFrom = filter.from.size > 0;
|
||||||
|
const hasTo = filter.to.size > 0;
|
||||||
|
if (hasFrom || hasTo) {
|
||||||
|
list = list.filter((m) => {
|
||||||
|
const fromMatch = hasFrom && m.from?.trim() && filter.from.has(m.from.trim());
|
||||||
|
const toMatch = hasTo && m.to?.trim() && filter.to.has(m.to.trim());
|
||||||
|
return fromMatch || toMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = searchQuery.trim().toLowerCase();
|
||||||
|
if (q) {
|
||||||
|
list = list.filter((m) => {
|
||||||
|
const text = (m.text ?? '').toLowerCase();
|
||||||
|
const summary = (m.summary ?? '').toLowerCase();
|
||||||
|
const from = (m.from ?? '').toLowerCase();
|
||||||
|
const to = (m.to ?? '').toLowerCase();
|
||||||
|
return text.includes(q) || summary.includes(q) || from.includes(q) || to.includes(q);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
60
test/renderer/utils/teamMessageFiltering.test.ts
Normal file
60
test/renderer/utils/teamMessageFiltering.test.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
||||||
|
|
||||||
|
import type { InboxMessage } from '@shared/types';
|
||||||
|
|
||||||
|
function makeMessage(overrides: Partial<InboxMessage> = {}): InboxMessage {
|
||||||
|
return {
|
||||||
|
from: 'team-lead',
|
||||||
|
text: 'Hello',
|
||||||
|
timestamp: '2026-03-09T12:00:00.000Z',
|
||||||
|
read: true,
|
||||||
|
messageId: 'msg-1',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('filterTeamMessages', () => {
|
||||||
|
it('keeps lead-to-user messages visible', () => {
|
||||||
|
const messages = [
|
||||||
|
makeMessage({
|
||||||
|
from: 'lead',
|
||||||
|
to: 'user',
|
||||||
|
text: 'Accepted cross-team request. Delegating now.',
|
||||||
|
source: 'lead_process',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = filterTeamMessages(messages, {
|
||||||
|
timeWindow: null,
|
||||||
|
filter: { from: new Set(), to: new Set(), showNoise: true },
|
||||||
|
searchQuery: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].to).toBe('user');
|
||||||
|
expect(result[0].source).toBe('lead_process');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still filters noise messages when showNoise is false', () => {
|
||||||
|
const messages = [
|
||||||
|
makeMessage({
|
||||||
|
text: '{"type":"idle_notification","idleReason":"available"}',
|
||||||
|
}),
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'msg-2',
|
||||||
|
text: 'Real visible message',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = filterTeamMessages(messages, {
|
||||||
|
timeWindow: null,
|
||||||
|
filter: { from: new Set(), to: new Set(), showNoise: false },
|
||||||
|
searchQuery: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].messageId).toBe('msg-2');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue