174 lines
6.0 KiB
TypeScript
174 lines
6.0 KiB
TypeScript
|
|
/**
|
|||
|
|
* 聊天输入框组件
|
|||
|
|
* 支持响应式布局(宽屏下居中显示)
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import React, { useMemo } from 'react';
|
|||
|
|
import { View, TouchableOpacity, TextInput, ActivityIndicator } 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,
|
|||
|
|
sending,
|
|||
|
|
isMuted,
|
|||
|
|
isGroupChat,
|
|||
|
|
muteAll,
|
|||
|
|
replyingTo,
|
|||
|
|
onCancelReply,
|
|||
|
|
onFocus,
|
|||
|
|
restrictionHint,
|
|||
|
|
disableMore = false,
|
|||
|
|
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]);
|
|||
|
|
|
|||
|
|
// 回复预览组件 - 定义在内部以访问 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}>
|
|||
|
|
{replyingTo.segments?.some(seg => seg.type === 'image') ? '[图片]' : extractTextFromSegments(replyingTo.segments)}
|
|||
|
|
</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>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<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}
|
|||
|
|
/>
|
|||
|
|
</View>
|
|||
|
|
|
|||
|
|
{inputText.trim() && !sending && !isDisabled ? (
|
|||
|
|
<TouchableOpacity
|
|||
|
|
style={styles.sendButtonActive}
|
|||
|
|
onPress={onSend}
|
|||
|
|
activeOpacity={0.8}
|
|||
|
|
>
|
|||
|
|
<MaterialCommunityIcons name="send" size={20} color="#FFF" />
|
|||
|
|
</TouchableOpacity>
|
|||
|
|
) : sending ? (
|
|||
|
|
<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>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default ChatInput;
|