feat: add typing indicator with ChatBubble component
- Add ChatBubble component for reusable chat bubble styling - Add TypingIndicator component with animated dots (. .. ...) - Show typing indicator after 500ms delay while waiting for AI response - Refactor ChatMessage to use ChatBubble component - Add isWaitingForResponse state to ChatStore
This commit is contained in:
@@ -21,6 +21,8 @@ import {
|
||||
import { ProposedEventChange } from "@calchat/shared";
|
||||
import { ProposedEventCard } from "../../components/ProposedEventCard";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import TypingIndicator from "../../components/TypingIndicator";
|
||||
import { ChatBubble } from "../../components/ChatBubble";
|
||||
|
||||
// TODO: better shadows for everything
|
||||
// (maybe with extra library because of differences between android and ios)
|
||||
@@ -40,10 +42,20 @@ type ChatInputProps = {
|
||||
onSend: (text: string) => void;
|
||||
};
|
||||
|
||||
const TYPING_INDICATOR_DELAY_MS = 500;
|
||||
|
||||
const Chat = () => {
|
||||
const { messages, addMessage, addMessages, updateMessage } = useChatStore();
|
||||
const {
|
||||
messages,
|
||||
addMessage,
|
||||
addMessages,
|
||||
updateMessage,
|
||||
isWaitingForResponse,
|
||||
setWaitingForResponse,
|
||||
} = useChatStore();
|
||||
const listRef =
|
||||
useRef<React.ComponentRef<typeof FlashList<MessageData>>>(null);
|
||||
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
const [currentConversationId, setCurrentConversationId] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
@@ -145,6 +157,11 @@ const Chat = () => {
|
||||
addMessage(userMessage);
|
||||
scrollToEnd();
|
||||
|
||||
// Show typing indicator after delay
|
||||
typingTimeoutRef.current = setTimeout(() => {
|
||||
setWaitingForResponse(true);
|
||||
}, TYPING_INDICATOR_DELAY_MS);
|
||||
|
||||
try {
|
||||
// Fetch server response (include conversationId for existing conversations)
|
||||
const response = await ChatService.sendMessage({
|
||||
@@ -169,6 +186,10 @@ const Chat = () => {
|
||||
scrollToEnd();
|
||||
} catch (error) {
|
||||
console.error("Failed to send message:", error);
|
||||
} finally {
|
||||
// Hide typing indicator
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
setWaitingForResponse(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -204,6 +225,7 @@ const Chat = () => {
|
||||
keyExtractor={(item) => item.id}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
ListFooterComponent={isWaitingForResponse ? <TypingIndicator /> : null}
|
||||
/>
|
||||
<ChatInput onSend={handleSend} />
|
||||
</KeyboardAvoidingView>
|
||||
@@ -290,13 +312,6 @@ const ChatMessage = ({
|
||||
}: ChatMessageProps) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
const borderColor =
|
||||
side === "left" ? currentTheme.chatBot : currentTheme.primeFg;
|
||||
const selfSide =
|
||||
side === "left"
|
||||
? "self-start ml-2 rounded-bl-sm"
|
||||
: "self-end mr-2 rounded-br-sm";
|
||||
|
||||
const hasProposals = proposedChanges && proposedChanges.length > 0;
|
||||
const hasMultiple = proposedChanges && proposedChanges.length > 1;
|
||||
const currentProposal = proposedChanges?.[currentIndex];
|
||||
@@ -311,13 +326,11 @@ const ChatMessage = ({
|
||||
const canGoNext = currentIndex < (proposedChanges?.length || 1) - 1;
|
||||
|
||||
return (
|
||||
<View
|
||||
className={`bg-white border-2 border-solid rounded-xl my-2 ${selfSide}`}
|
||||
<ChatBubble
|
||||
side={side}
|
||||
style={{
|
||||
borderColor: borderColor,
|
||||
maxWidth: "80%",
|
||||
minWidth: hasProposals ? "75%" : undefined,
|
||||
elevation: 8,
|
||||
}}
|
||||
>
|
||||
<Text className="p-2">{content}</Text>
|
||||
@@ -379,7 +392,7 @@ const ChatMessage = ({
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</ChatBubble>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user