perf(renderer): isolate pending replies from team page root
This commit is contained in:
parent
7019567537
commit
583bb5f26a
1 changed files with 149 additions and 72 deletions
|
|
@ -5,11 +5,11 @@ import {
|
||||||
Suspense,
|
Suspense,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useId,
|
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
|
useSyncExternalStore,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { useAppTranslation } from '@features/localization/renderer';
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
|
|
@ -895,20 +895,20 @@ const TeamOfflineStatusBanner = memo(function TeamOfflineStatusBanner({
|
||||||
type LeadUpdatedKey = `lead${'Con'}${'text'}UpdatedAt`;
|
type LeadUpdatedKey = `lead${'Con'}${'text'}UpdatedAt`;
|
||||||
type TeamMessagesPanelBridgeProps = Omit<
|
type TeamMessagesPanelBridgeProps = Omit<
|
||||||
ComponentProps<typeof MessagesPanel>,
|
ComponentProps<typeof MessagesPanel>,
|
||||||
'leadActivity' | LeadUpdatedKey
|
'leadActivity' | LeadUpdatedKey | 'pendingRepliesByMember' | 'onPendingReplyChange'
|
||||||
>;
|
>;
|
||||||
type SendMessageDialogBridgeProps = Omit<
|
type SendMessageDialogBridgeProps = Omit<
|
||||||
ComponentProps<typeof SendMessageDialog>,
|
ComponentProps<typeof SendMessageDialog>,
|
||||||
'sending' | 'sendError' | 'sendWarning' | 'sendDebugDetails' | 'lastResult' | 'onSend'
|
'sending' | 'sendError' | 'sendWarning' | 'sendDebugDetails' | 'lastResult' | 'onSend'
|
||||||
> & {
|
>;
|
||||||
onPendingReplyStart: (member: string, sentAtMs: number) => void;
|
|
||||||
onPendingReplySettled: (member: string, sentAtMs: number) => void;
|
|
||||||
};
|
|
||||||
type SendMessageDialogOnSend = ComponentProps<typeof SendMessageDialog>['onSend'];
|
type SendMessageDialogOnSend = ComponentProps<typeof SendMessageDialog>['onSend'];
|
||||||
|
type PendingRepliesUpdater =
|
||||||
|
| Record<string, number>
|
||||||
|
| ((current: Record<string, number>) => Record<string, number>);
|
||||||
type SharedTeamMessagesPanelProps = Omit<TeamMessagesPanelBridgeProps, 'position'>;
|
type SharedTeamMessagesPanelProps = Omit<TeamMessagesPanelBridgeProps, 'position'>;
|
||||||
type TeamMemberListBridgeProps = Omit<
|
type TeamMemberListBridgeProps = Omit<
|
||||||
ComponentProps<typeof MemberList>,
|
ComponentProps<typeof MemberList>,
|
||||||
'leadActivity' | 'memberSpawnStatuses'
|
'leadActivity' | 'memberSpawnStatuses' | 'pendingRepliesByMember'
|
||||||
> & {
|
> & {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
};
|
};
|
||||||
|
|
@ -932,6 +932,54 @@ interface LeadLoadBridgeProps {
|
||||||
isThisTabActive: boolean;
|
isThisTabActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pendingRepliesCacheByTeam = new Map<string, Record<string, number>>();
|
||||||
|
const pendingRepliesListenersByTeam = new Map<string, Set<() => void>>();
|
||||||
|
let pendingReplyRefreshSourceSequence = 0;
|
||||||
|
|
||||||
|
function getPendingRepliesSnapshot(teamName: string): Record<string, number> {
|
||||||
|
let snapshot = pendingRepliesCacheByTeam.get(teamName);
|
||||||
|
if (!snapshot) {
|
||||||
|
snapshot = getTeamPendingRepliesState(teamName);
|
||||||
|
pendingRepliesCacheByTeam.set(teamName, snapshot);
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribePendingReplies(teamName: string, listener: () => void): () => void {
|
||||||
|
let listeners = pendingRepliesListenersByTeam.get(teamName);
|
||||||
|
if (!listeners) {
|
||||||
|
listeners = new Set();
|
||||||
|
pendingRepliesListenersByTeam.set(teamName, listeners);
|
||||||
|
}
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
listeners?.delete(listener);
|
||||||
|
if (listeners?.size === 0) {
|
||||||
|
pendingRepliesListenersByTeam.delete(teamName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPendingRepliesForTeam(teamName: string, updater: PendingRepliesUpdater): void {
|
||||||
|
const current = getPendingRepliesSnapshot(teamName);
|
||||||
|
const next = typeof updater === 'function' ? updater(current) : updater;
|
||||||
|
if (next === current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingRepliesCacheByTeam.set(teamName, next);
|
||||||
|
setTeamPendingRepliesState(teamName, next);
|
||||||
|
pendingRepliesListenersByTeam.get(teamName)?.forEach((listener) => listener());
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTeamPendingReplies(teamName: string): Record<string, number> {
|
||||||
|
const subscribe = useCallback(
|
||||||
|
(listener: () => void) => subscribePendingReplies(teamName, listener),
|
||||||
|
[teamName]
|
||||||
|
);
|
||||||
|
const getSnapshot = useCallback(() => getPendingRepliesSnapshot(teamName), [teamName]);
|
||||||
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
const EMPTY_MESSAGES_PANEL_TASKS: TeamTaskWithKanban[] = [];
|
const EMPTY_MESSAGES_PANEL_TASKS: TeamTaskWithKanban[] = [];
|
||||||
|
|
||||||
function buildMessagesPanelTasksSignature(tasks: readonly TeamTaskWithKanban[]): string {
|
function buildMessagesPanelTasksSignature(tasks: readonly TeamTaskWithKanban[]): string {
|
||||||
|
|
@ -1337,6 +1385,7 @@ const TeamMemberListBridge = memo(function TeamMemberListBridge({
|
||||||
teamName,
|
teamName,
|
||||||
...props
|
...props
|
||||||
}: TeamMemberListBridgeProps): React.JSX.Element {
|
}: TeamMemberListBridgeProps): React.JSX.Element {
|
||||||
|
const pendingRepliesByMember = useTeamPendingReplies(teamName);
|
||||||
const { leadActivity, progress, memberSpawnStatuses, memberSpawnSnapshot, runtimeSnapshot } =
|
const { leadActivity, progress, memberSpawnStatuses, memberSpawnSnapshot, runtimeSnapshot } =
|
||||||
useStore(
|
useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
|
|
@ -1376,6 +1425,7 @@ const TeamMemberListBridge = memo(function TeamMemberListBridge({
|
||||||
{...props}
|
{...props}
|
||||||
teamName={teamName}
|
teamName={teamName}
|
||||||
leadActivity={leadActivity}
|
leadActivity={leadActivity}
|
||||||
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
memberSpawnStatuses={memberSpawnStatusMap}
|
memberSpawnStatuses={memberSpawnStatusMap}
|
||||||
memberRuntimeEntries={memberRuntimeMap}
|
memberRuntimeEntries={memberRuntimeMap}
|
||||||
runtimeRunId={runtimeRunId}
|
runtimeRunId={runtimeRunId}
|
||||||
|
|
@ -1386,21 +1436,53 @@ const TeamMemberListBridge = memo(function TeamMemberListBridge({
|
||||||
|
|
||||||
const TeamMessagesPanelBridge = memo(function TeamMessagesPanelBridge({
|
const TeamMessagesPanelBridge = memo(function TeamMessagesPanelBridge({
|
||||||
teamName,
|
teamName,
|
||||||
|
isTeamAlive,
|
||||||
...props
|
...props
|
||||||
}: TeamMessagesPanelBridgeProps): React.JSX.Element {
|
}: TeamMessagesPanelBridgeProps): React.JSX.Element {
|
||||||
const { leadActivity, leadContextUpdatedAt } = useStore(
|
const pendingRepliesByMember = useTeamPendingReplies(teamName);
|
||||||
|
const pendingReplyRefreshSourceId = useRef<string | null>(null);
|
||||||
|
if (pendingReplyRefreshSourceId.current === null) {
|
||||||
|
pendingReplyRefreshSourceSequence += 1;
|
||||||
|
pendingReplyRefreshSourceId.current = `team-messages:${pendingReplyRefreshSourceSequence}`;
|
||||||
|
}
|
||||||
|
const { leadActivity, leadContextUpdatedAt, syncTeamPendingReplyRefresh } = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
leadActivity: s.leadActivityByTeam[teamName],
|
leadActivity: s.leadActivityByTeam[teamName],
|
||||||
leadContextUpdatedAt: s.leadContextByTeam[teamName]?.updatedAt,
|
leadContextUpdatedAt: s.leadContextByTeam[teamName]?.updatedAt,
|
||||||
|
syncTeamPendingReplyRefresh: s.syncTeamPendingReplyRefresh,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hasPendingReplies = Object.keys(pendingRepliesByMember).length > 0;
|
||||||
|
syncTeamPendingReplyRefresh(
|
||||||
|
teamName,
|
||||||
|
pendingReplyRefreshSourceId.current!,
|
||||||
|
Boolean(isTeamAlive) && hasPendingReplies,
|
||||||
|
TEAM_PENDING_REPLY_REFRESH_DELAY_MS
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
syncTeamPendingReplyRefresh(teamName, pendingReplyRefreshSourceId.current!, false);
|
||||||
|
};
|
||||||
|
}, [isTeamAlive, pendingRepliesByMember, syncTeamPendingReplyRefresh, teamName]);
|
||||||
|
|
||||||
|
const handlePendingReplyChange = useCallback(
|
||||||
|
(updater: PendingRepliesUpdater) => {
|
||||||
|
setPendingRepliesForTeam(teamName, updater);
|
||||||
|
},
|
||||||
|
[teamName]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessagesPanel
|
<MessagesPanel
|
||||||
{...props}
|
{...props}
|
||||||
teamName={teamName}
|
teamName={teamName}
|
||||||
|
isTeamAlive={isTeamAlive}
|
||||||
leadActivity={leadActivity}
|
leadActivity={leadActivity}
|
||||||
leadContextUpdatedAt={leadContextUpdatedAt}
|
leadContextUpdatedAt={leadContextUpdatedAt}
|
||||||
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
|
onPendingReplyChange={handlePendingReplyChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -1409,19 +1491,60 @@ const TeamSidebarRailBridge = memo(function TeamSidebarRailBridge({
|
||||||
messagesPanelProps,
|
messagesPanelProps,
|
||||||
...props
|
...props
|
||||||
}: TeamSidebarRailBridgeProps): React.JSX.Element {
|
}: TeamSidebarRailBridgeProps): React.JSX.Element {
|
||||||
const { leadActivity, leadContextUpdatedAt } = useStore(
|
const teamName = messagesPanelProps.teamName;
|
||||||
|
const pendingRepliesByMember = useTeamPendingReplies(teamName);
|
||||||
|
const pendingReplyRefreshSourceId = useRef<string | null>(null);
|
||||||
|
if (pendingReplyRefreshSourceId.current === null) {
|
||||||
|
pendingReplyRefreshSourceSequence += 1;
|
||||||
|
pendingReplyRefreshSourceId.current = `team-sidebar:${pendingReplyRefreshSourceSequence}`;
|
||||||
|
}
|
||||||
|
const { leadActivity, leadContextUpdatedAt, syncTeamPendingReplyRefresh } = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
leadActivity: s.leadActivityByTeam[messagesPanelProps.teamName],
|
leadActivity: s.leadActivityByTeam[teamName],
|
||||||
leadContextUpdatedAt: s.leadContextByTeam[messagesPanelProps.teamName]?.updatedAt,
|
leadContextUpdatedAt: s.leadContextByTeam[teamName]?.updatedAt,
|
||||||
|
syncTeamPendingReplyRefresh: s.syncTeamPendingReplyRefresh,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
const hasPendingReplies = Object.keys(pendingRepliesByMember).length > 0;
|
||||||
|
syncTeamPendingReplyRefresh(
|
||||||
|
teamName,
|
||||||
|
pendingReplyRefreshSourceId.current!,
|
||||||
|
Boolean(messagesPanelProps.isTeamAlive) && hasPendingReplies,
|
||||||
|
TEAM_PENDING_REPLY_REFRESH_DELAY_MS
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
syncTeamPendingReplyRefresh(teamName, pendingReplyRefreshSourceId.current!, false);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
messagesPanelProps.isTeamAlive,
|
||||||
|
pendingRepliesByMember,
|
||||||
|
syncTeamPendingReplyRefresh,
|
||||||
|
teamName,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handlePendingReplyChange = useCallback(
|
||||||
|
(updater: PendingRepliesUpdater) => {
|
||||||
|
setPendingRepliesForTeam(teamName, updater);
|
||||||
|
},
|
||||||
|
[teamName]
|
||||||
|
);
|
||||||
const bridgedMessagesPanelProps = useMemo(
|
const bridgedMessagesPanelProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...messagesPanelProps,
|
...messagesPanelProps,
|
||||||
leadActivity,
|
leadActivity,
|
||||||
leadContextUpdatedAt,
|
leadContextUpdatedAt,
|
||||||
|
pendingRepliesByMember,
|
||||||
|
onPendingReplyChange: handlePendingReplyChange,
|
||||||
}),
|
}),
|
||||||
[leadActivity, leadContextUpdatedAt, messagesPanelProps]
|
[
|
||||||
|
handlePendingReplyChange,
|
||||||
|
leadActivity,
|
||||||
|
leadContextUpdatedAt,
|
||||||
|
messagesPanelProps,
|
||||||
|
pendingRepliesByMember,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <TeamSidebarRail {...props} messagesPanelProps={bridgedMessagesPanelProps} />;
|
return <TeamSidebarRail {...props} messagesPanelProps={bridgedMessagesPanelProps} />;
|
||||||
|
|
@ -1429,8 +1552,6 @@ const TeamSidebarRailBridge = memo(function TeamSidebarRailBridge({
|
||||||
|
|
||||||
const SendMessageDialogBridge = memo(function SendMessageDialogBridge({
|
const SendMessageDialogBridge = memo(function SendMessageDialogBridge({
|
||||||
teamName,
|
teamName,
|
||||||
onPendingReplyStart,
|
|
||||||
onPendingReplySettled,
|
|
||||||
...props
|
...props
|
||||||
}: SendMessageDialogBridgeProps): React.JSX.Element {
|
}: SendMessageDialogBridgeProps): React.JSX.Element {
|
||||||
const {
|
const {
|
||||||
|
|
@ -1454,7 +1575,7 @@ const SendMessageDialogBridge = memo(function SendMessageDialogBridge({
|
||||||
const handleSend = useCallback<SendMessageDialogOnSend>(
|
const handleSend = useCallback<SendMessageDialogOnSend>(
|
||||||
async (member, text, summary, attachments, actionMode, taskRefs) => {
|
async (member, text, summary, attachments, actionMode, taskRefs) => {
|
||||||
const sentAtMs = Date.now();
|
const sentAtMs = Date.now();
|
||||||
onPendingReplyStart(member, sentAtMs);
|
setPendingRepliesForTeam(teamName, (prev) => ({ ...prev, [member]: sentAtMs }));
|
||||||
try {
|
try {
|
||||||
const result = await sendTeamMessage(teamName, {
|
const result = await sendTeamMessage(teamName, {
|
||||||
member,
|
member,
|
||||||
|
|
@ -1465,15 +1586,25 @@ const SendMessageDialogBridge = memo(function SendMessageDialogBridge({
|
||||||
taskRefs,
|
taskRefs,
|
||||||
});
|
});
|
||||||
if (shouldClearPendingReplyForOpenCodeRuntimeDelivery(result?.runtimeDelivery)) {
|
if (shouldClearPendingReplyForOpenCodeRuntimeDelivery(result?.runtimeDelivery)) {
|
||||||
onPendingReplySettled(member, sentAtMs);
|
setPendingRepliesForTeam(teamName, (prev) => {
|
||||||
|
if (prev[member] !== sentAtMs) return prev;
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[member];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
onPendingReplySettled(member, sentAtMs);
|
setPendingRepliesForTeam(teamName, (prev) => {
|
||||||
|
if (prev[member] !== sentAtMs) return prev;
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[member];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onPendingReplySettled, onPendingReplyStart, sendTeamMessage, teamName]
|
[sendTeamMessage, teamName]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -1564,9 +1695,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
initialTab?: MemberDetailTab;
|
initialTab?: MemberDetailTab;
|
||||||
initialActivityFilter?: MemberActivityFilter;
|
initialActivityFilter?: MemberActivityFilter;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [pendingRepliesByMember, setPendingRepliesByMember] = useState<Record<string, number>>(() =>
|
|
||||||
getTeamPendingRepliesState(teamName)
|
|
||||||
);
|
|
||||||
const [createTaskDialog, setCreateTaskDialog] = useState<CreateTaskDialogState>({
|
const [createTaskDialog, setCreateTaskDialog] = useState<CreateTaskDialogState>({
|
||||||
open: false,
|
open: false,
|
||||||
defaultSubject: '',
|
defaultSubject: '',
|
||||||
|
|
@ -1725,7 +1853,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
refreshTeamData,
|
refreshTeamData,
|
||||||
refreshTeamMessagesHead,
|
refreshTeamMessagesHead,
|
||||||
refreshMemberActivityMeta,
|
refreshMemberActivityMeta,
|
||||||
syncTeamPendingReplyRefresh,
|
|
||||||
kanbanFilterQuery,
|
kanbanFilterQuery,
|
||||||
clearKanbanFilter,
|
clearKanbanFilter,
|
||||||
softDeleteTask,
|
softDeleteTask,
|
||||||
|
|
@ -1785,7 +1912,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
refreshTeamData: s.refreshTeamData,
|
refreshTeamData: s.refreshTeamData,
|
||||||
refreshTeamMessagesHead: s.refreshTeamMessagesHead,
|
refreshTeamMessagesHead: s.refreshTeamMessagesHead,
|
||||||
refreshMemberActivityMeta: s.refreshMemberActivityMeta,
|
refreshMemberActivityMeta: s.refreshMemberActivityMeta,
|
||||||
syncTeamPendingReplyRefresh: s.syncTeamPendingReplyRefresh,
|
|
||||||
kanbanFilterQuery: s.kanbanFilterQuery,
|
kanbanFilterQuery: s.kanbanFilterQuery,
|
||||||
clearKanbanFilter: s.clearKanbanFilter,
|
clearKanbanFilter: s.clearKanbanFilter,
|
||||||
softDeleteTask: s.softDeleteTask,
|
softDeleteTask: s.softDeleteTask,
|
||||||
|
|
@ -1848,14 +1974,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
}
|
}
|
||||||
}, [tabId, initTabUIState]);
|
}, [tabId, initTabUIState]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPendingRepliesByMember(getTeamPendingRepliesState(teamName));
|
|
||||||
}, [teamName]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTeamPendingRepliesState(teamName, pendingRepliesByMember);
|
|
||||||
}, [pendingRepliesByMember, teamName]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const wasProvisioning = wasProvisioningRef.current;
|
const wasProvisioning = wasProvisioningRef.current;
|
||||||
wasProvisioningRef.current = isTeamProvisioning;
|
wasProvisioningRef.current = isTeamProvisioning;
|
||||||
|
|
@ -1960,35 +2078,11 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
);
|
);
|
||||||
|
|
||||||
const leadSessionId = data?.config.leadSessionId ?? null;
|
const leadSessionId = data?.config.leadSessionId ?? null;
|
||||||
const pendingReplyRefreshSourceId = useId();
|
|
||||||
const sessionHistoryKey = useMemo(
|
const sessionHistoryKey = useMemo(
|
||||||
() => (data?.config.sessionHistory ?? []).join('|'),
|
() => (data?.config.sessionHistory ?? []).join('|'),
|
||||||
[data?.config.sessionHistory]
|
[data?.config.sessionHistory]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Keep team message state fresh while we are explicitly waiting for a reply.
|
|
||||||
// This stays enabled even for hidden mounted tabs, because the waiting state
|
|
||||||
// is renderer-local and should keep its lightweight polling until resolved.
|
|
||||||
useEffect(() => {
|
|
||||||
const hasPendingReplies = Object.keys(pendingRepliesByMember).length > 0;
|
|
||||||
syncTeamPendingReplyRefresh(
|
|
||||||
teamName,
|
|
||||||
pendingReplyRefreshSourceId,
|
|
||||||
Boolean(data?.isAlive) && hasPendingReplies,
|
|
||||||
TEAM_PENDING_REPLY_REFRESH_DELAY_MS
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
syncTeamPendingReplyRefresh(teamName, pendingReplyRefreshSourceId, false);
|
|
||||||
};
|
|
||||||
}, [
|
|
||||||
data?.isAlive,
|
|
||||||
pendingRepliesByMember,
|
|
||||||
pendingReplyRefreshSourceId,
|
|
||||||
syncTeamPendingReplyRefresh,
|
|
||||||
teamName,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isThisTabActive || !projectId) return;
|
if (!isThisTabActive || !projectId) return;
|
||||||
|
|
||||||
|
|
@ -2832,17 +2926,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
};
|
};
|
||||||
|
|
||||||
const messagesPanelTasks = useStableMessagesPanelTasks(data?.tasks);
|
const messagesPanelTasks = useStableMessagesPanelTasks(data?.tasks);
|
||||||
const handlePendingReplyStart = useCallback((member: string, sentAtMs: number): void => {
|
|
||||||
setPendingRepliesByMember((prev) => ({ ...prev, [member]: sentAtMs }));
|
|
||||||
}, []);
|
|
||||||
const handlePendingReplySettled = useCallback((member: string, sentAtMs: number): void => {
|
|
||||||
setPendingRepliesByMember((prev) => {
|
|
||||||
if (prev[member] !== sentAtMs) return prev;
|
|
||||||
const next = { ...prev };
|
|
||||||
delete next[member];
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sharedMessagesPanelProps = useMemo<SharedTeamMessagesPanelProps>(
|
const sharedMessagesPanelProps = useMemo<SharedTeamMessagesPanelProps>(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -2854,8 +2937,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
isTeamAlive: data?.isAlive,
|
isTeamAlive: data?.isAlive,
|
||||||
timeWindow,
|
timeWindow,
|
||||||
currentLeadSessionId: data?.config.leadSessionId,
|
currentLeadSessionId: data?.config.leadSessionId,
|
||||||
pendingRepliesByMember,
|
|
||||||
onPendingReplyChange: setPendingRepliesByMember,
|
|
||||||
onMemberClick: handleSelectMember,
|
onMemberClick: handleSelectMember,
|
||||||
onTaskClick: handleOpenMessagePanelTask,
|
onTaskClick: handleOpenMessagePanelTask,
|
||||||
onCreateTaskFromMessage: handleCreateTaskFromMessage,
|
onCreateTaskFromMessage: handleCreateTaskFromMessage,
|
||||||
|
|
@ -2878,7 +2959,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
handleFloatingComposerHeightChange,
|
handleFloatingComposerHeightChange,
|
||||||
messagesPanelTasks,
|
messagesPanelTasks,
|
||||||
messagesPanelMountPoint,
|
messagesPanelMountPoint,
|
||||||
pendingRepliesByMember,
|
|
||||||
teamName,
|
teamName,
|
||||||
timeWindow,
|
timeWindow,
|
||||||
changeMessagesPanelMode,
|
changeMessagesPanelMode,
|
||||||
|
|
@ -3326,7 +3406,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
expectedTeammateCount={activeTeammateCount}
|
expectedTeammateCount={activeTeammateCount}
|
||||||
memberTaskCounts={memberTaskCounts}
|
memberTaskCounts={memberTaskCounts}
|
||||||
taskMap={taskMap}
|
taskMap={taskMap}
|
||||||
pendingRepliesByMember={pendingRepliesByMember}
|
|
||||||
isRosterLoading={loading}
|
isRosterLoading={loading}
|
||||||
isTeamAlive={data.isAlive}
|
isTeamAlive={data.isAlive}
|
||||||
isTeamProvisioning={isTeamProvisioning}
|
isTeamProvisioning={isTeamProvisioning}
|
||||||
|
|
@ -3735,8 +3814,6 @@ export const TeamDetailView = memo(function TeamDetailView({
|
||||||
defaultChip={sendDialogDefaultChip}
|
defaultChip={sendDialogDefaultChip}
|
||||||
quotedMessage={replyQuote}
|
quotedMessage={replyQuote}
|
||||||
isTeamAlive={data.isAlive}
|
isTeamAlive={data.isAlive}
|
||||||
onPendingReplyStart={handlePendingReplyStart}
|
|
||||||
onPendingReplySettled={handlePendingReplySettled}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setSendDialogOpen(false);
|
setSendDialogOpen(false);
|
||||||
setReplyQuote(undefined);
|
setReplyQuote(undefined);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue