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
This commit is contained in:
2026-01-04 17:40:40 +01:00
parent 1532acab78
commit 7c081787fe
5 changed files with 87 additions and 48 deletions

View File

@@ -90,6 +90,7 @@ src/
└── stores/ # Zustand state management └── stores/ # Zustand state management
├── index.ts # Re-exports all stores ├── index.ts # Re-exports all stores
├── AuthStore.ts # user, token, isAuthenticated, login(), logout(), setToken() ├── AuthStore.ts # user, token, isAuthenticated, login(), logout(), setToken()
├── ChatStore.ts # messages[], addMessage(), updateMessage(), clearMessages()
└── EventsStore.ts # events[], setEvents(), addEvent(), updateEvent(), deleteEvent() └── 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 - Orange dot indicator for days with events
- Tap-to-open modal overlay showing EventCards for selected day - Tap-to-open modal overlay showing EventCards for selected day
- Supports events from adjacent months visible in grid - 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 - Chat screen functional with FlashList, message sending, and event confirm/reject
- Messages persisted via ChatStore (survives tab switches)
- `ApiClient`: get(), post(), put(), delete() implemented - `ApiClient`: get(), post(), put(), delete() implemented
- `EventService`: getAll(), getById(), getByDateRange(), create(), update(), delete() - fully 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 - `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 - `ProposedEventCard`: Displays proposed events (title, date, description, recurring indicator) with confirm/reject buttons
- `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator - `Themes.tsx`: Centralized color definitions including textPrimary, borderPrimary, eventIndicator
- `EventsStore`: Zustand store with setEvents(), addEvent(), updateEvent(), deleteEvent() - stores ExpandedEvent[] - `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 - Auth screens (Login, Register), Event Detail, and Note screens exist as skeletons
- AuthStore defined with `throw new Error('Not implemented')` - AuthStore defined with `throw new Error('Not implemented')`

View File

@@ -10,7 +10,14 @@ import {
import { DAYS, MONTHS, Month, ExpandedEvent } from "@caldav/shared"; import { DAYS, MONTHS, Month, ExpandedEvent } from "@caldav/shared";
import Header from "../../components/Header"; import Header from "../../components/Header";
import { EventCard } from "../../components/EventCard"; 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 currentTheme from "../../Themes";
import BaseBackground from "../../components/BaseBackground"; import BaseBackground from "../../components/BaseBackground";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
@@ -26,37 +33,39 @@ const Calendar = () => {
const { events, setEvents, deleteEvent } = useEventsStore(); 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 // Include days from prev/next month that are visible in the grid
useEffect(() => { useFocusEffect(
const loadEvents = async () => { useCallback(() => {
try { const loadEvents = async () => {
// Calculate first visible day (up to 6 days before month start) try {
const firstOfMonth = new Date(currentYear, monthIndex, 1); // Calculate first visible day (up to 6 days before month start)
const dayOfWeek = firstOfMonth.getDay(); const firstOfMonth = new Date(currentYear, monthIndex, 1);
const daysFromPrevMonth = dayOfWeek === 0 ? 6 : dayOfWeek - 1; const dayOfWeek = firstOfMonth.getDay();
const startDate = new Date( const daysFromPrevMonth = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
currentYear, const startDate = new Date(
monthIndex, currentYear,
1 - daysFromPrevMonth, monthIndex,
); 1 - daysFromPrevMonth,
);
// Calculate last visible day (6 weeks * 7 days = 42 days total) // Calculate last visible day (6 weeks * 7 days = 42 days total)
const endDate = new Date(startDate); const endDate = new Date(startDate);
endDate.setDate(startDate.getDate() + 41); endDate.setDate(startDate.getDate() + 41);
endDate.setHours(23, 59, 59); endDate.setHours(23, 59, 59);
const loadedEvents = await EventService.getByDateRange( const loadedEvents = await EventService.getByDateRange(
startDate, startDate,
endDate, endDate,
); );
setEvents(loadedEvents); setEvents(loadedEvents);
} catch (error) { } catch (error) {
console.error("Failed to load events:", error); console.error("Failed to load events:", error);
} }
}; };
loadEvents(); loadEvents();
}, [monthIndex, currentYear, setEvents]); }, [monthIndex, currentYear, setEvents]),
);
// Group events by date (YYYY-MM-DD format) // Group events by date (YYYY-MM-DD format)
const eventsByDate = useMemo(() => { const eventsByDate = useMemo(() => {

View File

@@ -5,6 +5,7 @@ import Header from "../../components/Header";
import BaseBackground from "../../components/BaseBackground"; import BaseBackground from "../../components/BaseBackground";
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { ChatService } from "../../services"; import { ChatService } from "../../services";
import { useChatStore, MessageData } from "../../stores";
import { ProposedEventChange } from "@caldav/shared"; import { ProposedEventChange } from "@caldav/shared";
import { ProposedEventCard } from "../../components/ProposedEventCard"; import { ProposedEventCard } from "../../components/ProposedEventCard";
@@ -24,17 +25,12 @@ type ChatMessageProps = {
onReject?: () => void; onReject?: () => void;
}; };
type MessageData = ChatMessageProps & {
id: string;
conversationId?: string;
};
type ChatInputProps = { type ChatInputProps = {
onSend: (text: string) => void; onSend: (text: string) => void;
}; };
const Chat = () => { const Chat = () => {
const [messages, setMessages] = useState<MessageData[]>([]); const { messages, addMessage, updateMessage } = useChatStore();
const handleEventResponse = async ( const handleEventResponse = async (
action: "confirm" | "reject", action: "confirm" | "reject",
@@ -43,11 +39,7 @@ const Chat = () => {
proposedChange?: ProposedEventChange, proposedChange?: ProposedEventChange,
) => { ) => {
// Mark message as responded (optimistic update) // Mark message as responded (optimistic update)
setMessages((prev) => updateMessage(messageId, { respondedAction: action });
prev.map((msg) =>
msg.id === messageId ? { ...msg, respondedAction: action } : msg,
),
);
try { try {
const response = const response =
@@ -68,15 +60,11 @@ const Chat = () => {
content: response.message.content, content: response.message.content,
conversationId: response.conversationId, conversationId: response.conversationId,
}; };
setMessages((prev) => [...prev, botMessage]); addMessage(botMessage);
} catch (error) { } catch (error) {
console.error(`Failed to ${action} event:`, error); console.error(`Failed to ${action} event:`, error);
// Revert on error // Revert on error
setMessages((prev) => updateMessage(messageId, { respondedAction: undefined });
prev.map((msg) =>
msg.id === messageId ? { ...msg, respondedAction: undefined } : msg,
),
);
} }
}; };
@@ -87,7 +75,7 @@ const Chat = () => {
side: "right", side: "right",
content: text, content: text,
}; };
setMessages((prev) => [...prev, userMessage]); addMessage(userMessage);
try { try {
// Fetch server response // Fetch server response
@@ -101,7 +89,7 @@ const Chat = () => {
proposedChange: response.message.proposedChange, proposedChange: response.message.proposedChange,
conversationId: response.conversationId, conversationId: response.conversationId,
}; };
setMessages((prev) => [...prev, botMessage]); addMessage(botMessage);
} catch (error) { } catch (error) {
console.error("Failed to send message:", error); console.error("Failed to send message:", error);
} }

View File

@@ -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<MessageData>) => void;
clearMessages: () => void;
}
export const useChatStore = create<ChatState>((set) => ({
messages: [],
addMessage: (message: MessageData) => {
set((state) => ({ messages: [...state.messages, message] }));
},
updateMessage: (id: string, updates: Partial<MessageData>) => {
set((state) => ({
messages: state.messages.map((msg) =>
msg.id === id ? { ...msg, ...updates } : msg,
),
}));
},
clearMessages: () => {
set({ messages: [] });
},
}));

View File

@@ -1,2 +1,3 @@
export { useAuthStore } from "./AuthStore"; export { useAuthStore } from "./AuthStore";
export { useChatStore, type MessageData } from "./ChatStore";
export { useEventsStore } from "./EventsStore"; export { useEventsStore } from "./EventsStore";