From 7c081787fedef011e902fee7ae86096b25f67fff Mon Sep 17 00:00:00 2001 From: Linus Waldowsky Date: Sun, 4 Jan 2026 17:40:40 +0100 Subject: [PATCH] fix tab switching state issues - Add useFocusEffect to calendar for automatic event reload on tab focus - Create ChatStore (Zustand) for persistent chat messages across tab switches - Replace local useState with store in chat screen --- CLAUDE.md | 4 ++ apps/client/src/app/(tabs)/calendar.tsx | 67 ++++++++++++++----------- apps/client/src/app/(tabs)/chat.tsx | 26 +++------- apps/client/src/stores/ChatStore.ts | 37 ++++++++++++++ apps/client/src/stores/index.ts | 1 + 5 files changed, 87 insertions(+), 48 deletions(-) create mode 100644 apps/client/src/stores/ChatStore.ts diff --git a/CLAUDE.md b/CLAUDE.md index 42090c3..d7dd982 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,6 +90,7 @@ src/ └── stores/ # Zustand state management ├── index.ts # Re-exports all stores ├── AuthStore.ts # user, token, isAuthenticated, login(), logout(), setToken() + ├── ChatStore.ts # messages[], addMessage(), updateMessage(), clearMessages() └── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent() ``` @@ -298,7 +299,9 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin - Orange dot indicator for days with events - Tap-to-open modal overlay showing EventCards for selected day - Supports events from adjacent months visible in grid + - Uses `useFocusEffect` for automatic reload on tab focus - Chat screen functional with FlashList, message sending, and event confirm/reject + - Messages persisted via ChatStore (survives tab switches) - `ApiClient`: get(), post(), put(), delete() implemented - `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully implemented - `ChatService`: sendMessage(), confirmEvent(convId, msgId, action, event?, eventId?, updates?), rejectEvent() - supports create/update/delete actions @@ -306,6 +309,7 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin - `ProposedEventCard`: Displays proposed events (title, date, description, recurring indicator) with confirm/reject buttons - `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator - `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[] +- `ChatStore`: Zustand store with addMessage(), updateMessage(), clearMessages() - persists messages across tab switches - Auth screens (Login, Register), Event Detail, and Note screens exist as skeletons - AuthStore defined with `throw new Error('Not implemented')` diff --git a/apps/client/src/app/(tabs)/calendar.tsx b/apps/client/src/app/(tabs)/calendar.tsx index e84465d..3208ef1 100644 --- a/apps/client/src/app/(tabs)/calendar.tsx +++ b/apps/client/src/app/(tabs)/calendar.tsx @@ -10,7 +10,14 @@ import { import { DAYS, MONTHS, Month, ExpandedEvent } from "@caldav/shared"; import Header from "../../components/Header"; import { EventCard } from "../../components/EventCard"; -import React, { useEffect, useMemo, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useFocusEffect } from "expo-router"; import currentTheme from "../../Themes"; import BaseBackground from "../../components/BaseBackground"; import { FlashList } from "@shopify/flash-list"; @@ -26,37 +33,39 @@ const Calendar = () => { const { events, setEvents, deleteEvent } = useEventsStore(); - // Load events when month/year changes + // Load events when tab gains focus or month/year changes // Include days from prev/next month that are visible in the grid - useEffect(() => { - const loadEvents = async () => { - try { - // Calculate first visible day (up to 6 days before month start) - const firstOfMonth = new Date(currentYear, monthIndex, 1); - const dayOfWeek = firstOfMonth.getDay(); - const daysFromPrevMonth = dayOfWeek === 0 ? 6 : dayOfWeek - 1; - const startDate = new Date( - currentYear, - monthIndex, - 1 - daysFromPrevMonth, - ); + useFocusEffect( + useCallback(() => { + const loadEvents = async () => { + try { + // Calculate first visible day (up to 6 days before month start) + const firstOfMonth = new Date(currentYear, monthIndex, 1); + const dayOfWeek = firstOfMonth.getDay(); + const daysFromPrevMonth = dayOfWeek === 0 ? 6 : dayOfWeek - 1; + const startDate = new Date( + currentYear, + monthIndex, + 1 - daysFromPrevMonth, + ); - // Calculate last visible day (6 weeks * 7 days = 42 days total) - const endDate = new Date(startDate); - endDate.setDate(startDate.getDate() + 41); - endDate.setHours(23, 59, 59); + // Calculate last visible day (6 weeks * 7 days = 42 days total) + const endDate = new Date(startDate); + endDate.setDate(startDate.getDate() + 41); + endDate.setHours(23, 59, 59); - const loadedEvents = await EventService.getByDateRange( - startDate, - endDate, - ); - setEvents(loadedEvents); - } catch (error) { - console.error("Failed to load events:", error); - } - }; - loadEvents(); - }, [monthIndex, currentYear, setEvents]); + const loadedEvents = await EventService.getByDateRange( + startDate, + endDate, + ); + setEvents(loadedEvents); + } catch (error) { + console.error("Failed to load events:", error); + } + }; + loadEvents(); + }, [monthIndex, currentYear, setEvents]), + ); // Group events by date (YYYY-MM-DD format) const eventsByDate = useMemo(() => { diff --git a/apps/client/src/app/(tabs)/chat.tsx b/apps/client/src/app/(tabs)/chat.tsx index a2adcf1..a4d866a 100644 --- a/apps/client/src/app/(tabs)/chat.tsx +++ b/apps/client/src/app/(tabs)/chat.tsx @@ -5,6 +5,7 @@ import Header from "../../components/Header"; import BaseBackground from "../../components/BaseBackground"; import { FlashList } from "@shopify/flash-list"; import { ChatService } from "../../services"; +import { useChatStore, MessageData } from "../../stores"; import { ProposedEventChange } from "@caldav/shared"; import { ProposedEventCard } from "../../components/ProposedEventCard"; @@ -24,17 +25,12 @@ type ChatMessageProps = { onReject?: () => void; }; -type MessageData = ChatMessageProps & { - id: string; - conversationId?: string; -}; - type ChatInputProps = { onSend: (text: string) => void; }; const Chat = () => { - const [messages, setMessages] = useState([]); + const { messages, addMessage, updateMessage } = useChatStore(); const handleEventResponse = async ( action: "confirm" | "reject", @@ -43,11 +39,7 @@ const Chat = () => { proposedChange?: ProposedEventChange, ) => { // Mark message as responded (optimistic update) - setMessages((prev) => - prev.map((msg) => - msg.id === messageId ? { ...msg, respondedAction: action } : msg, - ), - ); + updateMessage(messageId, { respondedAction: action }); try { const response = @@ -68,15 +60,11 @@ const Chat = () => { content: response.message.content, conversationId: response.conversationId, }; - setMessages((prev) => [...prev, botMessage]); + addMessage(botMessage); } catch (error) { console.error(`Failed to ${action} event:`, error); // Revert on error - setMessages((prev) => - prev.map((msg) => - msg.id === messageId ? { ...msg, respondedAction: undefined } : msg, - ), - ); + updateMessage(messageId, { respondedAction: undefined }); } }; @@ -87,7 +75,7 @@ const Chat = () => { side: "right", content: text, }; - setMessages((prev) => [...prev, userMessage]); + addMessage(userMessage); try { // Fetch server response @@ -101,7 +89,7 @@ const Chat = () => { proposedChange: response.message.proposedChange, conversationId: response.conversationId, }; - setMessages((prev) => [...prev, botMessage]); + addMessage(botMessage); } catch (error) { console.error("Failed to send message:", error); } diff --git a/apps/client/src/stores/ChatStore.ts b/apps/client/src/stores/ChatStore.ts new file mode 100644 index 0000000..3898a5e --- /dev/null +++ b/apps/client/src/stores/ChatStore.ts @@ -0,0 +1,37 @@ +import { create } from "zustand"; +import { ProposedEventChange } from "@caldav/shared"; + +type BubbleSide = "left" | "right"; + +export type MessageData = { + id: string; + side: BubbleSide; + content: string; + proposedChange?: ProposedEventChange; + respondedAction?: "confirm" | "reject"; + conversationId?: string; +}; + +interface ChatState { + messages: MessageData[]; + addMessage: (message: MessageData) => void; + updateMessage: (id: string, updates: Partial) => void; + clearMessages: () => void; +} + +export const useChatStore = create((set) => ({ + messages: [], + addMessage: (message: MessageData) => { + set((state) => ({ messages: [...state.messages, message] })); + }, + updateMessage: (id: string, updates: Partial) => { + set((state) => ({ + messages: state.messages.map((msg) => + msg.id === id ? { ...msg, ...updates } : msg, + ), + })); + }, + clearMessages: () => { + set({ messages: [] }); + }, +})); diff --git a/apps/client/src/stores/index.ts b/apps/client/src/stores/index.ts index 51bdbb1..41455b2 100644 --- a/apps/client/src/stores/index.ts +++ b/apps/client/src/stores/index.ts @@ -1,2 +1,3 @@ export { useAuthStore } from "./AuthStore"; +export { useChatStore, type MessageData } from "./ChatStore"; export { useEventsStore } from "./EventsStore";