diff --git a/src/components/TaskDetail.tsx b/src/components/TaskDetail.tsx index 38fa780..82ccba0 100644 --- a/src/components/TaskDetail.tsx +++ b/src/components/TaskDetail.tsx @@ -42,13 +42,22 @@ export const TaskDetail: Component = (props) => { }); let debounceTimer: number | undefined; + let pendingContent: string | null = null; + const updateTaskContent = (html: string) => { + pendingContent = html; clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { - updateTask(props.task.id, { content: html }); + if (pendingContent !== null) { + updateTask(props.task.id, { content: pendingContent }); + pendingContent = null; + } }, 1000); }; + // Removed onCleanup to avoid lockup + + const updateTitle = (val: string) => { setTitle(val); updateTask(props.task.id, { title: val }); @@ -87,7 +96,16 @@ export const TaskDetail: Component = (props) => { const currentUrgency = () => calculateUrgencyFromDate(props.task.dueDate).toString(); return ( - !open && props.onClose()}> + { + if (!open) { + // Flash save before closing to avoid lockup + if (pendingContent !== null) { + updateTask(props.task.id, { content: pendingContent }); + pendingContent = null; + } + props.onClose(); + } + }}> { diff --git a/src/store/index.ts b/src/store/index.ts index fb094c2..9b2280f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -287,6 +287,94 @@ export const getCombinedScore = (task: Task): number => { let heartbeatInterval: number | undefined; +const mapRecordToTask = (r: any): Task => { + // Check if it's a template? The caller handles filtering templates usually, + // but here we just map fields. + const tags = r.tags || []; + return { + id: r.id, + title: r.title, + startDate: r.startDate, + dueDate: r.dueDate, + priority: r.priority, + completed: r.completed, + tags: tags, + content: r.content, + deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined, + created: r.created, + updated: r.updated, + recurrence: r.recurrence + }; +}; + +export const subscribeToRealtime = async () => { + // Unsubscribe first to avoid duplicates + await pb.collection(TASGRID_COLLECTION).unsubscribe('*'); + + pb.collection(TASGRID_COLLECTION).subscribe('*', (e) => { + console.log("Realtime event:", e.action, e.record.id); + + if (e.action === 'create') { + // Check if it's a template + const isTemplate = e.record.tags?.includes("__template__"); + if (isTemplate) { + // We don't auto-add templates to the main task list usually? + // Wait, `store.templates` is separate. + // We should handle templates too if we want sync on templates. + let meta: any = {}; + try { meta = JSON.parse(e.record.content || "{}"); } catch (e) { } + const newTemplate: TaskTemplate = { + id: e.record.id, + name: e.record.title, + title: meta.title || "", + priority: e.record.priority, + urgency: meta.urgency || 5, + tags: meta.tags || [], + content: meta.content || "" + }; + setStore("templates", t => [...(t || []), newTemplate]); + return; + } + + // Regular Task + const exists = store.tasks.find(t => t.id === e.record.id); + if (!exists) { + const newTask = mapRecordToTask(e.record); + setStore("tasks", t => [newTask, ...t]); + } + } + + if (e.action === 'update') { + // Is it a template? + const isTemplate = e.record.tags?.includes("__template__"); + if (isTemplate) { + // Update template store + let meta: any = {}; + try { meta = JSON.parse(e.record.content || "{}"); } catch (e) { } + setStore("templates", t => t.id === e.record.id, { + name: e.record.title, + title: meta.title || "", + priority: e.record.priority, + urgency: meta.urgency || 5, + tags: meta.tags || [], + content: meta.content || "" + }); + return; + } + + // Normal Task + const updatedTask = mapRecordToTask(e.record); + setStore("tasks", t => t.id === e.record.id, updatedTask); + } + + if (e.action === 'delete') { + // Try deleting from both just in case + setStore("tasks", t => t.filter(x => x.id !== e.record.id)); + setStore("templates", t => (t || []).filter(x => x.id !== e.record.id)); + } + }); +}; + export const initStore = async () => { if (!pb.authStore.isValid) return; @@ -305,7 +393,10 @@ export const initStore = async () => { // Heartbeat - Ensure only one interval is running if (heartbeatInterval) clearInterval(heartbeatInterval); - heartbeatInterval = setInterval(() => setNow(Date.now()), 60000); + heartbeatInterval = setInterval(() => { + setNow(Date.now()); + checkRecurringTasks(); // Check for recurrence triggers (minutely, etc.) + }, 60000); try { // 1. Fetch Preferences from User Profile (Weights, Scale, Tags) @@ -367,19 +458,7 @@ export const initStore = async () => { content: meta.content || "" }); } else { - allTasks.push({ - id: r.id, - title: r.title, - startDate: r.startDate, - dueDate: r.dueDate, - priority: r.priority, - completed: r.completed, - tags: tags, - content: r.content, - deletedAt: r.deletedAt ? new Date(r.deletedAt).getTime() : undefined, - created: r.created, - updated: r.updated - }); + allTasks.push(mapRecordToTask(r)); } }); @@ -389,6 +468,9 @@ export const initStore = async () => { // Check recurring tasks after load checkRecurringTasks(); + // 3. Subscribe to Realtime Updates + await subscribeToRealtime(); + // 4. Reconstructive Migration: Verify all tags in tasks have definitions const foundTagNames = new Set(); allTasks.forEach(t => t.tags?.forEach(tag => foundTagNames.add(tag))); @@ -458,12 +540,21 @@ export const addTask = async (title: string, options?: { dueDate?: Date | string }); // Replace temp task with real record (merging server fields like id, created, updated) - setStore("tasks", (t) => t.map(task => task.id === tempId ? { - ...task, - id: record.id, - created: record.created, - updated: record.updated - } : task)); + setStore("tasks", (t) => { + // Check if REAL ID exists already (from subscription race) + const realExists = t.some(x => x.id === record.id); + if (realExists) { + // If real exists, just remove temp. + return t.filter(x => x.id !== tempId); + } + // Otherwise, replace temp with real + return t.map(task => task.id === tempId ? { + ...task, + id: record.id, + created: record.created, + updated: record.updated + } : task); + }); } catch (err) { console.error("create failed", err); toast.error("Failed to save task.");