A React Native app for the ultimate thinking partner.

feat: Redesign chat UI with modern, polished styling

MessageBubble.v2:
- Create brand new message bubble component with proper theme support
- User messages: warm orange background, right-aligned
- Assistant messages: surface background with border, left-aligned
- System messages: centered, muted, tertiary background
- Proper spacing, typography (Lexend), and border radius
- Platform-specific shadows and effects
- Support for multimodal content (images + text)

MessageInput.v2:
- Larger, more accessible input (44px min height)
- Rounded send button with orange background when active
- Better visual feedback (opacity, background color changes)
- Improved typography with Lexend font family
- Subtle background for attach button

ChatScreen:
- Update to use MessageBubbleV2
- Improve spacing and padding
- Platform-specific bottom padding for iOS
- Subtle border-top on input container

Design improvements:
- Consistent 8px vertical spacing between messages
- 75% max width for bubbles (better readability)
- Proper timestamp styling
- Better contrast and visual hierarchy

+242 -14
+217
src/components/MessageBubble.v2.tsx
··· 1 + import React from 'react'; 2 + import { View, Text, StyleSheet, Image, Platform } from 'react-native'; 3 + import { LettaMessage } from '../types/letta'; 4 + import MessageContent from './MessageContent'; 5 + import ToolCallItem from './ToolCallItem'; 6 + import type { Theme } from '../theme'; 7 + 8 + interface MessageBubbleProps { 9 + message: LettaMessage; 10 + theme: Theme; 11 + } 12 + 13 + export const MessageBubbleV2: React.FC<MessageBubbleProps> = ({ message, theme }) => { 14 + const isUser = message.role === 'user'; 15 + const isSystem = message.role === 'system'; 16 + const isTool = message.role === 'tool' || message.message_type?.includes('tool'); 17 + 18 + const formatTime = (dateString: string) => { 19 + const date = new Date(dateString); 20 + return date.toLocaleTimeString('en-US', { 21 + hour: 'numeric', 22 + minute: '2-digit', 23 + hour12: true 24 + }); 25 + }; 26 + 27 + // Render tool call messages separately 28 + if (isTool) { 29 + return ( 30 + <View style={styles.toolContainer}> 31 + <ToolCallItem message={message} theme={theme} /> 32 + </View> 33 + ); 34 + } 35 + 36 + // Render multimodal content (images + text) 37 + const renderContent = () => { 38 + if (Array.isArray(message.content)) { 39 + return ( 40 + <View> 41 + {message.content.map((item: any, index: number) => { 42 + if (item.type === 'image' && item.source) { 43 + const imageUri = item.source.type === 'base64' 44 + ? `data:${item.source.mediaType || 'image/jpeg'};base64,${item.source.data}` 45 + : item.source.url; 46 + 47 + return ( 48 + <Image 49 + key={index} 50 + source={{ uri: imageUri }} 51 + style={styles.messageImage} 52 + resizeMode="cover" 53 + /> 54 + ); 55 + } else if (item.type === 'text') { 56 + return ( 57 + <Text 58 + key={index} 59 + style={[ 60 + styles.messageText, 61 + { color: isUser ? theme.colors.text.inverse : theme.colors.text.primary } 62 + ]} 63 + > 64 + {item.text} 65 + </Text> 66 + ); 67 + } 68 + return null; 69 + })} 70 + </View> 71 + ); 72 + } 73 + 74 + // Render regular text content 75 + if (typeof message.content === 'string') { 76 + return ( 77 + <MessageContent 78 + content={message.content} 79 + messageType={message.message_type} 80 + theme={theme} 81 + /> 82 + ); 83 + } 84 + 85 + return null; 86 + }; 87 + 88 + // System messages - centered, muted 89 + if (isSystem) { 90 + return ( 91 + <View style={styles.systemContainer}> 92 + <View 93 + style={[ 94 + styles.systemBubble, 95 + { 96 + backgroundColor: theme.colors.background.tertiary, 97 + borderColor: theme.colors.border.primary, 98 + } 99 + ]} 100 + > 101 + <Text style={[styles.systemText, { color: theme.colors.text.secondary }]}> 102 + {typeof message.content === 'string' ? message.content : 'System message'} 103 + </Text> 104 + <Text style={[styles.timestamp, { color: theme.colors.text.tertiary }]}> 105 + {formatTime(message.created_at)} 106 + </Text> 107 + </View> 108 + </View> 109 + ); 110 + } 111 + 112 + // User and Assistant messages 113 + return ( 114 + <View style={[styles.container, isUser ? styles.userContainer : styles.assistantContainer]}> 115 + <View 116 + style={[ 117 + styles.bubble, 118 + isUser ? [ 119 + styles.userBubble, 120 + { backgroundColor: theme.colors.interactive.primary } 121 + ] : [ 122 + styles.assistantBubble, 123 + { 124 + backgroundColor: theme.colors.background.surface, 125 + borderColor: theme.colors.border.primary, 126 + } 127 + ], 128 + Platform.OS === 'web' && isUser && { 129 + // @ts-ignore 130 + boxShadow: '0 2px 8px rgba(239, 160, 78, 0.2)', 131 + }, 132 + Platform.OS === 'web' && !isUser && { 133 + // @ts-ignore 134 + boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)', 135 + }, 136 + ]} 137 + > 138 + {renderContent()} 139 + <Text 140 + style={[ 141 + styles.timestamp, 142 + { color: isUser ? 'rgba(255, 255, 255, 0.7)' : theme.colors.text.tertiary } 143 + ]} 144 + > 145 + {formatTime(message.created_at)} 146 + </Text> 147 + </View> 148 + </View> 149 + ); 150 + }; 151 + 152 + const styles = StyleSheet.create({ 153 + container: { 154 + marginVertical: 8, 155 + paddingHorizontal: 16, 156 + flexDirection: 'row', 157 + }, 158 + userContainer: { 159 + justifyContent: 'flex-end', 160 + }, 161 + assistantContainer: { 162 + justifyContent: 'flex-start', 163 + }, 164 + systemContainer: { 165 + marginVertical: 8, 166 + paddingHorizontal: 16, 167 + alignItems: 'center', 168 + }, 169 + toolContainer: { 170 + marginVertical: 4, 171 + paddingHorizontal: 16, 172 + }, 173 + bubble: { 174 + maxWidth: '75%', 175 + paddingHorizontal: 16, 176 + paddingVertical: 12, 177 + borderRadius: 20, 178 + }, 179 + userBubble: { 180 + borderBottomRightRadius: 6, 181 + }, 182 + assistantBubble: { 183 + borderBottomLeftRadius: 6, 184 + borderWidth: 1, 185 + }, 186 + systemBubble: { 187 + paddingHorizontal: 12, 188 + paddingVertical: 8, 189 + borderRadius: 12, 190 + borderWidth: 1, 191 + maxWidth: '90%', 192 + }, 193 + messageText: { 194 + fontSize: 16, 195 + lineHeight: 22, 196 + fontFamily: 'Lexend_400Regular', 197 + }, 198 + systemText: { 199 + fontSize: 13, 200 + lineHeight: 18, 201 + textAlign: 'center', 202 + fontFamily: 'Lexend_400Regular', 203 + }, 204 + messageImage: { 205 + width: 240, 206 + height: 240, 207 + borderRadius: 12, 208 + marginBottom: 8, 209 + }, 210 + timestamp: { 211 + fontSize: 11, 212 + marginTop: 6, 213 + fontFamily: 'Lexend_400Regular', 214 + }, 215 + }); 216 + 217 + export default MessageBubbleV2;
+20 -10
src/components/MessageInput.v2.tsx
··· 145 145 style={[ 146 146 styles.sendButton, 147 147 (inputText.trim() || selectedImages.length > 0) && !disabled 148 - ? { opacity: 1 } 149 - : { opacity: 0.5 }, 148 + ? { opacity: 1, backgroundColor: theme.colors.interactive.primary } 149 + : { opacity: 0.4, backgroundColor: 'rgba(239, 160, 78, 0.1)' }, 150 150 ]} 151 151 onPress={handleSend} 152 152 disabled={disabled || (!inputText.trim() && selectedImages.length === 0)} 153 153 > 154 - <Ionicons name="send" size={20} color={theme.colors.text.primary} /> 154 + <Ionicons 155 + name="send" 156 + size={18} 157 + color={(inputText.trim() || selectedImages.length > 0) && !disabled ? '#ffffff' : theme.colors.text.tertiary} 158 + /> 155 159 </TouchableOpacity> 156 160 </View> 157 161 </View> ··· 190 194 gap: 8, 191 195 }, 192 196 attachButton: { 193 - padding: 8, 197 + width: 40, 198 + height: 40, 199 + borderRadius: 20, 194 200 justifyContent: 'center', 195 201 alignItems: 'center', 196 202 }, 197 203 textInput: { 198 204 flex: 1, 199 - minHeight: 40, 205 + minHeight: 44, 200 206 maxHeight: 120, 201 - paddingHorizontal: 16, 202 - paddingVertical: 10, 203 - borderRadius: 20, 204 - fontSize: 16, 207 + paddingHorizontal: 18, 208 + paddingVertical: 12, 209 + borderRadius: 24, 210 + fontSize: 15, 205 211 lineHeight: 20, 206 212 borderWidth: 0, 213 + fontFamily: 'Lexend_400Regular', 207 214 ...(Platform.OS === 'web' && { 208 215 // @ts-ignore - outlineStyle is web-only 209 216 outlineStyle: 'none', 210 217 }), 211 218 }, 212 219 sendButton: { 213 - padding: 8, 220 + width: 40, 221 + height: 40, 222 + borderRadius: 20, 214 223 justifyContent: 'center', 215 224 alignItems: 'center', 225 + backgroundColor: 'rgba(239, 160, 78, 0.1)', 216 226 }, 217 227 }); 218 228
+5 -4
src/screens/ChatScreen.tsx
··· 13 13 import { useMessageStream } from '../hooks/useMessageStream'; 14 14 import { useChatStore } from '../stores/chatStore'; 15 15 16 - import MessageBubble from '../components/MessageBubble'; 16 + import MessageBubbleV2 from '../components/MessageBubble.v2'; 17 17 import MessageInputV2 from '../components/MessageInput.v2'; 18 18 import LiveStatusIndicator from '../components/LiveStatusIndicator'; 19 19 ··· 53 53 54 54 // Render message item 55 55 const renderMessage = ({ item }: { item: any }) => ( 56 - <MessageBubble message={item} theme={theme} /> 56 + <MessageBubbleV2 message={item} theme={theme} /> 57 57 ); 58 58 59 59 return ( ··· 123 123 flex: 1, 124 124 }, 125 125 messagesList: { 126 - paddingHorizontal: 16, 127 126 paddingTop: 16, 127 + paddingBottom: 16, 128 128 }, 129 129 inputContainer: { 130 130 position: 'absolute', ··· 133 133 right: 0, 134 134 paddingHorizontal: 16, 135 135 paddingVertical: 12, 136 + paddingBottom: Platform.OS === 'ios' ? 24 : 12, 136 137 borderTopWidth: 1, 137 - borderTopColor: 'rgba(255, 255, 255, 0.1)', 138 + borderTopColor: 'rgba(255, 255, 255, 0.08)', 138 139 }, 139 140 });