feat: improve chat keyboard handling and MonthSelector memory efficiency
- Add KeyboardAvoidingView with platform-specific behavior to chat screen - Implement auto-scroll to end on new messages and keyboard show - Configure keyboardDismissMode and keyboardShouldPersistTaps for better UX - Lazy-load MonthSelector data only when modal opens, clear on close - Add .env to gitignore
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules
|
||||
*.tsbuildinfo
|
||||
docs/praesi_2_context.md
|
||||
docs/*.png
|
||||
.env
|
||||
|
||||
@@ -296,7 +296,7 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
||||
- Tab navigation (Chat, Calendar) implemented with basic UI
|
||||
- Calendar screen fully functional:
|
||||
- Month navigation with grid display and Ionicons (chevron-back/forward)
|
||||
- MonthSelector dropdown with infinite scroll (dynamically loads months)
|
||||
- MonthSelector dropdown with infinite scroll (dynamically loads months, lazy-loaded when modal opens, cleared on close for memory efficiency)
|
||||
- Events loaded from API via EventService.getByDateRange()
|
||||
- Orange dot indicator for days with events
|
||||
- Tap-to-open modal overlay showing EventCards for selected day
|
||||
@@ -304,6 +304,9 @@ MONGODB_URI=mongodb://root:mongoose@localhost:27017/calchat?authSource=admin
|
||||
- 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)
|
||||
- KeyboardAvoidingView for proper keyboard handling (iOS padding, Android height)
|
||||
- Auto-scroll to end on new messages and keyboard show
|
||||
- keyboardDismissMode="interactive" and keyboardShouldPersistTaps="handled"
|
||||
- `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
|
||||
|
||||
@@ -323,16 +323,7 @@ const MonthSelector = ({
|
||||
const listRef = useRef<React.ComponentRef<typeof FlashList<MonthItem>>>(null);
|
||||
const INITIAL_RANGE = 12; // 12 months before and after current
|
||||
|
||||
const [monthSelectorData, setMonthSelectorData] = useState<MonthItem[]>(() =>
|
||||
generateMonths(currentYear, currentMonthIndex, INITIAL_RANGE),
|
||||
);
|
||||
|
||||
// Reset data when current month changes
|
||||
useEffect(() => {
|
||||
setMonthSelectorData(
|
||||
generateMonths(currentYear, currentMonthIndex, INITIAL_RANGE),
|
||||
);
|
||||
}, [currentYear, currentMonthIndex]);
|
||||
const [monthSelectorData, setMonthSelectorData] = useState<MonthItem[]>([]);
|
||||
|
||||
const appendMonths = (direction: "start" | "end", count: number) => {
|
||||
setMonthSelectorData((prevData) => {
|
||||
@@ -378,6 +369,10 @@ const MonthSelector = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (modalVisible) {
|
||||
// Generate fresh data centered on current month
|
||||
setMonthSelectorData(
|
||||
generateMonths(currentYear, currentMonthIndex, INITIAL_RANGE),
|
||||
);
|
||||
Animated.timing(heightAnim, {
|
||||
toValue: 200,
|
||||
duration: 200,
|
||||
@@ -385,8 +380,10 @@ const MonthSelector = ({
|
||||
}).start();
|
||||
} else {
|
||||
heightAnim.setValue(0);
|
||||
// Clear data when closing
|
||||
setMonthSelectorData([]);
|
||||
}
|
||||
}, [modalVisible, heightAnim]);
|
||||
}, [modalVisible, heightAnim, currentYear, currentMonthIndex]);
|
||||
|
||||
const renderItem = ({ item }: { item: MonthItem }) => (
|
||||
<Pressable
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { View, Text, TextInput, Pressable } from "react-native";
|
||||
import { View, Text, TextInput, Pressable, KeyboardAvoidingView, Platform, Keyboard } from "react-native";
|
||||
import currentTheme from "../../Themes";
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Header from "../../components/Header";
|
||||
import BaseBackground from "../../components/BaseBackground";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
@@ -31,6 +31,18 @@ type ChatInputProps = {
|
||||
|
||||
const Chat = () => {
|
||||
const { messages, addMessage, updateMessage } = useChatStore();
|
||||
const listRef = useRef<FlashList<MessageData>>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const keyboardDidShow = Keyboard.addListener("keyboardDidShow", scrollToEnd);
|
||||
return () => keyboardDidShow.remove();
|
||||
}, []);
|
||||
|
||||
const scrollToEnd = () => {
|
||||
setTimeout(() => {
|
||||
listRef.current?.scrollToEnd({ animated: true });
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleEventResponse = async (
|
||||
action: "confirm" | "reject",
|
||||
@@ -61,6 +73,7 @@ const Chat = () => {
|
||||
conversationId: response.conversationId,
|
||||
};
|
||||
addMessage(botMessage);
|
||||
scrollToEnd();
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${action} event:`, error);
|
||||
// Revert on error
|
||||
@@ -76,6 +89,7 @@ const Chat = () => {
|
||||
content: text,
|
||||
};
|
||||
addMessage(userMessage);
|
||||
scrollToEnd();
|
||||
|
||||
try {
|
||||
// Fetch server response
|
||||
@@ -90,6 +104,7 @@ const Chat = () => {
|
||||
conversationId: response.conversationId,
|
||||
};
|
||||
addMessage(botMessage);
|
||||
scrollToEnd();
|
||||
} catch (error) {
|
||||
console.error("Failed to send message:", error);
|
||||
}
|
||||
@@ -98,30 +113,38 @@ const Chat = () => {
|
||||
return (
|
||||
<BaseBackground>
|
||||
<ChatHeader />
|
||||
<FlashList
|
||||
data={messages}
|
||||
renderItem={({ item }) => (
|
||||
<ChatMessage
|
||||
side={item.side}
|
||||
content={item.content}
|
||||
proposedChange={item.proposedChange}
|
||||
respondedAction={item.respondedAction}
|
||||
onConfirm={() =>
|
||||
handleEventResponse(
|
||||
"confirm",
|
||||
item.id,
|
||||
item.conversationId!,
|
||||
item.proposedChange,
|
||||
)
|
||||
}
|
||||
onReject={() =>
|
||||
handleEventResponse("reject", item.id, item.conversationId!)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
/>
|
||||
<ChatInput onSend={handleSend} />
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<FlashList
|
||||
ref={listRef}
|
||||
data={messages}
|
||||
renderItem={({ item }) => (
|
||||
<ChatMessage
|
||||
side={item.side}
|
||||
content={item.content}
|
||||
proposedChange={item.proposedChange}
|
||||
respondedAction={item.respondedAction}
|
||||
onConfirm={() =>
|
||||
handleEventResponse(
|
||||
"confirm",
|
||||
item.id,
|
||||
item.conversationId!,
|
||||
item.proposedChange,
|
||||
)
|
||||
}
|
||||
onReject={() =>
|
||||
handleEventResponse("reject", item.id, item.conversationId!)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
keyExtractor={(item) => item.id}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
/>
|
||||
<ChatInput onSend={handleSend} />
|
||||
</KeyboardAvoidingView>
|
||||
</BaseBackground>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user