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:
@@ -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')`
|
||||||
|
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
37
apps/client/src/stores/ChatStore.ts
Normal file
37
apps/client/src/stores/ChatStore.ts
Normal 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: [] });
|
||||||
|
},
|
||||||
|
}));
|
||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user