Improve back navigation UX by checking for open panels or keyboard before leaving the chat screen. When back is triggered, dismiss keyboard or close panel first instead of immediately navigating away. Apply this logic to both the navigation gesture listener and Android hardware back button for consistent behavior.
700 lines
22 KiB
TypeScript
700 lines
22 KiB
TypeScript
/**
|
||
* 聊天页 ChatScreen
|
||
* 威友 - 私信/群聊聊天界面
|
||
* 高级现代化设计
|
||
* 支持群聊功能:显示发送者头像和昵称、@提及功能
|
||
* 支持响应式布局(桌面端宽屏优化)
|
||
*
|
||
* 重构说明:将原2264行的大文件拆分为多个模块化组件
|
||
* - types.ts: 类型定义
|
||
* - constants.ts: 常量定义
|
||
* - styles.ts: 样式定义
|
||
* - useChatScreen.ts: 自定义Hook,管理所有状态和逻辑
|
||
* - EmojiPanel.tsx: 表情面板组件
|
||
* - MorePanel.tsx: 更多功能面板组件
|
||
* - MentionPanel.tsx: @成员选择 面板组件
|
||
* - LongPressMenu.tsx: 长按菜单组件
|
||
* - ChatHeader.tsx: 聊天头部组件
|
||
* - MessageBubble.tsx: 消息气泡组件
|
||
* - ChatInput.tsx: 输入框组件
|
||
*/
|
||
|
||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||
import {
|
||
View,
|
||
ActivityIndicator,
|
||
KeyboardAvoidingView,
|
||
Platform,
|
||
TouchableOpacity,
|
||
BackHandler,
|
||
} from 'react-native';
|
||
import { FlashList, ListRenderItem } from '@shopify/flash-list';
|
||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||
import { useNavigation, useRouter } from 'expo-router';
|
||
import { StatusBar } from 'expo-status-bar';
|
||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
|
||
import { useAppColors, useResolvedColorScheme } from '../../theme';
|
||
import * as hrefs from '../../navigation/hrefs';
|
||
import { messageManager, callStore } from '../../stores';
|
||
import { useBreakpointGTE } from '../../hooks/useResponsive';
|
||
import {
|
||
useChatScreen,
|
||
useChatScreenStyles,
|
||
EmojiPanel,
|
||
MorePanel,
|
||
MentionPanel,
|
||
LongPressMenu,
|
||
ChatHeader,
|
||
MessageBubble,
|
||
ChatInput,
|
||
GroupInfoPanel,
|
||
PANEL_HEIGHTS,
|
||
ChatScreenProps,
|
||
} from './components/ChatScreen';
|
||
import { GroupMessage } from './components/ChatScreen/types';
|
||
|
||
export const ChatScreen: React.FC<ChatScreenProps> = (props) => {
|
||
const navigation = useNavigation();
|
||
const router = useRouter();
|
||
const insets = useSafeAreaInsets();
|
||
const colors = useAppColors();
|
||
const resolved = useResolvedColorScheme();
|
||
const styles = useChatScreenStyles();
|
||
|
||
// 响应式布局
|
||
const isWideScreen = useBreakpointGTE('lg');
|
||
|
||
// 监听屏幕宽度变化,当变为大屏幕时自动跳转到web端首页(嵌入式模式下不跳转)
|
||
useEffect(() => {
|
||
if (isWideScreen && !props.isEmbedded) {
|
||
router.replace(hrefs.hrefHome());
|
||
}
|
||
}, [isWideScreen, router, props.isEmbedded]);
|
||
|
||
// 输入框区域高度(用于定位浮动 mention 面板)
|
||
const [inputWrapperHeight, setInputWrapperHeight] = useState(60);
|
||
const [showEdgeLoadingIndicator, setShowEdgeLoadingIndicator] = useState(false);
|
||
const replyTargetMessageIdRef = useRef<string | null>(null);
|
||
const replyHighlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
const isPreloadingRef = useRef(false);
|
||
const lastScrollYRef = useRef(0);
|
||
const isUserDraggingRef = useRef(false);
|
||
const historyAnchorRef = useRef<{ active: boolean; beforeY: number; beforeHeight: number }>({
|
||
active: false,
|
||
beforeY: 0,
|
||
beforeHeight: 0,
|
||
});
|
||
const preloadCooldownTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
const showJumpToLatestRef = useRef(false);
|
||
const [showJumpToLatest, setShowJumpToLatest] = useState(false);
|
||
|
||
const containerStyle = useMemo(() => ([
|
||
styles.container,
|
||
isWideScreen && !props.isEmbedded ? { maxWidth: 1200, alignSelf: 'center' as const, width: '100%' as const } : null,
|
||
]), [isWideScreen, styles.container, props.isEmbedded]);
|
||
|
||
const listContentStyle = useMemo(() => ({
|
||
paddingHorizontal: isWideScreen && !props.isEmbedded ? 24 : 16,
|
||
...(isWideScreen && !props.isEmbedded ? { maxWidth: 900, alignSelf: 'center' as const } : {}),
|
||
paddingTop: 12,
|
||
paddingBottom: 24,
|
||
}), [isWideScreen, props.isEmbedded]);
|
||
|
||
const inputWrapperStyle = useMemo(() => ([
|
||
styles.inputWrapper,
|
||
isWideScreen && !props.isEmbedded ? { maxWidth: 900, alignSelf: 'center' as const, width: '100%' as const } : null,
|
||
]), [isWideScreen, styles.inputWrapper, props.isEmbedded]);
|
||
|
||
// 图片查看器状态
|
||
const [showImageViewer, setShowImageViewer] = useState(false);
|
||
const [chatImages, setChatImages] = useState<ImageGridItem[]>([]);
|
||
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
|
||
|
||
// 图片点击处理函数
|
||
const handleImagePress = (images: ImageGridItem[], index: number) => {
|
||
setChatImages(images);
|
||
setSelectedImageIndex(index);
|
||
setShowImageViewer(true);
|
||
};
|
||
|
||
// 关闭图片查看器
|
||
const handleCloseImageViewer = () => {
|
||
setShowImageViewer(false);
|
||
};
|
||
|
||
// 群信息面板状态
|
||
const [showGroupInfoPanel, setShowGroupInfoPanel] = useState(false);
|
||
|
||
// 打开群信息面板
|
||
const handleGroupInfoPress = useCallback(() => {
|
||
setShowGroupInfoPanel(true);
|
||
}, []);
|
||
|
||
// 关闭群信息面板
|
||
const handleCloseGroupInfoPanel = useCallback(() => {
|
||
setShowGroupInfoPanel(false);
|
||
}, []);
|
||
|
||
const {
|
||
// 状态
|
||
messages,
|
||
inputText,
|
||
otherUser,
|
||
currentUser,
|
||
currentUserId,
|
||
keyboardHeight,
|
||
loading,
|
||
isComposerBusy,
|
||
activePanel,
|
||
sendingImage,
|
||
uploadingAttachments,
|
||
pendingAttachments,
|
||
replyingTo,
|
||
longPressMenuVisible,
|
||
selectedMessage,
|
||
selectedMessageId,
|
||
setSelectedMessageId,
|
||
menuPosition,
|
||
isGroupChat,
|
||
groupInfo,
|
||
groupMembers,
|
||
currentUserRole,
|
||
mentionQuery,
|
||
isMuted,
|
||
muteAll,
|
||
followRestrictionHint,
|
||
canSendPrivateImage,
|
||
routeGroupId,
|
||
routeGroupName,
|
||
otherUserLastReadSeq,
|
||
messageMap,
|
||
loadingMore,
|
||
hasMoreHistory,
|
||
conversationId,
|
||
|
||
// Refs
|
||
flatListRef,
|
||
textInputRef,
|
||
scrollPositionRef,
|
||
|
||
// 方法
|
||
formatTime,
|
||
shouldShowTime,
|
||
handleInputChange,
|
||
handleSend,
|
||
removePendingAttachment,
|
||
handleMoreAction,
|
||
handleInsertEmoji,
|
||
handleSendSticker,
|
||
toggleEmojiPanel,
|
||
toggleMorePanel,
|
||
closePanel,
|
||
handleRecall,
|
||
handleLongPressMessage,
|
||
hideLongPressMenu,
|
||
handleDeleteMessage,
|
||
handleReplyMessage,
|
||
handleCancelReply,
|
||
handleAvatarPress,
|
||
handleAvatarLongPress,
|
||
handleSelectMention,
|
||
handleMentionAll,
|
||
getSenderInfo,
|
||
getTypingHint,
|
||
getInputBottom,
|
||
handleDismiss,
|
||
navigateToInfo,
|
||
navigateToChatSettings,
|
||
loadMoreHistory,
|
||
handleMessageListContentSizeChange,
|
||
handleReachLatestEdge,
|
||
jumpToLatestMessages,
|
||
setBrowsingHistory,
|
||
} = useChatScreen(props);
|
||
const displayMessages = useMemo(() => [...messages].reverse(), [messages]);
|
||
|
||
const longPressMenuMemberMap = useMemo(() => {
|
||
const map = new Map<string, { nickname: string; user_id: string }>();
|
||
groupMembers.forEach(m => {
|
||
map.set(m.user_id, {
|
||
nickname: m.nickname || m.user?.nickname || '用户',
|
||
user_id: m.user_id,
|
||
});
|
||
});
|
||
return map;
|
||
}, [groupMembers]);
|
||
|
||
useEffect(() => {
|
||
if (!loadingMore && showEdgeLoadingIndicator) {
|
||
setShowEdgeLoadingIndicator(false);
|
||
}
|
||
}, [loadingMore, showEdgeLoadingIndicator]);
|
||
|
||
useEffect(() => {
|
||
if (loading) {
|
||
showJumpToLatestRef.current = false;
|
||
setShowJumpToLatest(false);
|
||
}
|
||
}, [loading]);
|
||
|
||
useEffect(() => {
|
||
showJumpToLatestRef.current = false;
|
||
setShowJumpToLatest(false);
|
||
}, [conversationId]);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (preloadCooldownTimerRef.current) {
|
||
clearTimeout(preloadCooldownTimerRef.current);
|
||
}
|
||
if (replyHighlightTimerRef.current) {
|
||
clearTimeout(replyHighlightTimerRef.current);
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
const handleReplyPreviewPress = useCallback((messageId: string) => {
|
||
const targetId = String(messageId);
|
||
const targetIndex = displayMessages.findIndex(msg => String(msg.id) === targetId);
|
||
if (targetIndex < 0) return;
|
||
|
||
replyTargetMessageIdRef.current = targetId;
|
||
setBrowsingHistory(true);
|
||
|
||
flatListRef.current?.scrollToIndex({
|
||
index: targetIndex,
|
||
animated: true,
|
||
viewPosition: 0.5,
|
||
});
|
||
}, [displayMessages, flatListRef, setBrowsingHistory]);
|
||
|
||
// 监听返回事件:面板/键盘打开时先关闭它们,否则刷新会话列表后返回
|
||
useEffect(() => {
|
||
const unsubscribe = navigation.addListener('beforeRemove', (e) => {
|
||
const hasPanelOpen = activePanel !== 'none';
|
||
const hasKeyboardOpen = keyboardHeight > 0;
|
||
if (hasPanelOpen || hasKeyboardOpen) {
|
||
e.preventDefault();
|
||
if (hasKeyboardOpen) {
|
||
handleDismiss();
|
||
} else {
|
||
closePanel();
|
||
}
|
||
return;
|
||
}
|
||
// 真正返回时刷新会话列表
|
||
messageManager.refreshConversations(true, 'chat-before-remove');
|
||
});
|
||
return unsubscribe;
|
||
}, [navigation, activePanel, keyboardHeight, closePanel, handleDismiss]);
|
||
|
||
// Android 物理返回键:面板/键盘打开时先关闭它们
|
||
useEffect(() => {
|
||
const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
|
||
const hasPanelOpen = activePanel !== 'none';
|
||
const hasKeyboardOpen = keyboardHeight > 0;
|
||
if (hasPanelOpen || hasKeyboardOpen) {
|
||
if (hasKeyboardOpen) {
|
||
handleDismiss();
|
||
} else {
|
||
closePanel();
|
||
}
|
||
return true; // 阻止默认返回行为
|
||
}
|
||
return false;
|
||
});
|
||
|
||
return () => backHandler.remove();
|
||
}, [activePanel, keyboardHeight, closePanel, handleDismiss]);
|
||
|
||
// 渲染消息气泡
|
||
const messageIndexMap = useMemo(() => {
|
||
const map = new Map<string, number>();
|
||
messages.forEach((msg, idx) => {
|
||
map.set(String(msg.id), idx);
|
||
});
|
||
return map;
|
||
}, [messages]);
|
||
|
||
const renderMessage = useCallback<ListRenderItem<GroupMessage>>(({ item, index }) => {
|
||
// inverted 下 renderItem 的 index 与时间顺序不一致,需回查原始序索引
|
||
const logicalIndex = messageIndexMap.get(String(item.id)) ?? index;
|
||
return (
|
||
<MessageBubble
|
||
message={item}
|
||
index={logicalIndex}
|
||
currentUserId={currentUserId}
|
||
currentUser={currentUser}
|
||
otherUser={otherUser}
|
||
isGroupChat={isGroupChat}
|
||
groupMembers={groupMembers}
|
||
otherUserLastReadSeq={otherUserLastReadSeq}
|
||
selectedMessageId={selectedMessageId}
|
||
messageMap={messageMap}
|
||
onLongPress={handleLongPressMessage}
|
||
onAvatarPress={handleAvatarPress}
|
||
onAvatarLongPress={handleAvatarLongPress}
|
||
formatTime={formatTime}
|
||
shouldShowTime={shouldShowTime}
|
||
onImagePress={handleImagePress}
|
||
onReply={handleReplyMessage}
|
||
onReplyPress={handleReplyPreviewPress}
|
||
/>
|
||
);
|
||
}, [
|
||
messageIndexMap,
|
||
currentUserId,
|
||
currentUser,
|
||
otherUser,
|
||
isGroupChat,
|
||
groupMembers,
|
||
otherUserLastReadSeq,
|
||
selectedMessageId,
|
||
messageMap,
|
||
handleLongPressMessage,
|
||
handleAvatarPress,
|
||
handleAvatarLongPress,
|
||
formatTime,
|
||
shouldShowTime,
|
||
handleImagePress,
|
||
handleReplyMessage,
|
||
handleReplyPreviewPress,
|
||
]);
|
||
|
||
const keyExtractor = useCallback((item: GroupMessage) => String(item.id), []);
|
||
|
||
// 获取正在输入提示
|
||
const typingHint = getTypingHint();
|
||
const handleMessageListScroll = useCallback((event: any) => {
|
||
const { contentSize, contentOffset, layoutMeasurement } = event.nativeEvent;
|
||
scrollPositionRef.current = {
|
||
contentHeight: contentSize.height,
|
||
scrollY: contentOffset.y,
|
||
viewportHeight: layoutMeasurement.height,
|
||
};
|
||
|
||
// inverted:offset 越大离最新消息端越远,与 setBrowsingHistory(120) 阈值一致
|
||
const shouldShowJump =
|
||
contentOffset.y > 120 && !loading && messages.length > 0;
|
||
if (showJumpToLatestRef.current !== shouldShowJump) {
|
||
showJumpToLatestRef.current = shouldShowJump;
|
||
setShowJumpToLatest(shouldShowJump);
|
||
}
|
||
|
||
// 离开最新消息端后进入“浏览历史”模式,屏蔽自动跟随到底
|
||
if (contentOffset.y > 120) {
|
||
setBrowsingHistory(true);
|
||
}
|
||
|
||
// 仅用户手势主动回到底部时才解除历史阅读锁(避免加载阶段误解锁)
|
||
const isScrollingTowardLatest = contentOffset.y < lastScrollYRef.current;
|
||
if (
|
||
isUserDraggingRef.current &&
|
||
!loadingMore &&
|
||
isScrollingTowardLatest &&
|
||
contentOffset.y <= 40
|
||
) {
|
||
handleReachLatestEdge();
|
||
}
|
||
|
||
// inverted 下历史端在“列表尾部”,对应 offset 增大且距离尾部变小
|
||
const distanceToHistoryEdge = contentSize.height - (contentOffset.y + layoutMeasurement.height);
|
||
lastScrollYRef.current = contentOffset.y;
|
||
|
||
// 预加载阈值:按屏幕高度动态提前(至少 420),确保边界前开始加载
|
||
const PRELOAD_HISTORY_THRESHOLD = Math.max(420, layoutMeasurement.height * 1.2);
|
||
// 仅在真正接近边界时才显示旋转提示
|
||
const EDGE_LOADING_INDICATOR_THRESHOLD = 60;
|
||
const shouldShowEdgeIndicator = loadingMore && distanceToHistoryEdge <= EDGE_LOADING_INDICATOR_THRESHOLD;
|
||
if (shouldShowEdgeIndicator !== showEdgeLoadingIndicator) {
|
||
setShowEdgeLoadingIndicator(shouldShowEdgeIndicator);
|
||
}
|
||
|
||
if (
|
||
distanceToHistoryEdge <= PRELOAD_HISTORY_THRESHOLD &&
|
||
hasMoreHistory &&
|
||
!loadingMore &&
|
||
!loading &&
|
||
!isPreloadingRef.current
|
||
) {
|
||
isPreloadingRef.current = true;
|
||
loadMoreHistory().finally(() => {
|
||
if (preloadCooldownTimerRef.current) {
|
||
clearTimeout(preloadCooldownTimerRef.current);
|
||
}
|
||
preloadCooldownTimerRef.current = setTimeout(() => {
|
||
isPreloadingRef.current = false;
|
||
}, 350);
|
||
});
|
||
}
|
||
}, [
|
||
scrollPositionRef,
|
||
hasMoreHistory,
|
||
loadingMore,
|
||
loading,
|
||
messages.length,
|
||
loadMoreHistory,
|
||
handleReachLatestEdge,
|
||
showEdgeLoadingIndicator,
|
||
setBrowsingHistory,
|
||
]);
|
||
|
||
const handleContentSizeChange = useCallback((contentWidth: number, contentHeight: number) => {
|
||
handleMessageListContentSizeChange(contentWidth, contentHeight);
|
||
}, [handleMessageListContentSizeChange]);
|
||
|
||
return (
|
||
<SafeAreaView style={containerStyle} edges={props.isEmbedded ? [] : ['top']}>
|
||
<KeyboardAvoidingView
|
||
style={[styles.container, Platform.OS !== 'web' && { overflow: 'hidden' }]}
|
||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
||
>
|
||
{!props.isEmbedded && <StatusBar style={resolved === 'dark' ? 'light' : 'dark'} backgroundColor={colors.background.paper} />}
|
||
|
||
{/* 顶部栏 */}
|
||
<ChatHeader
|
||
isGroupChat={isGroupChat}
|
||
groupInfo={groupInfo}
|
||
otherUser={otherUser}
|
||
routeGroupName={routeGroupName ?? undefined}
|
||
typingHint={typingHint}
|
||
onBack={props.onEmbeddedBack ? props.onEmbeddedBack : () => router.back()}
|
||
onTitlePress={navigateToInfo}
|
||
onMorePress={navigateToChatSettings}
|
||
onGroupInfoPress={handleGroupInfoPress}
|
||
isWideScreen={isWideScreen}
|
||
/>
|
||
|
||
{/* 消息列表 */}
|
||
<View
|
||
style={styles.messageListContainer}
|
||
onLayout={e => {
|
||
scrollPositionRef.current.viewportHeight = e.nativeEvent.layout.height;
|
||
}}
|
||
onTouchEnd={handleDismiss}
|
||
>
|
||
{loading ? (
|
||
<View style={styles.loadingContainer}>
|
||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||
</View>
|
||
) : (
|
||
<FlashList
|
||
ref={flatListRef}
|
||
inverted
|
||
data={displayMessages}
|
||
renderItem={renderMessage}
|
||
keyExtractor={keyExtractor}
|
||
contentContainerStyle={listContentStyle}
|
||
showsVerticalScrollIndicator={false}
|
||
keyboardShouldPersistTaps="handled"
|
||
keyboardDismissMode="on-drag"
|
||
scrollEnabled={true}
|
||
drawDistance={250}
|
||
onScroll={handleMessageListScroll}
|
||
onScrollBeginDrag={() => {
|
||
isUserDraggingRef.current = true;
|
||
handleDismiss();
|
||
}}
|
||
onScrollEndDrag={() => {
|
||
isUserDraggingRef.current = false;
|
||
}}
|
||
onMomentumScrollEnd={() => {
|
||
isUserDraggingRef.current = false;
|
||
}}
|
||
scrollEventThrottle={16}
|
||
onContentSizeChange={handleContentSizeChange}
|
||
/>
|
||
)}
|
||
{showEdgeLoadingIndicator && (
|
||
<View
|
||
pointerEvents="none"
|
||
style={{
|
||
position: 'absolute',
|
||
top: 8,
|
||
left: 0,
|
||
right: 0,
|
||
alignItems: 'center',
|
||
}}
|
||
>
|
||
<View
|
||
style={{
|
||
backgroundColor: colors.background.paper + 'E6',
|
||
borderRadius: 12,
|
||
paddingHorizontal: 10,
|
||
paddingVertical: 6,
|
||
}}
|
||
>
|
||
<ActivityIndicator size="small" color={colors.primary.main} />
|
||
</View>
|
||
</View>
|
||
)}
|
||
{showJumpToLatest && (
|
||
<TouchableOpacity
|
||
style={styles.jumpToLatestFab}
|
||
onPress={jumpToLatestMessages}
|
||
activeOpacity={0.85}
|
||
accessibilityRole="button"
|
||
accessibilityLabel="回到最新消息"
|
||
>
|
||
<MaterialCommunityIcons name="chevron-down" size={20} color={colors.primary.main} />
|
||
<Text style={styles.jumpToLatestFabText}>最新消息</Text>
|
||
</TouchableOpacity>
|
||
)}
|
||
</View>
|
||
|
||
{/* 底部区域:输入框 + 面板 */}
|
||
<View style={{ marginBottom: keyboardHeight > 0 ? keyboardHeight : 0 }}>
|
||
{/* 输入框区域 */}
|
||
<View
|
||
style={[inputWrapperStyle, insets.bottom > 0 && { paddingBottom: insets.bottom }]}
|
||
onLayout={e => setInputWrapperHeight(e.nativeEvent.layout.height)}
|
||
>
|
||
<ChatInput
|
||
inputText={inputText}
|
||
onInputChange={handleInputChange}
|
||
onSend={handleSend}
|
||
onToggleEmoji={toggleEmojiPanel}
|
||
onToggleMore={toggleMorePanel}
|
||
activePanel={activePanel}
|
||
isComposerBusy={isComposerBusy}
|
||
isMuted={isMuted}
|
||
isGroupChat={isGroupChat}
|
||
muteAll={muteAll}
|
||
restrictionHint={followRestrictionHint}
|
||
replyingTo={replyingTo}
|
||
pendingAttachments={pendingAttachments}
|
||
onRemovePendingAttachment={removePendingAttachment}
|
||
onCancelReply={handleCancelReply}
|
||
onFocus={() => {
|
||
// 输入框获得焦点时,关闭其他面板(但不要关闭键盘)
|
||
if (activePanel !== 'none' && activePanel !== 'mention') {
|
||
closePanel();
|
||
}
|
||
}}
|
||
currentUser={currentUser}
|
||
otherUser={otherUser}
|
||
getSenderInfo={getSenderInfo}
|
||
/>
|
||
</View>
|
||
|
||
{/* 表情面板 */}
|
||
{activePanel === 'emoji' && keyboardHeight === 0 && (
|
||
<View style={[styles.panelWrapper, styles.emojiPanelWrapper, { height: PANEL_HEIGHTS.emoji + insets.bottom, paddingBottom: insets.bottom }]}>
|
||
<EmojiPanel
|
||
onInsertEmoji={handleInsertEmoji}
|
||
onInsertSticker={handleSendSticker}
|
||
onClose={closePanel}
|
||
onFocusInput={() => textInputRef.current?.focus()}
|
||
/>
|
||
</View>
|
||
)}
|
||
|
||
{/* 更多功能面板 */}
|
||
{activePanel === 'more' && keyboardHeight === 0 && (
|
||
<View style={[styles.panelWrapper, { height: PANEL_HEIGHTS.more + insets.bottom, paddingBottom: insets.bottom }]}>
|
||
<MorePanel
|
||
onAction={handleMoreAction}
|
||
disabledActionIds={!isGroupChat && !canSendPrivateImage ? ['image', 'camera'] : []}
|
||
isGroupChat={isGroupChat}
|
||
/>
|
||
</View>
|
||
)}
|
||
</View>
|
||
|
||
{/* 发送图片加载遮罩 */}
|
||
{(sendingImage || uploadingAttachments) && (
|
||
<View style={styles.overlay}>
|
||
<View style={styles.overlayContent}>
|
||
<ActivityIndicator size="large" color={colors.primary.main} />
|
||
<Text style={styles.overlayText}>
|
||
{uploadingAttachments ? '正在上传图片…' : '发送图片中…'}
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
|
||
{/* @成员选择浮层 - 绝对定位在输入框上方,覆盖消息列表 */}
|
||
{activePanel === 'mention' && (
|
||
<View
|
||
style={[
|
||
styles.mentionPanelFloat,
|
||
{
|
||
bottom: keyboardHeight > 0
|
||
? keyboardHeight + inputWrapperHeight
|
||
: inputWrapperHeight + (insets.bottom > 0 ? insets.bottom : 0),
|
||
height: PANEL_HEIGHTS.mention,
|
||
},
|
||
]}
|
||
>
|
||
<MentionPanel
|
||
members={groupMembers}
|
||
currentUserId={currentUserId}
|
||
mentionQuery={mentionQuery}
|
||
currentUserRole={currentUserRole}
|
||
onSelectMention={handleSelectMention}
|
||
onMentionAll={handleMentionAll}
|
||
onClose={closePanel}
|
||
/>
|
||
</View>
|
||
)}
|
||
|
||
{/* 长按菜单 */}
|
||
<LongPressMenu
|
||
visible={longPressMenuVisible}
|
||
message={selectedMessage}
|
||
currentUserId={currentUserId}
|
||
position={menuPosition}
|
||
onClose={hideLongPressMenu}
|
||
onReply={handleReplyMessage}
|
||
onRecall={handleRecall}
|
||
onDelete={handleDeleteMessage}
|
||
memberMap={longPressMenuMemberMap}
|
||
/>
|
||
|
||
{/* 图片查看器 */}
|
||
<ImageGallery
|
||
visible={showImageViewer}
|
||
images={chatImages.map(img => ({
|
||
id: img.id || img.url || String(Math.random()),
|
||
url: img.url || img.uri || ''
|
||
}))}
|
||
initialIndex={selectedImageIndex}
|
||
onClose={handleCloseImageViewer}
|
||
enableSave
|
||
/>
|
||
|
||
{/* 群信息侧边栏面板 - 仅大屏幕显示 */}
|
||
<GroupInfoPanel
|
||
visible={showGroupInfoPanel}
|
||
groupId={routeGroupId ? String(routeGroupId) : undefined}
|
||
groupInfo={groupInfo as any}
|
||
conversationId={conversationId || undefined}
|
||
currentUserId={currentUserId}
|
||
isPinned={false}
|
||
onClose={handleCloseGroupInfoPanel}
|
||
onTogglePin={(pinned) => {
|
||
// TODO: 实现置顶功能
|
||
console.log('Toggle pin:', pinned);
|
||
}}
|
||
onLeaveGroup={() => {
|
||
// TODO: 实现退出群聊功能
|
||
console.log('Leave group');
|
||
}}
|
||
onInviteMembers={() => {
|
||
// TODO: 实现邀请新成员功能
|
||
console.log('Invite members');
|
||
}}
|
||
onViewAllMembers={() => {
|
||
// TODO: 实现查看全部成员功能
|
||
console.log('View all members');
|
||
}}
|
||
/>
|
||
</KeyboardAvoidingView>
|
||
</SafeAreaView>
|
||
);
|
||
};
|
||
|
||
export default ChatScreen;
|