feat: add EditEventScreen with calendar and chat mode support

Add a unified event editor that works in two modes:
- Calendar mode: Create/edit events directly via EventService API
- Chat mode: Edit AI-proposed events before confirming them

The chat mode allows users to modify proposed events (title, time,
recurrence) and persists changes both locally and to the server.

New components: DateTimePicker, ScrollableDropdown, useDropdownPosition
New API: PUT /api/chat/messages/:messageId/proposal
This commit is contained in:
2026-01-31 18:46:31 +01:00
parent 617543a603
commit 6f0d172bf2
33 changed files with 1394 additions and 289 deletions

View File

@@ -9,7 +9,7 @@ import {
} from "react-native";
import { useThemeStore } from "../../stores/ThemeStore";
import React, { useState, useRef, useEffect, useCallback } from "react";
import { useFocusEffect } from "expo-router";
import { useFocusEffect, router } from "expo-router";
import Header from "../../components/Header";
import BaseBackground from "../../components/BaseBackground";
import { FlashList } from "@shopify/flash-list";
@@ -38,6 +38,7 @@ type ChatMessageProps = {
proposedChanges?: ProposedEventChange[];
onConfirm?: (proposalId: string, proposal: ProposedEventChange) => void;
onReject?: (proposalId: string) => void;
onEdit?: (proposalId: string, proposal: ProposedEventChange) => void;
};
type ChatInputProps = {
@@ -62,6 +63,7 @@ const Chat = () => {
const [currentConversationId, setCurrentConversationId] = useState<
string | undefined
>();
const [hasLoadedMessages, setHasLoadedMessages] = useState(false);
useEffect(() => {
const keyboardDidShow = Keyboard.addListener(
@@ -71,10 +73,11 @@ const Chat = () => {
return () => keyboardDidShow.remove();
}, []);
// Load existing messages from database once authenticated and screen is focused
// Load existing messages from database only once (on initial mount)
// Skip on subsequent focus events to preserve local edits (e.g., edited proposals)
useFocusEffect(
useCallback(() => {
if (isAuthLoading || !isAuthenticated) return;
if (isAuthLoading || !isAuthenticated || hasLoadedMessages) return;
const fetchMessages = async () => {
try {
@@ -91,10 +94,12 @@ const Chat = () => {
}
} catch (error) {
console.error("Failed to load messages:", error);
} finally {
setHasLoadedMessages(true);
}
};
fetchMessages();
}, [isAuthLoading, isAuthenticated]),
}, [isAuthLoading, isAuthenticated, hasLoadedMessages]),
);
const scrollToEnd = () => {
@@ -159,6 +164,22 @@ const Chat = () => {
}
};
const handleEditProposal = (
messageId: string,
conversationId: string,
proposalId: string,
proposal: ProposedEventChange,
) => {
router.push({
pathname: "/editEvent",
params: {
mode: "chat",
eventData: JSON.stringify(proposal.event),
proposalContext: JSON.stringify({ messageId, proposalId, conversationId }),
},
});
};
const handleSend = async (text: string) => {
// Show user message immediately
const userMessage: MessageData = {
@@ -238,6 +259,14 @@ const Chat = () => {
proposalId,
)
}
onEdit={(proposalId, proposal) =>
handleEditProposal(
item.id,
item.conversationId!,
proposalId,
proposal,
)
}
/>
)}
keyExtractor={(item) => item.id}
@@ -334,6 +363,7 @@ const ChatMessage = ({
proposedChanges,
onConfirm,
onReject,
onEdit,
}: ChatMessageProps) => {
const { theme } = useThemeStore();
const [currentIndex, setCurrentIndex] = useState(0);
@@ -361,7 +391,7 @@ const ChatMessage = ({
{content}
</Text>
{hasProposals && currentProposal && onConfirm && onReject && (
{hasProposals && currentProposal && onConfirm && onReject && onEdit && (
<View>
{/* Event card with optional navigation arrows */}
<View className="flex-row items-center">
@@ -381,8 +411,9 @@ const ChatMessage = ({
<View className="flex-1">
<ProposedEventCard
proposedChange={currentProposal}
onConfirm={() => onConfirm(currentProposal.id, currentProposal)}
onConfirm={(proposal) => onConfirm(proposal.id, proposal)}
onReject={() => onReject(currentProposal.id)}
onEdit={(proposal) => onEdit(proposal.id, proposal)}
/>
</View>