refactor(App, navigation): migrate to Expo Router and clean up navigation structure
- Updated App entry point to utilize Expo Router, simplifying the navigation setup. - Removed legacy navigation components and services to streamline the codebase. - Adjusted package.json to reflect the new entry point and updated dependencies for compatibility. - Enhanced overall application structure by consolidating navigation logic and improving maintainability.
This commit is contained in:
@@ -7,10 +7,10 @@ import React, { useMemo } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Avatar, Text } from '../../../../components/common';
|
||||
import { Avatar, Text, AppBackButton } from '../../../../components/common';
|
||||
import { colors, spacing } from '../../../../theme';
|
||||
import { chatScreenStyles as baseStyles } from './styles';
|
||||
import { useResponsive, useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||
import { useBreakpointGTE } from '../../../../hooks/useResponsive';
|
||||
import { ChatHeaderProps } from './types';
|
||||
|
||||
export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||
@@ -26,7 +26,6 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||
isWideScreen: propIsWideScreen,
|
||||
}) => {
|
||||
// 响应式布局
|
||||
const { width } = useResponsive();
|
||||
// 使用 768px 作为大屏幕和小屏幕的分界线
|
||||
const isWideScreenHook = useBreakpointGTE('lg');
|
||||
// 优先使用 props 传入的 isWideScreen,否则使用 hook
|
||||
@@ -74,12 +73,7 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||
<View style={styles.header}>
|
||||
{/* 大屏幕(>= 768px)时隐藏返回按钮 */}
|
||||
{!isWideScreen ? (
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={onBack}
|
||||
>
|
||||
<MaterialCommunityIcons name="arrow-left" size={22} color={colors.primary.dark} />
|
||||
</TouchableOpacity>
|
||||
<AppBackButton style={styles.backButton} onPress={onBack} />
|
||||
) : (
|
||||
<View style={styles.backButton} />
|
||||
)}
|
||||
@@ -131,14 +125,14 @@ export const ChatHeader: React.FC<ChatHeaderProps> = ({
|
||||
style={styles.moreButton}
|
||||
onPress={onGroupInfoPress}
|
||||
>
|
||||
<MaterialCommunityIcons name="dots-horizontal" size={22} color="#667085" />
|
||||
<MaterialCommunityIcons name="dots-horizontal" size={22} color={colors.text.primary} />
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
style={styles.moreButton}
|
||||
onPress={onMorePress}
|
||||
>
|
||||
<MaterialCommunityIcons name="dots-horizontal" size={22} color="#667085" />
|
||||
<MaterialCommunityIcons name="dots-horizontal" size={22} color={colors.text.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -120,7 +120,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
const data = s.data as ImageSegmentData;
|
||||
return {
|
||||
id: `img-${message.id}-${idx}`,
|
||||
url: data.url,
|
||||
url: data.url || data.thumbnail_url || '',
|
||||
thumbnail_url: data.thumbnail_url,
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
@@ -128,7 +128,6 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
});
|
||||
|
||||
// 检查当前消息是否被选中
|
||||
const isSelected = selectedMessageId === String(message.id);
|
||||
|
||||
// 获取发送者信息(群聊)- 供子组件使用
|
||||
const getSenderInfo = React.useCallback((senderId: string): SenderInfo => {
|
||||
@@ -307,7 +306,6 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
isMe ? styles.myBubble : styles.theirBubble,
|
||||
hasReply && segmentStyles.replyBubble,
|
||||
isPureImageMessage && segmentStyles.pureImageBubble,
|
||||
isSelected && (isMe ? styles.mySelectedBubble : styles.theirSelectedBubble),
|
||||
]}>
|
||||
<MessageSegmentsRenderer
|
||||
segments={segments}
|
||||
@@ -320,7 +318,9 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
onReplyPress={onReplyPress}
|
||||
onImagePress={(url) => {
|
||||
// 查找点击的图片索引
|
||||
const clickIndex = imageSegments.findIndex(img => img.url === url);
|
||||
const clickIndex = imageSegments.findIndex(
|
||||
img => img.url === url || img.thumbnail_url === url
|
||||
);
|
||||
if (clickIndex !== -1 && onImagePress) {
|
||||
onImagePress(imageSegments, clickIndex);
|
||||
}
|
||||
@@ -419,7 +419,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={styles.messageContent}>
|
||||
<View style={[styles.messageContent, isMe ? styles.myMessageContentPanel : null]}>
|
||||
{/* 群聊模式:显示发送者昵称 */}
|
||||
{isGroupChat && !isMe && senderInfo && (
|
||||
<Text style={styles.senderName}>{senderInfo.nickname}</Text>
|
||||
@@ -483,16 +483,4 @@ const segmentStyles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
// 使用 React.memo 优化渲染性能
|
||||
export default React.memo(MessageBubble, (prevProps, nextProps) => {
|
||||
// 自定义比较逻辑:只比较关键属性
|
||||
return (
|
||||
prevProps.message.id === nextProps.message.id &&
|
||||
prevProps.message.status === nextProps.message.status &&
|
||||
prevProps.selectedMessageId === nextProps.selectedMessageId &&
|
||||
prevProps.currentUserId === nextProps.currentUserId &&
|
||||
prevProps.isGroupChat === nextProps.isGroupChat &&
|
||||
prevProps.otherUserLastReadSeq === nextProps.otherUserLastReadSeq &&
|
||||
prevProps.index === nextProps.index
|
||||
);
|
||||
});
|
||||
export default MessageBubble;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 用于渲染消息链中的各种 Segment 类型
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -131,11 +131,21 @@ const ImageSegment: React.FC<{
|
||||
const pressPositionRef = useRef({ x: 0, y: 0 });
|
||||
const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null);
|
||||
const [loadError, setLoadError] = useState(false);
|
||||
const imageUrl = data.thumbnail_url || data.url;
|
||||
const [currentImageUri, setCurrentImageUri] = useState(data.thumbnail_url || data.url || '');
|
||||
const imageUrl = currentImageUri;
|
||||
const pressUrl = data.url || data.thumbnail_url || '';
|
||||
const stableImageKey = `${data.url || ''}|${data.thumbnail_url || ''}|${data.width || 0}x${data.height || 0}`;
|
||||
|
||||
// 如果没有有效的图片URL,显示图片占位符
|
||||
const hasValidUrl = !!imageUrl && imageUrl.trim() !== '';
|
||||
|
||||
useEffect(() => {
|
||||
// 切换消息图片时重置状态,避免列表复用导致旧状态污染
|
||||
setCurrentImageUri(data.thumbnail_url || data.url || '');
|
||||
setDimensions(null);
|
||||
setLoadError(false);
|
||||
}, [data.thumbnail_url, data.url]);
|
||||
|
||||
useEffect(() => {
|
||||
// 重置状态
|
||||
setLoadError(false);
|
||||
@@ -169,9 +179,7 @@ const ImageSegment: React.FC<{
|
||||
|
||||
// 添加超时处理,防止高分辨率图片加载卡住
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!dimensions) {
|
||||
setDimensions(IMAGE_FALLBACK_SIZE);
|
||||
}
|
||||
setDimensions(prev => prev || IMAGE_FALLBACK_SIZE);
|
||||
}, 3000);
|
||||
|
||||
// 否则从图片获取实际尺寸
|
||||
@@ -196,7 +204,7 @@ const ImageSegment: React.FC<{
|
||||
|
||||
setDimensions({ width: Math.round(newWidth), height: Math.round(newHeight) });
|
||||
},
|
||||
(error) => {
|
||||
() => {
|
||||
clearTimeout(timeoutId);
|
||||
// 获取失败时使用默认 4:3 比例
|
||||
setDimensions(IMAGE_FALLBACK_SIZE);
|
||||
@@ -221,10 +229,9 @@ const ImageSegment: React.FC<{
|
||||
if (!hasValidUrl || loadError) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`image-${data.url || Math.random()}`}
|
||||
style={[styles.imageContainer, isMe ? styles.imageMe : styles.imageOther]}
|
||||
onPressIn={handlePressIn}
|
||||
onPress={() => hasValidUrl && onImagePress?.(data.url)}
|
||||
onPress={() => hasValidUrl && onImagePress?.(pressUrl)}
|
||||
onLongPress={handleLongPress}
|
||||
delayLongPress={500}
|
||||
activeOpacity={0.9}
|
||||
@@ -250,10 +257,9 @@ const ImageSegment: React.FC<{
|
||||
if (!dimensions) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`image-${data.url}`}
|
||||
style={[styles.imageContainer, isMe ? styles.imageMe : styles.imageOther]}
|
||||
onPressIn={handlePressIn}
|
||||
onPress={() => onImagePress?.(data.url)}
|
||||
onPress={() => onImagePress?.(pressUrl)}
|
||||
onLongPress={handleLongPress}
|
||||
delayLongPress={500}
|
||||
activeOpacity={0.9}
|
||||
@@ -272,10 +278,9 @@ const ImageSegment: React.FC<{
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`image-${data.url}`}
|
||||
style={[styles.imageContainer, isMe ? styles.imageMe : styles.imageOther]}
|
||||
onPressIn={handlePressIn}
|
||||
onPress={() => onImagePress?.(data.url)}
|
||||
onPress={() => onImagePress?.(pressUrl)}
|
||||
onLongPress={handleLongPress}
|
||||
delayLongPress={500}
|
||||
activeOpacity={0.9}
|
||||
@@ -287,11 +292,20 @@ const ImageSegment: React.FC<{
|
||||
height: dimensions.height,
|
||||
borderRadius: 8,
|
||||
}}
|
||||
recyclingKey={stableImageKey}
|
||||
contentFit="cover"
|
||||
cachePolicy="disk"
|
||||
cachePolicy="memory-disk"
|
||||
priority="normal"
|
||||
transition={200}
|
||||
onLoadStart={() => {
|
||||
setLoadError(false);
|
||||
}}
|
||||
onError={() => {
|
||||
// 缩略图失败时自动回退原图,降低跳转定位后的白块/丢图概率
|
||||
if (data.thumbnail_url && data.url && imageUrl === data.thumbnail_url) {
|
||||
setCurrentImageUri(data.url);
|
||||
return;
|
||||
}
|
||||
setLoadError(true);
|
||||
}}
|
||||
/>
|
||||
@@ -680,7 +694,9 @@ export const MessageSegmentsRenderer: React.FC<{
|
||||
{/* 其他 Segment 内容 */}
|
||||
<View style={styles.segmentsContent}>
|
||||
{otherSegments.map((segment, index) => (
|
||||
<React.Fragment key={`segment-${index}-${segment.type}`}>
|
||||
<React.Fragment
|
||||
key={`segment-${segment.type}-${(segment as any)?.data?.url || (segment as any)?.data?.id || (segment as any)?.data?.user_id || index}`}
|
||||
>
|
||||
{renderSegment(segment, renderProps)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
@@ -738,17 +754,17 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 2,
|
||||
},
|
||||
atTextMe: {
|
||||
color: '#1976D2', // 蓝色
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.12)',
|
||||
color: '#4A88C7',
|
||||
backgroundColor: 'rgba(74, 136, 199, 0.12)',
|
||||
borderRadius: 4,
|
||||
},
|
||||
atTextOther: {
|
||||
color: '#1976D2', // 蓝色
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.12)',
|
||||
color: '#4A88C7',
|
||||
backgroundColor: 'rgba(74, 136, 199, 0.12)',
|
||||
borderRadius: 4,
|
||||
},
|
||||
atHighlight: {
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.2)',
|
||||
backgroundColor: 'rgba(74, 136, 199, 0.2)',
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 4,
|
||||
},
|
||||
@@ -966,7 +982,7 @@ const styles = StyleSheet.create({
|
||||
elevation: 0,
|
||||
},
|
||||
replyMe: {
|
||||
backgroundColor: 'rgba(25, 118, 210, 0.1)',
|
||||
backgroundColor: 'rgba(74, 136, 199, 0.12)',
|
||||
},
|
||||
replyOther: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.03)',
|
||||
@@ -984,7 +1000,7 @@ const styles = StyleSheet.create({
|
||||
replySender: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: '#1976D2',
|
||||
color: '#4A88C7',
|
||||
marginRight: 6,
|
||||
flexShrink: 0,
|
||||
},
|
||||
|
||||
@@ -23,8 +23,8 @@ export const bubbleColors = {
|
||||
},
|
||||
// 被回复的消息高亮
|
||||
replyHighlight: {
|
||||
background: 'rgba(255, 107, 53, 0.08)',
|
||||
borderLeft: '#FF6B35',
|
||||
background: 'rgba(74, 136, 199, 0.1)',
|
||||
borderLeft: '#4A88C7',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
senderName: {
|
||||
fontSize: 13,
|
||||
color: '#1976D2',
|
||||
color: '#4A88C7',
|
||||
marginBottom: 4,
|
||||
marginLeft: 4,
|
||||
fontWeight: '600',
|
||||
@@ -215,7 +215,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
minWidth: 60,
|
||||
},
|
||||
myBubble: {
|
||||
backgroundColor: '#E3F2FD', // Telegram风格的浅蓝色
|
||||
backgroundColor: '#DFF2FF', // Telegram风格的浅蓝色
|
||||
borderBottomRightRadius: 4, // 右下角尖,箭头在右下
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
@@ -255,17 +255,23 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
// 选中状态
|
||||
selectedBubble: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#007AFF',
|
||||
borderColor: '#7FB6E6',
|
||||
},
|
||||
myMessageContentPanel: {
|
||||
backgroundColor: '#DFF2FF',
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 4,
|
||||
},
|
||||
mySelectedBubble: {
|
||||
backgroundColor: '#0051D5',
|
||||
backgroundColor: '#CFEAFF',
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(255,255,255,0.5)',
|
||||
borderColor: '#7FB6E6',
|
||||
},
|
||||
theirSelectedBubble: {
|
||||
backgroundColor: '#E8F1FF',
|
||||
backgroundColor: '#EEF5FC',
|
||||
borderWidth: 2,
|
||||
borderColor: '#007AFF',
|
||||
borderColor: '#7FB6E6',
|
||||
},
|
||||
selectedText: {
|
||||
// 文本选中时的样式
|
||||
@@ -539,7 +545,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
marginRight: spacing.xs,
|
||||
},
|
||||
panelTabActive: {
|
||||
backgroundColor: '#E3F2FD',
|
||||
backgroundColor: '#DFF2FF',
|
||||
},
|
||||
panelTabEmoji: {
|
||||
fontSize: 24,
|
||||
@@ -1025,11 +1031,11 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
// 表情包选择状态
|
||||
stickerItemSelected: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#007AFF',
|
||||
borderColor: '#7FB6E6',
|
||||
},
|
||||
stickerCheckOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.1)',
|
||||
backgroundColor: 'rgba(127, 182, 230, 0.14)',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
padding: 4,
|
||||
@@ -1045,8 +1051,8 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
stickerCheckBoxSelected: {
|
||||
backgroundColor: '#007AFF',
|
||||
borderColor: '#007AFF',
|
||||
backgroundColor: '#7FB6E6',
|
||||
borderColor: '#7FB6E6',
|
||||
},
|
||||
|
||||
// 表情管理模态框
|
||||
@@ -1077,12 +1083,12 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
manageDoneText: {
|
||||
fontSize: 16,
|
||||
color: '#007AFF',
|
||||
color: '#4A88C7',
|
||||
fontWeight: '500',
|
||||
},
|
||||
manageSelectText: {
|
||||
fontSize: 16,
|
||||
color: '#007AFF',
|
||||
color: '#4A88C7',
|
||||
fontWeight: '500',
|
||||
},
|
||||
manageInfoBar: {
|
||||
@@ -1120,7 +1126,7 @@ export const chatScreenStyles = StyleSheet.create({
|
||||
},
|
||||
manageSelectAllText: {
|
||||
fontSize: 15,
|
||||
color: '#007AFF',
|
||||
color: '#4A88C7',
|
||||
fontWeight: '500',
|
||||
},
|
||||
manageDeleteButton: {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
KeyboardEvent,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
|
||||
import { useLocalSearchParams, router } from 'expo-router';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
@@ -30,8 +30,8 @@ import { useChat, useGroupTyping, useGroupMuted, messageManager } from '../../..
|
||||
import { groupService } from '../../../../services/groupService';
|
||||
import { userManager } from '../../../../stores/userManager';
|
||||
import { groupManager } from '../../../../stores/groupManager';
|
||||
import { RootStackParamList } from '../../../../navigation/types';
|
||||
import { navigationService } from '../../../../infrastructure/navigation/navigationService';
|
||||
import * as hrefs from '../../../../navigation/hrefs';
|
||||
import { firstRouteParam } from '../../../../navigation/paramUtils';
|
||||
import {
|
||||
GroupMessage,
|
||||
PanelType,
|
||||
@@ -46,8 +46,6 @@ import {
|
||||
clearConversationMessages,
|
||||
} from '../../../../services/database';
|
||||
|
||||
type ChatRouteProp = RouteProp<RootStackParamList, 'Chat'>;
|
||||
|
||||
export const useChatScreen = () => {
|
||||
const getSendErrorMessage = useCallback((error: unknown, fallback: string) => {
|
||||
if (error instanceof ApiError && error.message) {
|
||||
@@ -56,17 +54,30 @@ export const useChatScreen = () => {
|
||||
return fallback;
|
||||
}, []);
|
||||
|
||||
const route = useRoute<ChatRouteProp>();
|
||||
const navigation = useNavigation();
|
||||
|
||||
// 路由参数
|
||||
const routeParams = useMemo(() => ({
|
||||
conversationId: route.params?.conversationId || null,
|
||||
userId: route.params?.userId || null,
|
||||
isGroupChat: route.params?.isGroupChat || false,
|
||||
groupId: route.params?.groupId,
|
||||
groupName: route.params?.groupName,
|
||||
}), [route.params?.conversationId, route.params?.userId, route.params?.isGroupChat, route.params?.groupId, route.params?.groupName]);
|
||||
const rawParams = useLocalSearchParams<{
|
||||
conversationId?: string | string[];
|
||||
userId?: string | string[];
|
||||
isGroupChat?: string | string[];
|
||||
groupId?: string | string[];
|
||||
groupName?: string | string[];
|
||||
}>();
|
||||
// 路由参数(动态段 + query);群 ID 勿用 Number(),避免超过 2^53-1 时精度丢失
|
||||
const routeParams = useMemo(() => {
|
||||
const isGroupFlag = firstRouteParam(rawParams.isGroupChat);
|
||||
return {
|
||||
conversationId: firstRouteParam(rawParams.conversationId) ?? null,
|
||||
userId: firstRouteParam(rawParams.userId) ?? null,
|
||||
isGroupChat: isGroupFlag === '1' || isGroupFlag === 'true',
|
||||
groupId: firstRouteParam(rawParams.groupId),
|
||||
groupName: firstRouteParam(rawParams.groupName),
|
||||
};
|
||||
}, [
|
||||
rawParams.conversationId,
|
||||
rawParams.userId,
|
||||
rawParams.isGroupChat,
|
||||
rawParams.groupId,
|
||||
rawParams.groupName,
|
||||
]);
|
||||
|
||||
const { conversationId: routeConversationId, userId: routeUserId, isGroupChat, groupId: routeGroupId, groupName: routeGroupName } = routeParams;
|
||||
|
||||
@@ -399,8 +410,30 @@ export const useChatScreen = () => {
|
||||
console.error('获取成员信息失败:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
const isGroupNotFound =
|
||||
error instanceof ApiError &&
|
||||
(error.code === 404 ||
|
||||
error.message === '群组不存在' ||
|
||||
error.message.includes('群组不存在'));
|
||||
|
||||
if (isGroupNotFound) {
|
||||
Alert.alert('提示', '该群组不存在或已解散', [
|
||||
{
|
||||
text: '确定',
|
||||
onPress: () => {
|
||||
if (router.canGoBack()) {
|
||||
router.back();
|
||||
} else {
|
||||
router.replace(hrefs.hrefMessages());
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('获取群组信息失败:', error);
|
||||
Alert.alert('错误', '无法获取群组信息');
|
||||
Alert.alert('错误', getSendErrorMessage(error, '无法获取群组信息'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1129,7 +1162,7 @@ export const useChatScreen = () => {
|
||||
// 点击头像跳转到用户主页
|
||||
const handleAvatarPress = useCallback((userId: string) => {
|
||||
if (userId && userId !== currentUserId) {
|
||||
navigationService.navigate('UserProfile', { userId: String(userId) });
|
||||
router.push(hrefs.hrefUserProfile(String(userId)));
|
||||
}
|
||||
}, [currentUserId]);
|
||||
|
||||
@@ -1198,31 +1231,27 @@ export const useChatScreen = () => {
|
||||
// 导航到群组信息或用户资料
|
||||
const navigateToInfo = useCallback(() => {
|
||||
if (isGroupChat && routeGroupId) {
|
||||
navigation.navigate('GroupInfo' as any, {
|
||||
groupId: routeGroupId,
|
||||
conversationId: conversationId || undefined,
|
||||
});
|
||||
router.push(hrefs.hrefGroupInfo(routeGroupId, conversationId || undefined));
|
||||
} else if (otherUser?.id) {
|
||||
navigationService.navigate('UserProfile', { userId: String(otherUser.id) });
|
||||
router.push(hrefs.hrefUserProfile(String(otherUser.id)));
|
||||
}
|
||||
}, [navigation, isGroupChat, routeGroupId, otherUser]);
|
||||
}, [isGroupChat, routeGroupId, otherUser, conversationId]);
|
||||
|
||||
// 导航到聊天管理页面
|
||||
const navigateToChatSettings = useCallback(() => {
|
||||
if (isGroupChat && routeGroupId) {
|
||||
navigation.navigate('GroupInfo' as any, {
|
||||
groupId: routeGroupId,
|
||||
conversationId: conversationId || undefined,
|
||||
});
|
||||
router.push(hrefs.hrefGroupInfo(routeGroupId, conversationId || undefined));
|
||||
} else if (otherUser?.id && conversationId) {
|
||||
navigation.navigate('PrivateChatInfo' as any, {
|
||||
conversationId: conversationId,
|
||||
userId: String(otherUser.id),
|
||||
userName: otherUser.nickname,
|
||||
userAvatar: otherUser.avatar,
|
||||
});
|
||||
router.push(
|
||||
hrefs.hrefPrivateChatInfo({
|
||||
conversationId,
|
||||
userId: String(otherUser.id),
|
||||
userName: otherUser.nickname,
|
||||
userAvatar: otherUser.avatar,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [navigation, isGroupChat, routeGroupId, otherUser, conversationId]);
|
||||
}, [isGroupChat, routeGroupId, otherUser, conversationId]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
|
||||
@@ -16,17 +16,18 @@ import {
|
||||
Dimensions,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Image as ExpoImage } from 'expo-image';
|
||||
import { colors, spacing, fontSizes, shadows } from '../../../theme';
|
||||
import { ConversationResponse, MessageResponse, MessageSegment, GroupMemberResponse, ImageSegmentData } from '../../../types/dto';
|
||||
import { Avatar, Text, ImageGallery } from '../../../components/common';
|
||||
import { Avatar, Text, ImageGallery, AppBackButton } from '../../../components/common';
|
||||
import { useAuthStore, messageManager, MessageEvent, MessageSubscriber } from '../../../stores';
|
||||
import { extractTextFromSegments } from '../../../types/dto';
|
||||
import { useBreakpointGTE } from '../../../hooks/useResponsive';
|
||||
import { useMarkAsRead } from '../../../stores/messageManagerHooks';
|
||||
import { messageService } from '../../../services';
|
||||
import * as hrefs from '../../../navigation/hrefs';
|
||||
import { GroupInfoPanel } from './ChatScreen/GroupInfoPanel';
|
||||
import { LongPressMenu, MenuPosition, GroupMessage } from './ChatScreen';
|
||||
|
||||
@@ -39,7 +40,7 @@ interface EmbeddedChatProps {
|
||||
}
|
||||
|
||||
export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack }) => {
|
||||
const navigation = useNavigation();
|
||||
const router = useRouter();
|
||||
const currentUser = useAuthStore(state => state.currentUser);
|
||||
|
||||
// 响应式布局 - 使用 768px 作为大屏幕和小屏幕的分界线
|
||||
@@ -190,21 +191,24 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
// 导航到完整聊天页面
|
||||
const handleNavigateToFullChat = () => {
|
||||
if (isGroupChat && conversation.group) {
|
||||
(navigation as any).navigate('Chat', {
|
||||
conversationId: String(conversation.id),
|
||||
groupId: String(conversation.group.id),
|
||||
groupName: conversation.group.name,
|
||||
isGroupChat: true,
|
||||
});
|
||||
router.push(
|
||||
hrefs.hrefChat({
|
||||
conversationId: String(conversation.id),
|
||||
groupId: String(conversation.group.id),
|
||||
groupName: conversation.group.name,
|
||||
isGroupChat: true,
|
||||
}) as any
|
||||
);
|
||||
} else {
|
||||
const currentUserId = currentUser?.id;
|
||||
const otherUser = conversation.participants?.find(p => String(p.id) !== String(currentUserId));
|
||||
(navigation as any).navigate('Chat', {
|
||||
conversationId: String(conversation.id),
|
||||
userId: otherUser ? String(otherUser.id) : undefined,
|
||||
userName: otherUser?.nickname || otherUser?.username,
|
||||
isGroupChat: false,
|
||||
});
|
||||
router.push(
|
||||
hrefs.hrefChat({
|
||||
conversationId: String(conversation.id),
|
||||
userId: otherUser ? String(otherUser.id) : undefined,
|
||||
isGroupChat: false,
|
||||
}) as any
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -407,9 +411,7 @@ export const EmbeddedChat: React.FC<EmbeddedChatProps> = ({ conversation, onBack
|
||||
<View style={styles.header}>
|
||||
{/* 大屏幕(>= 768px)时隐藏返回按钮 */}
|
||||
{!isWideScreen ? (
|
||||
<TouchableOpacity onPress={onBack} style={styles.headerButton}>
|
||||
<MaterialCommunityIcons name="arrow-left" size={24} color="#666" />
|
||||
</TouchableOpacity>
|
||||
<AppBackButton onPress={onBack} style={styles.headerButton} iconColor="#666" />
|
||||
) : (
|
||||
<View style={styles.headerButton} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user