add ios app
This commit is contained in:
162
app-ios/components/chat/ChatBubble.tsx
Normal file
162
app-ios/components/chat/ChatBubble.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
180
app-ios/components/chat/ChatInput.tsx
Normal file
180
app-ios/components/chat/ChatInput.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
131
app-ios/components/chat/ConversationItem.tsx
Normal file
131
app-ios/components/chat/ConversationItem.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user