fix(tasks): eliminate duplicate task entries by stabilizing WS listener and deduplicating task state
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 55s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 55s
This commit is contained in:
1 parent
af7a9eb200
commit
6e09888a44
2 files changed
+40
-14
No files matched your search
@@ -208,7 +208,7 @@ export const App: React.FC = () => {
|
|||||||
} else {
|
} else {
|
||||||
disconnectWebSocket();
|
disconnectWebSocket();
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user?.id]);
|
||||||
|
|
||||||
// Refresh stats on task/doc real-time events
|
// Refresh stats on task/doc real-time events
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -253,7 +253,10 @@ export const App: React.FC = () => {
|
|||||||
target: 'inbox' | 'task' | 'list_item' | 'document' = 'inbox'
|
target: 'inbox' | 'task' | 'list_item' | 'document' = 'inbox'
|
||||||
) => {
|
) => {
|
||||||
if (target === 'inbox' && thought.status === 'inbox') {
|
if (target === 'inbox' && thought.status === 'inbox') {
|
||||||
setDeck((prev) => [thought, ...prev]);
|
setDeck((prev) => {
|
||||||
|
if (prev.some((t) => t.id === thought.id)) return prev;
|
||||||
|
return [thought, ...prev];
|
||||||
|
});
|
||||||
setStats((prev) => ({ ...prev, inboxCount: prev.inboxCount + 1 }));
|
setStats((prev) => ({ ...prev, inboxCount: prev.inboxCount + 1 }));
|
||||||
} else if (target === 'document' && thought.convertedRefId) {
|
} else if (target === 'document' && thought.convertedRefId) {
|
||||||
setSelectedDocId(thought.convertedRefId);
|
setSelectedDocId(thought.convertedRefId);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -96,7 +96,16 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
loadData();
|
loadData();
|
||||||
}, [refreshTrigger, selectedListId]);
|
}, [refreshTrigger, selectedListId]);
|
||||||
|
|
||||||
// Live-Sync WebSocket listener for tasks
|
const selectedListIdRef = useRef(selectedListId);
|
||||||
|
selectedListIdRef.current = selectedListId;
|
||||||
|
|
||||||
|
const activeNotesTaskIdRef = useRef(activeNotesTaskId);
|
||||||
|
activeNotesTaskIdRef.current = activeNotesTaskId;
|
||||||
|
|
||||||
|
const onRefreshStatsRef = useRef(onRefreshStats);
|
||||||
|
onRefreshStatsRef.current = onRefreshStats;
|
||||||
|
|
||||||
|
// Live-Sync WebSocket listener for tasks (registered once on mount)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleWs = (e: any) => {
|
const handleWs = (e: any) => {
|
||||||
const msg = e.detail;
|
const msg = e.detail;
|
||||||
@@ -113,10 +122,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
) {
|
) {
|
||||||
setTasks((prev) => {
|
setTasks((prev) => {
|
||||||
if (prev.some((t) => t.id === created.id)) return prev;
|
if (prev.some((t) => t.id === created.id)) return prev;
|
||||||
if (selectedListId !== 'all' && created.taskListId !== selectedListId) return prev;
|
if (selectedListIdRef.current !== 'all' && created.taskListId !== selectedListIdRef.current) return prev;
|
||||||
return [created, ...prev];
|
return [created, ...prev];
|
||||||
});
|
});
|
||||||
onRefreshStats();
|
onRefreshStatsRef.current();
|
||||||
}
|
}
|
||||||
} else if (msg.event === 'TASK_UPDATED') {
|
} else if (msg.event === 'TASK_UPDATED') {
|
||||||
const updated = msg.payload as Thought;
|
const updated = msg.payload as Thought;
|
||||||
@@ -129,37 +138,37 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
updated.kanbanStatus ||
|
updated.kanbanStatus ||
|
||||||
updated.taskListId ||
|
updated.taskListId ||
|
||||||
updated.isCompleted !== undefined) &&
|
updated.isCompleted !== undefined) &&
|
||||||
(selectedListId === 'all' || updated.taskListId === selectedListId)
|
(selectedListIdRef.current === 'all' || updated.taskListId === selectedListIdRef.current)
|
||||||
) {
|
) {
|
||||||
return [updated, ...prev];
|
return [updated, ...prev];
|
||||||
}
|
}
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
if (selectedListId !== 'all' && updated.taskListId !== selectedListId) {
|
if (selectedListIdRef.current !== 'all' && updated.taskListId !== selectedListIdRef.current) {
|
||||||
return prev.filter((t) => t.id !== updated.id);
|
return prev.filter((t) => t.id !== updated.id);
|
||||||
}
|
}
|
||||||
return prev.map((t) => {
|
return prev.map((t) => {
|
||||||
if (t.id !== updated.id) return t;
|
if (t.id !== updated.id) return t;
|
||||||
return {
|
return {
|
||||||
...updated,
|
...updated,
|
||||||
notes: activeNotesTaskId === updated.id ? t.notes : updated.notes,
|
notes: activeNotesTaskIdRef.current === updated.id ? t.notes : updated.notes,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
onRefreshStats();
|
onRefreshStatsRef.current();
|
||||||
}
|
}
|
||||||
} else if (msg.event === 'TASK_DELETED') {
|
} else if (msg.event === 'TASK_DELETED') {
|
||||||
const deletedId = typeof msg.payload === 'string' ? msg.payload : msg.payload?.id;
|
const deletedId = typeof msg.payload === 'string' ? msg.payload : msg.payload?.id;
|
||||||
if (deletedId) {
|
if (deletedId) {
|
||||||
setTasks((prev) => prev.filter((t) => t.id !== deletedId));
|
setTasks((prev) => prev.filter((t) => t.id !== deletedId));
|
||||||
onRefreshStats();
|
onRefreshStatsRef.current();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('mindsift:ws', handleWs);
|
window.addEventListener('mindsift:ws', handleWs);
|
||||||
return () => window.removeEventListener('mindsift:ws', handleWs);
|
return () => window.removeEventListener('mindsift:ws', handleWs);
|
||||||
}, [selectedListId, activeNotesTaskId, onRefreshStats]);
|
}, []);
|
||||||
|
|
||||||
const handleToggleDone = async (task: Thought) => {
|
const handleToggleDone = async (task: Thought) => {
|
||||||
triggerHaptic(15);
|
triggerHaptic(15);
|
||||||
@@ -400,7 +409,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
target: 'task',
|
target: 'task',
|
||||||
taskListId: selectedListId === 'all' ? '' : selectedListId,
|
taskListId: selectedListId === 'all' ? '' : selectedListId,
|
||||||
});
|
});
|
||||||
setTasks((prev) => [created, ...prev]);
|
setTasks((prev) => {
|
||||||
|
if (prev.some((t) => t.id === created.id)) return prev;
|
||||||
|
return [created, ...prev];
|
||||||
|
});
|
||||||
onRefreshStats();
|
onRefreshStats();
|
||||||
} catch {
|
} catch {
|
||||||
loadData();
|
loadData();
|
||||||
@@ -466,7 +478,18 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const safeTasks = Array.isArray(tasks) ? tasks : [];
|
const safeTasks = useMemo(() => {
|
||||||
|
if (!Array.isArray(tasks)) return [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const unique: Thought[] = [];
|
||||||
|
for (const t of tasks) {
|
||||||
|
if (t && t.id && !seen.has(t.id)) {
|
||||||
|
seen.add(t.id);
|
||||||
|
unique.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unique;
|
||||||
|
}, [tasks]);
|
||||||
const safeTaskLists = Array.isArray(taskLists) ? taskLists : [];
|
const safeTaskLists = Array.isArray(taskLists) ? taskLists : [];
|
||||||
const safeSpaces = Array.isArray(spaces) ? spaces : [];
|
const safeSpaces = Array.isArray(spaces) ? spaces : [];
|
||||||
const safeDiscarded = Array.isArray(discarded) ? discarded : [];
|
const safeDiscarded = Array.isArray(discarded) ? discarded : [];
|
||||||
|
|||||||
Reference in new issue
Block a user