- Updated app.json to set userInterfaceStyle to automatic for improved theme adaptability. - Refactored layout components to utilize useAppColors for dynamic theming, ensuring consistent color usage. - Introduced SystemChrome component to manage system UI background color based on theme. - Enhanced TabsLayout, ProfileStackLayout, and other components to leverage new theming structure. - Improved QRCodeScanner, SearchBar, and CommentItem styles to align with the updated theme system. - Consolidated styles in SystemMessageItem and TabBar for better maintainability and visual coherence.
248 lines
8.3 KiB
TypeScript
248 lines
8.3 KiB
TypeScript
/**
|
|
* 聊天输入框组件
|
|
* 支持响应式布局(宽屏下居中显示)
|
|
*/
|
|
|
|
import React, { useMemo } from 'react';
|
|
import { View, TouchableOpacity, TextInput, ActivityIndicator, ScrollView, StyleSheet, Image as RNImage } from 'react-native';
|
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
import { Text } from '../../../../components/common';
|
|
import { useAppColors } from '../../../../theme';
|
|
import { useChatScreenStyles } from './styles';
|
|
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
|
import { ChatInputProps, GroupMessage, SenderInfo } from './types';
|
|
import { extractTextFromSegments } from '../../../../types/dto';
|
|
|
|
export const ChatInput: React.FC<ChatInputProps & {
|
|
currentUser: { id?: string; nickname?: string; avatar?: string | null } | null;
|
|
otherUser: { id?: string; nickname?: string; avatar?: string | null } | null;
|
|
getSenderInfo: (senderId: string) => SenderInfo;
|
|
}> = ({
|
|
inputText,
|
|
onInputChange,
|
|
onSend,
|
|
onToggleEmoji,
|
|
onToggleMore,
|
|
activePanel,
|
|
isComposerBusy,
|
|
isMuted,
|
|
isGroupChat,
|
|
muteAll,
|
|
replyingTo,
|
|
onCancelReply,
|
|
onFocus,
|
|
restrictionHint,
|
|
disableMore = false,
|
|
pendingAttachments = [],
|
|
onRemovePendingAttachment,
|
|
currentUser,
|
|
otherUser,
|
|
getSenderInfo,
|
|
}) => {
|
|
const colors = useAppColors();
|
|
const styles = useChatScreenStyles();
|
|
const isDisabled = isGroupChat && isMuted;
|
|
|
|
// 获取当前用户ID
|
|
const currentUserId = currentUser?.id || '';
|
|
|
|
// 响应式布局
|
|
const isWideScreen = useBreakpointGTE('lg');
|
|
|
|
const inputContainerStyle = useMemo(() => ([
|
|
styles.inputContainer,
|
|
isWideScreen ? { maxWidth: 900, alignSelf: 'center' as const, width: '100%' as const } : null,
|
|
]), [isWideScreen, styles.inputContainer]);
|
|
|
|
const canSend =
|
|
(!!inputText.trim() || pendingAttachments.length > 0) &&
|
|
!isComposerBusy &&
|
|
!isDisabled;
|
|
|
|
// 回复预览组件 - 定义在内部以访问 styles
|
|
const ReplyPreview: React.FC<{
|
|
replyingTo: GroupMessage;
|
|
onCancel: () => void;
|
|
}> = ({ replyingTo, onCancel }) => {
|
|
const senderInfo = replyingTo.sender_id === currentUserId
|
|
? { nickname: '我', avatar: currentUser?.avatar }
|
|
: isGroupChat
|
|
? getSenderInfo(replyingTo.sender_id)
|
|
: { nickname: otherUser?.nickname || '对方', avatar: otherUser?.avatar };
|
|
|
|
return (
|
|
<View style={styles.replyPreviewContainer}>
|
|
<View style={styles.replyPreviewContent}>
|
|
<MaterialCommunityIcons name="reply" size={16} color={colors.primary.main} />
|
|
<View style={styles.replyPreviewText}>
|
|
<Text style={styles.replyPreviewName}>回复 {senderInfo.nickname}</Text>
|
|
<Text style={styles.replyPreviewMessage} numberOfLines={1}>
|
|
{(() => {
|
|
const segs = replyingTo.segments;
|
|
const imgCount = segs?.filter(seg => seg.type === 'image').length ?? 0;
|
|
const raw = extractTextFromSegments(segs);
|
|
if (imgCount > 1) {
|
|
const textPart = raw.replace(/\[图片\]/g, '').replace(/\s+/g, ' ').trim();
|
|
return textPart ? `${textPart} [${imgCount}张图片]` : `[${imgCount}张图片]`;
|
|
}
|
|
return raw;
|
|
})()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<TouchableOpacity onPress={onCancel} style={styles.replyPreviewClose}>
|
|
<MaterialCommunityIcons name="close" size={18} color="#999" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<View style={inputContainerStyle}>
|
|
{/* 回复预览 */}
|
|
{replyingTo && (
|
|
<ReplyPreview
|
|
replyingTo={replyingTo}
|
|
onCancel={onCancelReply}
|
|
/>
|
|
)}
|
|
|
|
{/* 禁言提示 */}
|
|
{isGroupChat && isMuted && (
|
|
<View style={styles.mutedBanner}>
|
|
<MaterialCommunityIcons name="minus-circle" size={16} color="#FF3B30" />
|
|
<Text style={styles.mutedBannerText}>
|
|
{muteAll ? '全员禁言中,暂无法发送消息' : '你已被禁言,暂无法发送消息'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{!!restrictionHint && (
|
|
<View style={styles.mutedBanner}>
|
|
<MaterialCommunityIcons name="information-outline" size={16} color={colors.warning.main} />
|
|
<Text style={styles.mutedBannerText}>{restrictionHint}</Text>
|
|
</View>
|
|
)}
|
|
|
|
{pendingAttachments.length > 0 && (
|
|
<ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
style={attachmentStyles.stripScroll}
|
|
contentContainerStyle={attachmentStyles.stripContent}
|
|
>
|
|
{pendingAttachments.map(item => (
|
|
<View key={item.id} style={attachmentStyles.thumbWrap}>
|
|
<RNImage source={{ uri: item.uri }} style={attachmentStyles.thumb} />
|
|
{!isDisabled && onRemovePendingAttachment ? (
|
|
<TouchableOpacity
|
|
style={attachmentStyles.removeBtn}
|
|
onPress={() => onRemovePendingAttachment(item.id)}
|
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
>
|
|
<MaterialCommunityIcons name="close-circle" size={22} color="rgba(0,0,0,0.55)" />
|
|
</TouchableOpacity>
|
|
) : null}
|
|
</View>
|
|
))}
|
|
</ScrollView>
|
|
)}
|
|
|
|
<View style={[styles.inputInner, isDisabled && styles.inputInnerMuted]}>
|
|
<TouchableOpacity
|
|
style={[styles.iconButton, activePanel === 'emoji' && styles.iconButtonActive]}
|
|
onPress={onToggleEmoji}
|
|
disabled={isDisabled}
|
|
>
|
|
<MaterialCommunityIcons
|
|
name={activePanel === 'emoji' ? "keyboard" : "emoticon-happy-outline"}
|
|
size={26}
|
|
color={isDisabled ? '#CCC' : (activePanel === 'emoji' ? colors.primary.main : '#666')}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
<View style={styles.inputBox}>
|
|
<TextInput
|
|
style={[styles.input, isDisabled && styles.inputMuted]}
|
|
placeholder={isGroupChat ? (isMuted ? (muteAll ? "全员禁言中..." : "你已被禁言...") : "发送消息,@提及成员...") : "发送消息..."}
|
|
placeholderTextColor={isDisabled ? '#CCC' : '#999'}
|
|
value={inputText}
|
|
onChangeText={onInputChange}
|
|
multiline
|
|
maxLength={500}
|
|
returnKeyType="send"
|
|
onSubmitEditing={onSend}
|
|
blurOnSubmit={false}
|
|
editable={!isDisabled}
|
|
onFocus={onFocus}
|
|
// 确保光标可见
|
|
cursorColor={colors.primary.main}
|
|
selectionColor={`${colors.primary.main}40`}
|
|
/>
|
|
</View>
|
|
|
|
{canSend ? (
|
|
<TouchableOpacity
|
|
style={styles.sendButtonActive}
|
|
onPress={onSend}
|
|
activeOpacity={0.8}
|
|
>
|
|
<MaterialCommunityIcons name="send" size={20} color="#FFF" />
|
|
</TouchableOpacity>
|
|
) : isComposerBusy && !isDisabled ? (
|
|
<TouchableOpacity
|
|
style={[styles.sendButtonActive, styles.sendButtonDisabled]}
|
|
disabled
|
|
>
|
|
<ActivityIndicator size="small" color="#FFF" />
|
|
</TouchableOpacity>
|
|
) : (
|
|
<TouchableOpacity
|
|
style={[styles.iconButton, activePanel === 'more' && styles.iconButtonActive]}
|
|
onPress={onToggleMore}
|
|
disabled={isDisabled || disableMore}
|
|
>
|
|
<MaterialCommunityIcons
|
|
name={activePanel === 'more' ? "close-circle" : "plus-circle-outline"}
|
|
size={26}
|
|
color={isDisabled || disableMore ? '#CCC' : (activePanel === 'more' ? colors.primary.main : '#666')}
|
|
/>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const attachmentStyles = StyleSheet.create({
|
|
stripScroll: {
|
|
maxHeight: 88,
|
|
marginBottom: 6,
|
|
paddingHorizontal: 4,
|
|
},
|
|
stripContent: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
paddingVertical: 4,
|
|
},
|
|
thumbWrap: {
|
|
position: 'relative',
|
|
marginRight: 8,
|
|
},
|
|
thumb: {
|
|
width: 72,
|
|
height: 72,
|
|
borderRadius: 10,
|
|
backgroundColor: '#E8EAED',
|
|
},
|
|
removeBtn: {
|
|
position: 'absolute',
|
|
top: -6,
|
|
right: -6,
|
|
backgroundColor: '#FFF',
|
|
borderRadius: 12,
|
|
},
|
|
});
|
|
|
|
export default ChatInput;
|