Skip to Content
TutorialsMobile AppSending Messages

Sending Messages

The ChatInput component handles composing and sending messages, including text, image attachments, and optimistic UI updates.

Optimistic UI Updates

Add the user’s message to the UI immediately before the API responds. This creates a snappy experience by showing the message right away. On error, the optimistic update is reverted by reloading from the database.

// Optimistic update: show the user's message in the UI immediately rather // than waiting for the API round-trip. On error we revert to the DB state. // After success we merge stored messages with the optimistic file URLs, // because the DB strips data URIs from attachments. const existingMessages = currentConversationId ? await getMessages(currentConversationId) : []; onMessagesChange([ ...existingMessages, { uniqueId: "temp", messageId: 0, conversationId: "", role: "user", content: prompt, files: optimisticFiles, createdAt: new Date(), updatedAt: new Date(), } as StoredMessage, ]);

Building Content Parts

The API expects content as an array of typed parts. Text is always included, and images are added as image_url parts when an attachment is present.

const userContent: Array<{ type: "text"; text: string } | { type: "image_url"; image_url: { url: string } }> = [ { type: "text", text: prompt }, ]; if (attachedImage) { userContent.push({ type: "image_url", image_url: { url: attachedImage } }); }

Calling sendMessage

The content parts are passed to sendMessage along with the selected model. includeHistory: true tells the SDK to prepend conversation history automatically.

const result = await sendMessage({ messages: [{ role: "user", content: userContent }], model: selectedModel, includeHistory: true, serverTools: [], });

Title Generation

After the first message in a new conversation, the title is set to a truncated version of the user’s input using updateConversationTitle from the SDK.

// Set conversation title to first message (truncated) const isNewConversation = !conversationId; if (isNewConversation) { const title = prompt.length > 50 ? prompt.slice(0, 50) + "..." : prompt; await updateConversationTitle(convId, title); }

Post-Stream Merge

After streaming completes, messages are reloaded from the database. Because WatermelonDB strips data URIs from file attachments, the merge step preserves the original image URLs from the optimistic update so attached images continue to render.

// Instead of reloading from DB (which strips data URIs), // merge stored messages with preserved file URLs from optimistic update const storedMessages = await getMessages(convId); // Merge: preserve file URLs for user messages that had attachments const mergedMessages = storedMessages.map((msg: StoredMessage) => { // If this is the user message we just sent and it had files if (msg.role === "user" && msg.content === prompt && optimisticFiles) { // Check if stored message has files without URLs (stripped data URIs) if (!msg.files || msg.files.every((f) => !f.url)) { return { ...msg, files: optimisticFiles }; } } return msg; }); onMessagesChange(mergedMessages);

Message Format Conversion

StoredMessage from the SDK is converted to a UI-friendly format. Messages with image files are transformed into multi-part content arrays so the chat view can render both text and images.

// Convert StoredMessage to UI Message format const toUIMessage = (msg: StoredMessage): Message => { // Check if message has files with URLs const imageFiles = msg.files?.filter((f) => f.url) || []; if (imageFiles.length === 0) { return { role: msg.role, content: msg.content }; } // Build content array with text and images const contentArray: MessageContent = [ { type: "text", text: msg.content }, ...imageFiles.map((f) => ({ type: "image_url" as const, image_url: { url: f.url! }, })), ]; return { role: msg.role, content: contentArray }; };
Last updated on