add ios app

This commit is contained in:
penghanyuan
2026-01-31 21:20:50 +01:00
parent a170632270
commit 748f252c2f
63 changed files with 38507 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { AppColors } from '@/constants/theme';
import { Message } from '@/data/mockData';
interface ChatBubbleProps {
message: Message;
}
export function ChatBubble({ message }: ChatBubbleProps) {
const isAI = message.senderType === 'ai';
return (
<View style={[styles.container, isAI ? styles.aiContainer : styles.userContainer]}>
{/* Avatar */}
<View style={[styles.avatar, isAI ? styles.aiAvatar : styles.userAvatar]}>
<Text style={styles.avatarEmoji}>{isAI ? '📖' : '👤'}</Text>
</View>
{/* Bubble */}
<View style={[styles.bubble, isAI ? styles.aiBubble : styles.userBubble]}>
<Text style={[styles.text, isAI ? styles.aiText : styles.userText]}>
{message.content}
</Text>
</View>
</View>
);
}
// Typing indicator component
export function TypingIndicator() {
return (
<View style={[styles.container, styles.aiContainer]}>
<View style={[styles.avatar, styles.aiAvatar]}>
<Text style={styles.avatarEmoji}>📖</Text>
</View>
<View style={[styles.bubble, styles.aiBubble]}>
<View style={styles.typingDots}>
<View style={[styles.dot, styles.dot1]} />
<View style={[styles.dot, styles.dot2]} />
<View style={[styles.dot, styles.dot3]} />
</View>
</View>
</View>
);
}
// Time separator component
export function TimeSeparator({ time }: { time: string }) {
return (
<View style={styles.timeContainer}>
<View style={styles.timeBadge}>
<Text style={styles.timeText}>{time}</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginBottom: 16,
paddingHorizontal: 14,
},
aiContainer: {
flexDirection: 'row',
},
userContainer: {
flexDirection: 'row-reverse',
},
avatar: {
width: 40,
height: 40,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
aiAvatar: {
backgroundColor: AppColors.lavender,
marginRight: 10,
},
userAvatar: {
backgroundColor: AppColors.blushPink,
marginLeft: 10,
},
avatarEmoji: {
fontSize: 18,
},
bubble: {
maxWidth: '70%',
paddingVertical: 12,
paddingHorizontal: 16,
borderRadius: 18,
},
aiBubble: {
backgroundColor: AppColors.white,
borderTopLeftRadius: 4,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 8,
elevation: 2,
},
userBubble: {
backgroundColor: AppColors.mediumPurple,
borderTopRightRadius: 4,
shadowColor: AppColors.mediumPurple,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 3,
},
text: {
fontSize: 15,
lineHeight: 22,
},
aiText: {
color: AppColors.deepPurple,
},
userText: {
color: AppColors.white,
},
// Typing indicator styles
typingDots: {
flexDirection: 'row',
alignItems: 'center',
height: 20,
paddingHorizontal: 4,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: AppColors.slatePurple,
marginHorizontal: 2,
opacity: 0.5,
},
dot1: {
opacity: 1,
},
dot2: {
opacity: 0.7,
},
dot3: {
opacity: 0.4,
},
// Time separator styles
timeContainer: {
alignItems: 'center',
marginVertical: 16,
},
timeBadge: {
paddingVertical: 4,
paddingHorizontal: 12,
backgroundColor: 'rgba(140, 142, 163, 0.1)',
borderRadius: 12,
},
timeText: {
fontSize: 12,
color: AppColors.slatePurple,
},
});

View File

@@ -0,0 +1,180 @@
import React, { useState } from 'react';
import {
View,
TextInput,
TouchableOpacity,
StyleSheet,
Platform,
KeyboardAvoidingView,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { AppColors } from '@/constants/theme';
interface ChatInputProps {
onSendMessage: (text: string) => void;
onVoicePress?: () => void;
onEmojiPress?: () => void;
onMorePress?: () => void;
}
export function ChatInput({
onSendMessage,
onVoicePress,
onEmojiPress,
onMorePress
}: ChatInputProps) {
const [text, setText] = useState('');
const [isVoiceMode, setIsVoiceMode] = useState(false);
const handleSend = () => {
if (text.trim()) {
onSendMessage(text.trim());
setText('');
}
};
const toggleVoiceMode = () => {
setIsVoiceMode(!isVoiceMode);
};
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={0}
>
<View style={styles.container}>
<View style={styles.inputRow}>
{/* Voice/Keyboard Toggle */}
<TouchableOpacity
style={styles.iconButton}
onPress={toggleVoiceMode}
activeOpacity={0.7}
>
<Ionicons
name={isVoiceMode ? 'keypad-outline' : 'mic-outline'}
size={26}
color={AppColors.slatePurple}
/>
</TouchableOpacity>
{/* Text Input or Voice Button */}
{isVoiceMode ? (
<TouchableOpacity
style={styles.voiceButton}
onPress={onVoicePress}
activeOpacity={0.7}
>
<Ionicons name="mic" size={20} color={AppColors.slatePurple} />
</TouchableOpacity>
) : (
<TextInput
style={styles.textInput}
placeholder="说点什么..."
placeholderTextColor={AppColors.slatePurple}
value={text}
onChangeText={setText}
multiline
maxLength={1000}
returnKeyType="send"
onSubmitEditing={handleSend}
blurOnSubmit={false}
/>
)}
{/* Emoji Button */}
<TouchableOpacity
style={styles.iconButton}
onPress={onEmojiPress}
activeOpacity={0.7}
>
<Ionicons
name="happy-outline"
size={26}
color={AppColors.slatePurple}
/>
</TouchableOpacity>
{/* More Options / Send Button */}
{text.trim() ? (
<TouchableOpacity
style={styles.sendButton}
onPress={handleSend}
activeOpacity={0.8}
>
<Ionicons name="send" size={20} color={AppColors.white} />
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.iconButton}
onPress={onMorePress}
activeOpacity={0.7}
>
<Ionicons
name="add-circle-outline"
size={26}
color={AppColors.slatePurple}
/>
</TouchableOpacity>
)}
</View>
</View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: AppColors.white,
borderTopWidth: 1,
borderTopColor: 'rgba(32, 0, 40, 0.08)',
paddingHorizontal: 14,
paddingTop: 10,
paddingBottom: Platform.OS === 'ios' ? 34 : 10,
},
inputRow: {
flexDirection: 'row',
alignItems: 'flex-end',
gap: 8,
},
iconButton: {
width: 40,
height: 40,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
textInput: {
flex: 1,
minHeight: 40,
maxHeight: 100,
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: AppColors.cream,
borderRadius: 20,
fontSize: 16,
color: AppColors.deepPurple,
lineHeight: 20,
},
voiceButton: {
flex: 1,
height: 40,
backgroundColor: AppColors.cream,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
},
sendButton: {
width: 40,
height: 40,
borderRadius: 12,
backgroundColor: AppColors.mediumPurple,
alignItems: 'center',
justifyContent: 'center',
shadowColor: AppColors.mediumPurple,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 3,
},
});

View File

@@ -0,0 +1,131 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
runOnJS,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { AppColors } from '@/constants/theme';
import { Conversation } from '@/data/mockData';
interface ConversationItemProps {
conversation: Conversation;
onPress: () => void;
}
export function ConversationItem({ conversation, onPress }: ConversationItemProps) {
const scale = useSharedValue(1);
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
opacity: opacity.value,
}));
const tapGesture = Gesture.Tap()
.onBegin(() => {
scale.value = withSpring(0.97, { damping: 15, stiffness: 400 });
opacity.value = withTiming(0.8, { duration: 100 });
})
.onFinalize((_, success) => {
scale.value = withSpring(1, { damping: 15, stiffness: 400 });
opacity.value = withTiming(1, { duration: 150 });
if (success) {
runOnJS(onPress)();
}
});
return (
<GestureDetector gesture={tapGesture}>
<Animated.View style={[styles.container, animatedStyle]}>
<View style={styles.avatar}>
<Text style={styles.avatarEmoji}>{conversation.avatarEmoji}</Text>
</View>
<View style={styles.content}>
<Text style={styles.title} numberOfLines={1}>
{conversation.title}
</Text>
<Text style={styles.preview} numberOfLines={1}>
{conversation.lastMessage}
</Text>
</View>
<View style={styles.meta}>
<Text style={styles.timestamp}>{conversation.timestamp}</Text>
{conversation.unreadCount && conversation.unreadCount > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{conversation.unreadCount}</Text>
</View>
)}
</View>
</Animated.View>
</GestureDetector>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 14,
paddingHorizontal: 20,
backgroundColor: AppColors.cream,
},
avatar: {
width: 56,
height: 56,
borderRadius: 14,
backgroundColor: AppColors.lavender,
alignItems: 'center',
justifyContent: 'center',
marginRight: 14,
shadowColor: AppColors.deepPurple,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 2,
},
avatarEmoji: {
fontSize: 26,
},
content: {
flex: 1,
marginRight: 12,
},
title: {
fontSize: 17,
fontWeight: '500',
color: AppColors.deepPurple,
marginBottom: 4,
},
preview: {
fontSize: 14,
color: AppColors.slatePurple,
lineHeight: 20,
},
meta: {
alignItems: 'flex-end',
},
timestamp: {
fontSize: 12,
color: AppColors.slatePurple,
marginBottom: 6,
},
badge: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: AppColors.mediumPurple,
alignItems: 'center',
justifyContent: 'center',
},
badgeText: {
fontSize: 12,
color: AppColors.white,
fontWeight: '500',
},
});