Files
frontend/src/screens/message/components/ChatScreen/ChatInput.tsx

247 lines
8.2 KiB
TypeScript
Raw Normal View History

/**
*
*
*/
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 { colors } from '../../../../theme';
import { chatScreenStyles as baseStyles } 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 isDisabled = isGroupChat && isMuted;
// 获取当前用户ID
const currentUserId = currentUser?.id || '';
// 响应式布局
const isWideScreen = useBreakpointGTE('lg');
const styles = baseStyles;
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}
2026-03-16 17:47:10 +08:00
// 确保光标可见
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;