implement chat messaging with event proposals
- Add functional chat with server communication and test responses - Add ProposedEventCard component for confirm/reject actions - Move Constants (Day, Month) from client to shared package - Add dateHelpers utility for weekday calculations - Extend Themes.tsx with button and text colors - Update CLAUDE.md with current implementation status - Add *.tsbuildinfo to .gitignore
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { View, Text, TextInput } from "react-native";
|
||||
import { View, Text, TextInput, Pressable } from "react-native";
|
||||
import currentTheme from "../../Themes";
|
||||
import { useState } from "react";
|
||||
import Header from "../../components/Header";
|
||||
import BaseBackground from "../../components/BaseBackground";
|
||||
import { FlashList } from "@shopify/flash-list";
|
||||
import { ChatService } from "../../services";
|
||||
import { ProposedEventChange } from "@caldav/shared";
|
||||
import { ProposedEventCard } from "../../components/ProposedEventCard";
|
||||
|
||||
// TODO: better shadows for everything
|
||||
// (maybe with extra library because of differences between android and ios)
|
||||
@@ -11,239 +14,91 @@ import { FlashList } from "@shopify/flash-list";
|
||||
// TODO: create new messages
|
||||
|
||||
type BubbleSide = "left" | "right";
|
||||
|
||||
type ChatMessageProps = {
|
||||
side: BubbleSide;
|
||||
width: number;
|
||||
height: number;
|
||||
content: string;
|
||||
proposedChange?: ProposedEventChange;
|
||||
respondedAction?: "confirm" | "reject";
|
||||
onConfirm?: () => void;
|
||||
onReject?: () => void;
|
||||
};
|
||||
|
||||
type MessageData = {
|
||||
type MessageData = ChatMessageProps & {
|
||||
id: string;
|
||||
side: BubbleSide;
|
||||
width: number;
|
||||
height: number;
|
||||
conversationId?: string;
|
||||
};
|
||||
|
||||
// NOTE: only for testing
|
||||
const getRandomInt = (min: number, max: number) => {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
type ChatInputProps = {
|
||||
onSend: (text: string) => void;
|
||||
};
|
||||
|
||||
const randomWidth = () => getRandomInt(100, 400);
|
||||
const randomHeight = () => getRandomInt(50, 100);
|
||||
|
||||
const messages: MessageData[] = [
|
||||
// {{{
|
||||
{
|
||||
id: "1",
|
||||
side: "left",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
side: "right",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
side: "left",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
side: "right",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
side: "left",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
side: "right",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
side: "left",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
side: "right",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
side: "left",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
side: "right",
|
||||
width: randomWidth(),
|
||||
height: randomHeight(),
|
||||
},
|
||||
// {
|
||||
// id: "11",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "12",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "13",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "14",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "15",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "16",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "17",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "18",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "19",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "20",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "21",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "22",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "23",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "24",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "25",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "26",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "27",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "28",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "29",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "30",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "31",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "32",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "33",
|
||||
// side: "left",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
// {
|
||||
// id: "34",
|
||||
// side: "right",
|
||||
// width: randomWidth(),
|
||||
// height: randomHeight(),
|
||||
// },
|
||||
//, width: randomWidth, height: getRandomInt(50, 500) }}}
|
||||
];
|
||||
|
||||
const Chat = () => {
|
||||
const [messages, setMessages] = useState<MessageData[]>([]);
|
||||
|
||||
const handleEventResponse = async (
|
||||
action: "confirm" | "reject",
|
||||
messageId: string,
|
||||
conversationId: string
|
||||
) => {
|
||||
// Mark message as responded (optimistic update)
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) =>
|
||||
msg.id === messageId ? { ...msg, respondedAction: action } : msg
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
const response =
|
||||
action === "confirm"
|
||||
? await ChatService.confirmEvent(conversationId, messageId)
|
||||
: await ChatService.rejectEvent(conversationId, messageId);
|
||||
|
||||
const botMessage: MessageData = {
|
||||
id: response.message.id,
|
||||
side: "left",
|
||||
content: response.message.content,
|
||||
conversationId: response.conversationId,
|
||||
};
|
||||
setMessages((prev) => [...prev, 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
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async (text: string) => {
|
||||
// Show user message immediately
|
||||
const userMessage: MessageData = {
|
||||
id: Date.now().toString(),
|
||||
side: "right",
|
||||
content: text,
|
||||
};
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
|
||||
try {
|
||||
// Fetch server response
|
||||
const response = await ChatService.sendMessage({ content: text });
|
||||
|
||||
// Show bot response
|
||||
const botMessage: MessageData = {
|
||||
id: response.message.id,
|
||||
side: "left",
|
||||
content: response.message.content,
|
||||
proposedChange: response.message.proposedChange,
|
||||
conversationId: response.conversationId,
|
||||
};
|
||||
setMessages((prev) => [...prev, botMessage]);
|
||||
} catch (error) {
|
||||
console.error("Failed to send message:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseBackground>
|
||||
<ChatHeader />
|
||||
@@ -252,18 +107,20 @@ const Chat = () => {
|
||||
renderItem={({ item }) => (
|
||||
<ChatMessage
|
||||
side={item.side}
|
||||
width={item.width}
|
||||
height={item.height}
|
||||
content={item.content}
|
||||
proposedChange={item.proposedChange}
|
||||
respondedAction={item.respondedAction}
|
||||
onConfirm={() =>
|
||||
handleEventResponse("confirm", item.id, item.conversationId!)
|
||||
}
|
||||
onReject={() =>
|
||||
handleEventResponse("reject", item.id, item.conversationId!)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
maintainVisibleContentPosition={{
|
||||
autoscrollToBottomThreshold: 0.2,
|
||||
startRenderingFromBottom: true,
|
||||
}}
|
||||
keyExtractor={(item) => item.id}
|
||||
// extraData={selectedId} might need this later for re-rendering
|
||||
/>
|
||||
<ChatInput />
|
||||
<ChatInput onSend={handleSend} />
|
||||
</BaseBackground>
|
||||
);
|
||||
};
|
||||
@@ -297,51 +154,81 @@ const ChatHeader = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ChatInput = () => {
|
||||
const [text, onChangeText] = useState("Nachricht");
|
||||
const MIN_INPUT_HEIGHT = 40;
|
||||
const MAX_INPUT_HEIGHT = 150;
|
||||
|
||||
const ChatInput = ({ onSend }: ChatInputProps) => {
|
||||
const [text, setText] = useState("");
|
||||
|
||||
const handleSend = () => {
|
||||
if (text.trim()) {
|
||||
onSend(text.trim());
|
||||
setText("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex flex-row w-full h-8 my-2">
|
||||
<View className="flex flex-row w-full items-end my-2 px-2">
|
||||
<TextInput
|
||||
className="w-4/5 h-full border border-solid rounded-2xl mx-2 px-2"
|
||||
className="flex-1 border border-solid rounded-2xl px-3 py-2 mr-2"
|
||||
style={{
|
||||
backgroundColor: currentTheme.messageBorderBg,
|
||||
minHeight: MIN_INPUT_HEIGHT,
|
||||
maxHeight: MAX_INPUT_HEIGHT,
|
||||
textAlignVertical: "top",
|
||||
}}
|
||||
onChangeText={onChangeText}
|
||||
onChangeText={setText}
|
||||
value={text}
|
||||
placeholder="Nachricht..."
|
||||
placeholderTextColor="#999"
|
||||
multiline
|
||||
/>
|
||||
<View
|
||||
className="w-8 h-full rounded-2xl"
|
||||
style={{
|
||||
backgroundColor: currentTheme.placeholderBg,
|
||||
}}
|
||||
></View>
|
||||
<Pressable onPress={handleSend}>
|
||||
<View
|
||||
className="w-10 h-10 rounded-full items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: currentTheme.placeholderBg,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatMessage = (props: ChatMessageProps) => {
|
||||
const ChatMessage = ({
|
||||
side,
|
||||
content,
|
||||
proposedChange,
|
||||
respondedAction,
|
||||
onConfirm,
|
||||
onReject,
|
||||
}: ChatMessageProps) => {
|
||||
const borderColor =
|
||||
props.side === "left" ? currentTheme.chatBot : currentTheme.primeFg;
|
||||
side === "left" ? currentTheme.chatBot : currentTheme.primeFg;
|
||||
const selfSide =
|
||||
props.side === "left"
|
||||
side === "left"
|
||||
? "self-start ml-2 rounded-bl-sm"
|
||||
: "self-end mr-2 rounded-br-sm";
|
||||
|
||||
return (
|
||||
<View
|
||||
className={
|
||||
`bg-white border-2 border-solid rounded-xl my-2 ` + `${selfSide}`
|
||||
}
|
||||
className={`bg-white border-2 border-solid rounded-xl my-2 ${selfSide}`}
|
||||
style={{
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
borderColor: borderColor,
|
||||
|
||||
maxWidth: "80%",
|
||||
elevation: 8,
|
||||
}}
|
||||
>
|
||||
<Text className="p-1">Lorem Ipsum Dolor sit amet</Text>
|
||||
<Text className="p-2">{content}</Text>
|
||||
|
||||
{proposedChange && onConfirm && onReject && (
|
||||
<ProposedEventCard
|
||||
proposedChange={proposedChange}
|
||||
respondedAction={respondedAction}
|
||||
onConfirm={onConfirm}
|
||||
onReject={onReject}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user