refine member message pagination
This commit is contained in:
parent
0531fc1dbf
commit
9296ce3988
2 changed files with 87 additions and 56 deletions
|
|
@ -1,11 +1,8 @@
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '@renderer/api';
|
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@renderer/components/ui/dialog';
|
import { Dialog, DialogContent, DialogFooter, DialogHeader } from '@renderer/components/ui/dialog';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
||||||
import { useMemberStats } from '@renderer/hooks/useMemberStats';
|
import { useMemberStats } from '@renderer/hooks/useMemberStats';
|
||||||
import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages';
|
|
||||||
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
import { isLeadAgentType, isLeadMember } from '@shared/utils/leadDetection';
|
||||||
import { BarChart3, FileText, ListPlus, MessageSquare, UserMinus } from 'lucide-react';
|
import { BarChart3, FileText, ListPlus, MessageSquare, UserMinus } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -42,8 +39,6 @@ interface MemberDetailDialogProps {
|
||||||
onViewMemberChanges?: (memberName: string, filePath?: string) => void;
|
onViewMemberChanges?: (memberName: string, filePath?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MEMBER_MESSAGES_PAGE_SIZE = 200;
|
|
||||||
|
|
||||||
export const MemberDetailDialog = ({
|
export const MemberDetailDialog = ({
|
||||||
open,
|
open,
|
||||||
member,
|
member,
|
||||||
|
|
@ -71,52 +66,7 @@ export const MemberDetailDialog = ({
|
||||||
() => (member ? messages.filter((m) => m.from === member.name || m.to === member.name) : []),
|
() => (member ? messages.filter((m) => m.from === member.name || m.to === member.name) : []),
|
||||||
[messages, member]
|
[messages, member]
|
||||||
);
|
);
|
||||||
const [pagedMemberMessages, setPagedMemberMessages] = useState<InboxMessage[] | null>(null);
|
const memberMessages = seedMemberMessages;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open || !member) {
|
|
||||||
setPagedMemberMessages(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
setPagedMemberMessages(null);
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
let cursor: string | undefined;
|
|
||||||
let hasMore = true;
|
|
||||||
let allMessages: InboxMessage[] = [];
|
|
||||||
|
|
||||||
while (!cancelled && hasMore) {
|
|
||||||
const page = await api.teams.getMessagesPage(teamName, {
|
|
||||||
beforeTimestamp: cursor,
|
|
||||||
limit: MEMBER_MESSAGES_PAGE_SIZE,
|
|
||||||
});
|
|
||||||
allMessages = mergeTeamMessages(allMessages, page.messages);
|
|
||||||
hasMore = page.hasMore && page.nextCursor != null;
|
|
||||||
cursor = page.nextCursor ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled) return;
|
|
||||||
|
|
||||||
setPagedMemberMessages(
|
|
||||||
allMessages.filter((message) => message.from === member.name || message.to === member.name)
|
|
||||||
);
|
|
||||||
})().catch(() => {
|
|
||||||
if (!cancelled) {
|
|
||||||
setPagedMemberMessages([]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [open, teamName, member?.name]);
|
|
||||||
|
|
||||||
const memberMessages = useMemo(
|
|
||||||
() => mergeTeamMessages(seedMemberMessages, pagedMemberMessages ?? []),
|
|
||||||
[seedMemberMessages, pagedMemberMessages]
|
|
||||||
);
|
|
||||||
|
|
||||||
const inProgressTasks = useMemo(
|
const inProgressTasks = useMemo(
|
||||||
() => memberTasks.filter((t) => t.status === 'in_progress').length,
|
() => memberTasks.filter((t) => t.status === 'in_progress').length,
|
||||||
|
|
@ -204,7 +154,11 @@ export const MemberDetailDialog = ({
|
||||||
<MemberTasksTab tasks={memberTasks} onTaskClick={onTaskClick} />
|
<MemberTasksTab tasks={memberTasks} onTaskClick={onTaskClick} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="messages">
|
<TabsContent value="messages">
|
||||||
<MemberMessagesTab messages={memberMessages} teamName={teamName} />
|
<MemberMessagesTab
|
||||||
|
messages={memberMessages}
|
||||||
|
teamName={teamName}
|
||||||
|
memberName={member.name}
|
||||||
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="stats">
|
<TabsContent value="stats">
|
||||||
<MemberStatsTab
|
<MemberStatsTab
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { useMemo } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { api } from '@renderer/api';
|
||||||
|
import { Button } from '@renderer/components/ui/button';
|
||||||
|
import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages';
|
||||||
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
||||||
|
|
||||||
import { ActivityItem } from '../activity/ActivityItem';
|
import { ActivityItem } from '../activity/ActivityItem';
|
||||||
|
|
@ -9,24 +12,91 @@ import type { InboxMessage } from '@shared/types';
|
||||||
interface MemberMessagesTabProps {
|
interface MemberMessagesTabProps {
|
||||||
messages: InboxMessage[];
|
messages: InboxMessage[];
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
memberName: string;
|
||||||
onCreateTask?: (subject: string, description: string) => void;
|
onCreateTask?: (subject: string, description: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_MESSAGES = 100;
|
const MAX_MESSAGES = 100;
|
||||||
|
const MEMBER_MESSAGES_PAGE_SIZE = 50;
|
||||||
|
|
||||||
export const MemberMessagesTab = ({
|
export const MemberMessagesTab = ({
|
||||||
messages,
|
messages,
|
||||||
teamName,
|
teamName,
|
||||||
|
memberName,
|
||||||
onCreateTask,
|
onCreateTask,
|
||||||
}: MemberMessagesTabProps): React.JSX.Element => {
|
}: MemberMessagesTabProps): React.JSX.Element => {
|
||||||
|
const [pagedMessages, setPagedMessages] = useState<InboxMessage[]>([]);
|
||||||
|
const [nextCursor, setNextCursor] = useState<string | null>(null);
|
||||||
|
const [hasMore, setHasMore] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setPagedMessages([]);
|
||||||
|
setNextCursor(null);
|
||||||
|
setHasMore(false);
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const page = await api.teams.getMessagesPage(teamName, { limit: MEMBER_MESSAGES_PAGE_SIZE });
|
||||||
|
if (cancelled) return;
|
||||||
|
const memberPageMessages = page.messages.filter(
|
||||||
|
(message) => message.from === memberName || message.to === memberName
|
||||||
|
);
|
||||||
|
setPagedMessages(memberPageMessages);
|
||||||
|
setNextCursor(page.nextCursor);
|
||||||
|
setHasMore(page.hasMore);
|
||||||
|
} catch {
|
||||||
|
if (!cancelled) {
|
||||||
|
setPagedMessages([]);
|
||||||
|
setNextCursor(null);
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [teamName, memberName]);
|
||||||
|
|
||||||
|
const loadOlderMessages = useCallback(async () => {
|
||||||
|
if (!nextCursor || loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const page = await api.teams.getMessagesPage(teamName, {
|
||||||
|
beforeTimestamp: nextCursor,
|
||||||
|
limit: MEMBER_MESSAGES_PAGE_SIZE,
|
||||||
|
});
|
||||||
|
const memberPageMessages = page.messages.filter(
|
||||||
|
(message) => message.from === memberName || message.to === memberName
|
||||||
|
);
|
||||||
|
setPagedMessages((prev) => mergeTeamMessages(prev, memberPageMessages));
|
||||||
|
setNextCursor(page.nextCursor);
|
||||||
|
setHasMore(page.hasMore);
|
||||||
|
} catch {
|
||||||
|
// best-effort
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [teamName, memberName, nextCursor, loading]);
|
||||||
|
|
||||||
|
const effectiveMessages = useMemo(
|
||||||
|
() => mergeTeamMessages(messages, pagedMessages),
|
||||||
|
[messages, pagedMessages]
|
||||||
|
);
|
||||||
|
|
||||||
const displayMessages = useMemo(
|
const displayMessages = useMemo(
|
||||||
() =>
|
() =>
|
||||||
filterTeamMessages(messages, {
|
filterTeamMessages(effectiveMessages, {
|
||||||
timeWindow: null,
|
timeWindow: null,
|
||||||
filter: { from: new Set(), to: new Set(), showNoise: true },
|
filter: { from: new Set(), to: new Set(), showNoise: true },
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
}).slice(0, MAX_MESSAGES),
|
}).slice(0, MAX_MESSAGES),
|
||||||
[messages]
|
[effectiveMessages]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (displayMessages.length === 0) {
|
if (displayMessages.length === 0) {
|
||||||
|
|
@ -47,6 +117,13 @@ export const MemberMessagesTab = ({
|
||||||
onCreateTask={onCreateTask}
|
onCreateTask={onCreateTask}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{hasMore && (
|
||||||
|
<div className="flex justify-center pt-2">
|
||||||
|
<Button variant="ghost" size="sm" className="text-xs" disabled={loading} onClick={() => void loadOlderMessages()}>
|
||||||
|
{loading ? 'Loading...' : 'Load older messages'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue