refactor(App, navigation): migrate to Expo Router and clean up navigation structure
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m27s
Frontend CI / ota-android (push) Successful in 11m6s
Frontend CI / build-android-apk (push) Successful in 1h16m45s

- 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:
lafay
2026-03-24 14:21:31 +08:00
parent b91e78c921
commit 2ddb9cadd8
111 changed files with 1829 additions and 3214 deletions

View File

@@ -27,11 +27,12 @@ import {
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { useNavigation, useRouter } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
import { colors } from '../../theme';
import * as hrefs from '../../navigation/hrefs';
import { messageManager } from '../../stores';
import { useBreakpointGTE } from '../../hooks/useResponsive';
import {
@@ -49,7 +50,8 @@ import {
} from './components/ChatScreen';
export const ChatScreen: React.FC = () => {
const navigation = useNavigation<any>();
const navigation = useNavigation();
const router = useRouter();
const insets = useSafeAreaInsets();
// 响应式布局
@@ -59,21 +61,9 @@ export const ChatScreen: React.FC = () => {
// 监听屏幕宽度变化当变为大屏幕时自动跳转到web端首页
useEffect(() => {
if (isWideScreen) {
// 导航到大屏幕模式的主界面首页
navigation.reset({
index: 0,
routes: [
{
name: 'Main',
params: {
screen: 'HomeTab',
params: { screen: 'Home' }
}
}
]
});
router.replace(hrefs.hrefHome());
}
}, [isWideScreen, navigation]);
}, [isWideScreen, router]);
// 输入框区域高度(用于定位浮动 mention 面板)
const [inputWrapperHeight, setInputWrapperHeight] = useState(60);
@@ -228,16 +218,6 @@ export const ChatScreen: React.FC = () => {
};
}, []);
const highlightMessageTemporarily = useCallback((messageId: string, duration = 1500) => {
if (replyHighlightTimerRef.current) {
clearTimeout(replyHighlightTimerRef.current);
}
setSelectedMessageId(messageId);
replyHighlightTimerRef.current = setTimeout(() => {
setSelectedMessageId(prev => (prev === messageId ? null : prev));
}, duration);
}, [setSelectedMessageId]);
const handleReplyPreviewPress = useCallback((messageId: string) => {
const targetId = String(messageId);
const targetIndex = displayMessages.findIndex(msg => String(msg.id) === targetId);
@@ -245,14 +225,13 @@ export const ChatScreen: React.FC = () => {
replyTargetMessageIdRef.current = targetId;
setBrowsingHistory(true);
highlightMessageTemporarily(targetId);
flatListRef.current?.scrollToIndex({
index: targetIndex,
animated: true,
viewPosition: 0.5,
});
}, [displayMessages, flatListRef, highlightMessageTemporarily, setBrowsingHistory]);
}, [displayMessages, flatListRef, setBrowsingHistory]);
// 监听返回事件,刷新会话列表
useEffect(() => {
@@ -399,7 +378,7 @@ export const ChatScreen: React.FC = () => {
otherUser={otherUser}
routeGroupName={routeGroupName}
typingHint={typingHint}
onBack={() => navigation.goBack()}
onBack={() => router.back()}
onTitlePress={navigateToInfo}
onMorePress={navigateToChatSettings}
onGroupInfoPress={handleGroupInfoPress}
@@ -431,8 +410,8 @@ export const ChatScreen: React.FC = () => {
initialNumToRender={14}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
windowSize={9}
removeClippedSubviews={Platform.OS !== 'web'}
windowSize={15}
removeClippedSubviews={false}
onScroll={handleMessageListScroll}
onScrollBeginDrag={() => {
isUserDraggingRef.current = true;

View File

@@ -16,8 +16,7 @@ import {
Image,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
@@ -25,13 +24,10 @@ import { groupService } from '../../services/groupService';
import { uploadService } from '../../services/uploadService';
import { Avatar, Text, Button, Loading } from '../../components/common';
import { User } from '../../types';
import { RootStackParamList } from '../../navigation/types';
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
const CreateGroupScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const router = useRouter();
// 表单状态
const [groupName, setGroupName] = useState('');
@@ -138,7 +134,7 @@ const CreateGroupScreen: React.FC = () => {
Alert.alert('成功', '群组创建成功', [
{
text: '确定',
onPress: () => navigation.goBack(),
onPress: () => router.back(),
},
]);
} catch (error: any) {

View File

@@ -21,8 +21,8 @@ import {
Dimensions,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useFocusEffect } from '@react-navigation/native';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
@@ -39,16 +39,14 @@ import {
JoinType,
} from '../../types/dto';
import { User } from '../../types';
import { RootStackParamList } from '../../navigation/types';
import { firstRouteParam } from '../../navigation/paramUtils';
import * as hrefs from '../../navigation/hrefs';
import { messageManager } from '../../stores/messageManager';
import { groupManager } from '../../stores/groupManager';
import MutualFollowSelectorModal from './components/MutualFollowSelectorModal';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
type GroupInfoRouteProp = RouteProp<RootStackParamList, 'GroupInfo'>;
// 加群方式选项
const JOIN_TYPE_OPTIONS: { value: JoinType; label: string; icon: string; desc: string }[] = [
{ value: 0, label: '允许任何人加入', icon: 'earth', desc: '任何人都可以直接加入群聊' },
@@ -57,9 +55,13 @@ const JOIN_TYPE_OPTIONS: { value: JoinType; label: string; icon: string; desc: s
];
const GroupInfoScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const route = useRoute<GroupInfoRouteProp>();
const { groupId, conversationId } = route.params;
const router = useRouter();
const { groupId: groupIdParam, conversationId: conversationIdParam } = useLocalSearchParams<{
groupId?: string | string[];
conversationId?: string | string[];
}>();
const groupId = firstRouteParam(groupIdParam) ?? '';
const conversationId = firstRouteParam(conversationIdParam);
const { currentUser } = useAuthStore();
// 响应式布局
@@ -112,6 +114,7 @@ const GroupInfoScreen: React.FC = () => {
// 加载群组信息
const loadGroupInfo = useCallback(async () => {
if (!groupId) return;
try {
setLoading(true);
@@ -138,7 +141,7 @@ const GroupInfoScreen: React.FC = () => {
} catch (error) {
console.error('加载群组信息失败:', error);
Alert.alert('错误', '加载群组信息失败', [
{ text: '确定', onPress: () => navigation.goBack() },
{ text: '确定', onPress: () => router.back() },
]);
} finally {
setLoading(false);
@@ -394,7 +397,7 @@ const GroupInfoScreen: React.FC = () => {
try {
await groupService.dissolveGroup(groupId);
Alert.alert('成功', '群组已解散', [
{ text: '确定', onPress: () => navigation.goBack() },
{ text: '确定', onPress: () => router.back() },
]);
} catch (error: any) {
console.error('解散群组失败:', error);
@@ -420,7 +423,7 @@ const GroupInfoScreen: React.FC = () => {
try {
await groupService.leaveGroup(groupId);
Alert.alert('成功', '已退出群聊', [
{ text: '确定', onPress: () => navigation.goBack() },
{ text: '确定', onPress: () => router.back() },
]);
} catch (error: any) {
console.error('退出群聊失败:', error);
@@ -464,7 +467,7 @@ const GroupInfoScreen: React.FC = () => {
// 跳转到成员管理
const goToMembers = () => {
navigation.navigate('GroupMembers' as any, { groupId });
router.push(hrefs.hrefGroupMembers(groupId));
};
// 获取加群方式文本
@@ -485,8 +488,8 @@ const GroupInfoScreen: React.FC = () => {
Alert.alert('已复制', '群号已复制到剪贴板');
};
const formatGroupNo = (id: string | number) => {
const raw = String(id);
const formatGroupNo = (id: string) => {
const raw = id;
if (raw.length <= 12) return raw;
return `${raw.slice(0, 6)}...${raw.slice(-4)}`;
};

View File

@@ -1,25 +1,36 @@
import React, { useEffect, useMemo, useState } from 'react';
import { View, StyleSheet, ActivityIndicator, Alert, ScrollView } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { View, StyleSheet, ActivityIndicator, Alert, ScrollView, TouchableOpacity } from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { Avatar, Text } from '../../components/common';
import { RootStackParamList } from '../../navigation/types';
import { routePayloadCache } from '../../stores/routePayloadCache';
import { groupService } from '../../services/groupService';
import { groupManager } from '../../stores/groupManager';
import { GroupMemberResponse } from '../../types/dto';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
type Route = RouteProp<RootStackParamList, 'GroupInviteDetail'>;
type Navigation = NativeStackNavigationProp<RootStackParamList>;
const GroupInviteDetailScreen: React.FC = () => {
const route = useRoute<Route>();
const navigation = useNavigation<Navigation>();
const { message } = route.params;
const router = useRouter();
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
if (!message) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<View style={styles.emptyWrap}>
<Text variant="body" color="secondary">
</Text>
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
<Text variant="body"></Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const { extra_data } = message;
const [loadingMembers, setLoadingMembers] = useState(false);
@@ -35,7 +46,7 @@ const GroupInviteDetailScreen: React.FC = () => {
if (!extra_data?.group_id) return;
setLoadingGroup(true);
try {
const group = await groupManager.getGroup(extra_data.group_id);
const group = await groupManager.getGroup(String(extra_data.group_id));
setMemberCount(group.member_count ?? null);
} catch {
setMemberCount(null);
@@ -51,7 +62,7 @@ const GroupInviteDetailScreen: React.FC = () => {
if (!extra_data?.group_id) return;
setLoadingMembers(true);
try {
const res = await groupManager.getMembers(extra_data.group_id, 1, 100);
const res = await groupManager.getMembers(String(extra_data.group_id), 1, 100);
const admins = (res.list || []).filter(m => m.role === 'owner' || m.role === 'admin');
setMembers(admins);
} catch {
@@ -76,9 +87,9 @@ const GroupInviteDetailScreen: React.FC = () => {
}
setSubmitting(true);
try {
await groupService.respondInvite(groupId, { flag, approve });
await groupService.respondInvite(String(groupId), { flag, approve });
Alert.alert('成功', approve ? '已同意加入群聊' : '已拒绝邀请', [
{ text: '确定', onPress: () => navigation.goBack() },
{ text: '确定', onPress: () => router.back() },
]);
} catch {
Alert.alert('操作失败', '请稍后重试');
@@ -87,7 +98,7 @@ const GroupInviteDetailScreen: React.FC = () => {
}
};
const groupNo = useMemo(() => extra_data?.group_id || '-', [extra_data?.group_id]);
const groupNo = useMemo(() => String(extra_data?.group_id ?? '-'), [extra_data?.group_id]);
const formatGroupNo = (id: string) => {
if (id.length <= 12) return id;
return `${id.slice(0, 6)}...${id.slice(-4)}`;
@@ -168,6 +179,16 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: colors.background.default,
},
emptyWrap: {
flex: 1,
padding: spacing.lg,
justifyContent: 'center',
alignItems: 'center',
gap: spacing.md,
},
backBtn: {
padding: spacing.sm,
},
scrollView: {
flex: 1,
},

View File

@@ -19,8 +19,7 @@ import {
Dimensions,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius } from '../../theme';
import { useAuthStore } from '../../stores';
@@ -38,8 +37,7 @@ import {
GroupMemberResponse,
GroupRole,
} from '../../types/dto';
import { RootStackParamList } from '../../navigation/types';
import { firstRouteParam } from '../../navigation/paramUtils';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
const GROUP_MEMBER_REMOTE_LIST_KIND: GroupMemberListSourceKind = 'cursor';
@@ -50,9 +48,6 @@ const GRID_CONFIG = {
desktop: { columns: 3, itemWidth: '31%' },
};
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
type GroupMembersRouteProp = RouteProp<RootStackParamList, 'GroupMembers'>;
// 成员分组
interface MemberGroup {
title: string;
@@ -60,9 +55,8 @@ interface MemberGroup {
}
const GroupMembersScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const route = useRoute<GroupMembersRouteProp>();
const { groupId } = route.params;
const { groupId: groupIdParam } = useLocalSearchParams<{ groupId?: string | string[] }>();
const groupId = firstRouteParam(groupIdParam) ?? '';
const { currentUser } = useAuthStore();
// 响应式布局
@@ -155,8 +149,9 @@ const GroupMembersScreen: React.FC = () => {
// 初始加载
useEffect(() => {
if (!groupId) return;
refresh();
}, [groupId]);
}, [groupId, refresh]);
// 按角色分组
const groupMembers = useCallback((): MemberGroup[] => {

View File

@@ -1,25 +1,38 @@
import React, { useEffect, useMemo, useState } from 'react';
import { View, StyleSheet, TouchableOpacity, Alert, ScrollView } from 'react-native';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { Avatar, Text } from '../../components/common';
import { RootStackParamList } from '../../navigation/types';
import * as hrefs from '../../navigation/hrefs';
import { routePayloadCache } from '../../stores/routePayloadCache';
import { groupService } from '../../services/groupService';
import { groupManager } from '../../stores/groupManager';
import { userManager } from '../../stores/userManager';
import { GroupInfoSummaryCard, DecisionFooter } from './components/GroupRequestShared';
type Route = RouteProp<RootStackParamList, 'GroupRequestDetail'>;
type Navigation = NativeStackNavigationProp<RootStackParamList>;
const GroupRequestDetailScreen: React.FC = () => {
const route = useRoute<Route>();
const navigation = useNavigation<Navigation>();
const { message } = route.params;
const router = useRouter();
const { messageId } = useLocalSearchParams<{ messageId?: string }>();
const message = messageId ? routePayloadCache.getSystemMessage(messageId) : undefined;
if (!message) {
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<View style={styles.emptyWrap}>
<Text variant="body" color="secondary">
</Text>
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
<Text variant="body"></Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const { extra_data } = message;
const [submitting, setSubmitting] = useState(false);
const [memberCount, setMemberCount] = useState<number | null>(null);
@@ -64,7 +77,7 @@ const GroupRequestDetailScreen: React.FC = () => {
if (!extra_data?.group_id) return;
setLoadingGroup(true);
try {
const group = await groupManager.getGroup(extra_data.group_id);
const group = await groupManager.getGroup(String(extra_data.group_id));
setMemberCount(group.member_count ?? null);
} catch {
setMemberCount(null);
@@ -90,18 +103,18 @@ const GroupRequestDetailScreen: React.FC = () => {
setSubmitting(true);
try {
if (message.system_type === 'group_invite') {
await groupService.respondInvite(groupId, {
await groupService.respondInvite(String(groupId), {
flag,
approve,
});
} else {
await groupService.reviewJoinRequest(groupId, {
await groupService.reviewJoinRequest(String(groupId), {
flag,
approve,
});
}
Alert.alert('成功', approve ? '已同意申请' : '已拒绝申请', [
{ text: '确定', onPress: () => navigation.goBack() },
{ text: '确定', onPress: () => router.back() },
]);
} catch (error) {
Alert.alert('操作失败', '请稍后重试');
@@ -114,7 +127,7 @@ const GroupRequestDetailScreen: React.FC = () => {
if (!applicantId) return;
const user = await userManager.getUserById(applicantId);
if (user?.id) {
navigation.navigate('UserProfile', { userId: String(user.id) });
router.push(hrefs.hrefUserProfile(String(user.id)));
}
};
@@ -124,7 +137,7 @@ const GroupRequestDetailScreen: React.FC = () => {
<GroupInfoSummaryCard
groupName={extra_data?.group_name}
groupAvatar={extra_data?.group_avatar}
groupNo={extra_data?.group_id}
groupNo={String(extra_data?.group_id ?? '')}
groupDescription={extra_data?.group_description}
memberCountText={loadingGroup ? '人数加载中...' : `${memberCount ?? '-'}`}
/>
@@ -166,6 +179,16 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: colors.background.default,
},
emptyWrap: {
flex: 1,
padding: spacing.lg,
justifyContent: 'center',
alignItems: 'center',
gap: spacing.md,
},
backBtn: {
padding: spacing.sm,
},
scrollView: {
flex: 1,
},

View File

@@ -10,8 +10,7 @@ import {
FlatList,
RefreshControl,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius } from '../../theme';
@@ -19,15 +18,12 @@ import Avatar from '../../components/common/Avatar';
import Text from '../../components/common/Text';
import { groupService } from '../../services/groupService';
import { groupManager } from '../../stores/groupManager';
import { RootStackParamList } from '../../navigation/types';
import { GroupResponse, JoinType } from '../../types/dto';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { EmptyState } from '../../components/common';
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
const JoinGroupScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const router = useRouter();
const [keyword, setKeyword] = useState('');
const [searching, setSearching] = useState(false);
const [joiningGroupId, setJoiningGroupId] = useState<string | null>(null);
@@ -92,7 +88,7 @@ const JoinGroupScreen: React.FC = () => {
Alert.alert('成功', '操作已提交', [
{
text: '确定',
onPress: () => navigation.goBack(),
onPress: () => router.back(),
},
]);
} catch (error: any) {
@@ -106,13 +102,13 @@ const JoinGroupScreen: React.FC = () => {
}
};
const handleCopyGroupId = (groupId: string | number) => {
Clipboard.setString(String(groupId));
const handleCopyGroupId = (groupId: string) => {
Clipboard.setString(groupId);
Alert.alert('已复制', '群号已复制到剪贴板');
};
const formatGroupNo = (id: string | number) => {
const raw = String(id);
const formatGroupNo = (id: string) => {
const raw = id;
if (raw.length <= 12) return raw;
return `${raw.slice(0, 6)}...${raw.slice(-4)}`;
};

View File

@@ -26,8 +26,8 @@ import {
Platform,
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useNavigation, useIsFocused } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useRouter } from 'expo-router';
import { useIsFocused } from '@react-navigation/native';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, shadows, borderRadius } from '../../theme';
@@ -43,9 +43,9 @@ import {
useMarkAsRead,
useMessageManagerConversations,
} from '../../stores';
import { Avatar, Text, EmptyState, ResponsiveContainer } from '../../components/common';
import { Avatar, Text, EmptyState, ResponsiveContainer, AppBackButton } from '../../components/common';
import { useResponsive, useBreakpointGTE } from '../../hooks/useResponsive';
import { RootStackParamList, MessageStackParamList } from '../../navigation/types';
import * as hrefs from '../../navigation/hrefs';
// 导入 EmbeddedChat 组件用于桌面端双栏布局
import { EmbeddedChat } from './components/EmbeddedChat';
import { ConversationListRow } from './components/ConversationListRow';
@@ -54,9 +54,6 @@ import { NotificationsScreen } from './NotificationsScreen';
// 导入扫码组件
import { QRCodeScanner } from '../../components/business/QRCodeScanner';
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
type MessageNavProp = NativeStackNavigationProp<MessageStackParamList>;
// 系统通知会话特殊ID
const SYSTEM_MESSAGE_CHANNEL_ID = '-1';
@@ -76,7 +73,7 @@ interface SearchResultItem {
* 支持响应式双栏布局
*/
export const MessageListScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp & MessageNavProp>();
const router = useRouter();
const isFocused = useIsFocused();
const insets = useSafeAreaInsets();
// 在大屏幕模式下不使用 Bottom Tab Navigator所以不需要 tabBarHeight
@@ -286,22 +283,25 @@ export const MessageListScreen: React.FC = () => {
setSelectedConversation(conversation);
} else if (conversation.type === 'group' && 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,
})
);
} else {
// 私聊 - 窄屏下导航
const currentUserId = useAuthStore.getState().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,
})
);
}
});
};
@@ -311,12 +311,12 @@ export const MessageListScreen: React.FC = () => {
// 创建群聊
const handleCreateGroup = () => {
(navigation as any).navigate('CreateGroup');
router.push(hrefs.hrefGroupCreate());
};
// 主动加群
const handleJoinGroup = () => {
(navigation as any).navigate('JoinGroup');
router.push(hrefs.hrefGroupJoin());
};
const handleOpenActionMenu = () => {
@@ -581,12 +581,13 @@ export const MessageListScreen: React.FC = () => {
const conversation = await createConversation(String(user.id));
if (conversation) {
(navigation as any).navigate('Chat', {
conversationId: String(conversation.id),
userId: String(user.id),
userName: user.nickname || user.username,
isGroupChat: false,
});
router.push(
hrefs.hrefChat({
conversationId: String(conversation.id),
userId: String(user.id),
isGroupChat: false,
})
);
}
} catch (error) {
console.error('创建会话失败:', error);
@@ -660,9 +661,7 @@ export const MessageListScreen: React.FC = () => {
const renderSearchMode = () => (
<View style={styles.searchModeContainer}>
<View style={styles.searchInputContainer}>
<TouchableOpacity onPress={handleCloseSearch}>
<MaterialCommunityIcons name="arrow-left" size={22} color="#666" />
</TouchableOpacity>
<AppBackButton onPress={handleCloseSearch} iconColor="#666" />
<TextInput
style={styles.searchInput}
placeholder={activeSearchTab === 'chat' ? '搜索聊天记录' : '搜索用户'}

View File

@@ -18,17 +18,16 @@ import {
} from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useIsFocused } from '@react-navigation/native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useRouter } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, borderRadius, shadows } from '../../theme';
import { SystemMessageResponse } from '../../types/dto';
import { messageService } from '../../services/messageService';
import { commentService } from '../../services/commentService';
import { SystemMessageItem } from '../../components/business';
import { Text, EmptyState, ResponsiveContainer } from '../../components/common';
import { Text, EmptyState, ResponsiveContainer, AppBackButton } from '../../components/common';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { RootStackParamList } from '../../navigation/types';
import * as hrefs from '../../navigation/hrefs';
import { useMessageManagerSystemUnreadCount } from '../../stores';
import { messageManager } from '../../stores/messageManager';
@@ -48,7 +47,7 @@ const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_j
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
const isFocused = useIsFocused();
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const router = useRouter();
// 修复竞态条件:使用本地状态管理窗口尺寸,避免 hydration 问题
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
@@ -291,17 +290,17 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
) {
const postId = await resolvePostId(message);
if (postId) {
navigation.navigate('PostDetail', { postId });
router.push(hrefs.hrefPostDetail(postId));
}
} else if (system_type === 'follow') {
// 关注 - 跳转到用户主页
if (extra_data?.actor_id_str) {
navigation.navigate('UserProfile', { userId: extra_data.actor_id_str });
router.push(hrefs.hrefUserProfile(extra_data.actor_id_str));
}
} else if (system_type === 'group_join_apply') {
navigation.navigate('GroupRequestDetail', { message });
router.push(hrefs.hrefGroupRequestDetail(message));
} else if (system_type === 'group_invite') {
navigation.navigate('GroupInviteDetail', { message });
router.push(hrefs.hrefGroupInviteDetail(message));
}
// 其他类型暂不处理跳转
} catch (error) {
@@ -316,7 +315,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
const actorId = extra_data?.actor_id_str || (extra_data?.actor_id ? String(extra_data.actor_id) : null);
if (actorId) {
navigation.navigate('UserProfile', { userId: actorId });
router.push(hrefs.hrefUserProfile(actorId));
}
};
@@ -350,13 +349,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
return (
<View style={[styles.header, isWideScreen ? styles.headerWide : null]}>
{shouldShowBackButton ? (
<TouchableOpacity
style={styles.backButton}
onPress={onBack}
activeOpacity={0.7}
>
<MaterialCommunityIcons name="arrow-left" size={24} color={colors.text.primary} />
</TouchableOpacity>
<AppBackButton style={styles.backButton} onPress={onBack} />
) : (
<View style={styles.backButtonPlaceholder} />
)}

View File

@@ -14,8 +14,7 @@ import {
Switch,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { MaterialCommunityIcons } from '@expo/vector-icons';
import { colors, spacing, fontSizes, borderRadius, shadows } from '../../theme';
import { useAuthStore } from '../../stores';
@@ -30,16 +29,20 @@ import { messageManager } from '../../stores/messageManager';
import { userManager } from '../../stores/userManager';
import { Avatar, Text, Button, Loading, Divider } from '../../components/common';
import { User } from '../../types';
import { RootStackParamList, MessageStackParamList } from '../../navigation/types';
import { navigationService } from '../../infrastructure/navigation/navigationService';
type NavigationProp = NativeStackNavigationProp<MessageStackParamList>;
type PrivateChatInfoRouteProp = RouteProp<MessageStackParamList, 'PrivateChatInfo'>;
import * as hrefs from '../../navigation/hrefs';
const PrivateChatInfoScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const route = useRoute<PrivateChatInfoRouteProp>();
const { conversationId, userId, userName, userAvatar } = route.params;
const router = useRouter();
const raw = useLocalSearchParams<{
conversationId?: string;
userId?: string;
userName?: string;
userAvatar?: string;
}>();
const conversationId = raw.conversationId ?? '';
const userId = raw.userId ?? '';
const userName = raw.userName;
const userAvatar = raw.userAvatar;
const { currentUser } = useAuthStore();
// 用户信息状态
@@ -150,8 +153,7 @@ const PrivateChatInfoScreen: React.FC = () => {
// 查看用户资料
const handleViewProfile = () => {
// 使用导航服务跳转到用户资料页面
navigationService.navigate('UserProfile', { userId });
router.push(hrefs.hrefUserProfile(userId));
};
// 举报/投诉
@@ -219,7 +221,7 @@ const PrivateChatInfoScreen: React.FC = () => {
}
setIsBlocked(true);
messageManager.removeConversation(conversationId);
navigation.navigate('MessageList' as any);
router.replace(hrefs.hrefMessages());
Alert.alert('已拉黑', '你已成功拉黑该用户');
} catch (error) {
Alert.alert('错误', '拉黑失败,请稍后重试');
@@ -245,7 +247,7 @@ const PrivateChatInfoScreen: React.FC = () => {
await messageService.deleteConversationForSelf(conversationId);
messageManager.removeConversation(conversationId);
// 返回消息列表
navigation.navigate('MessageList' as any);
router.replace(hrefs.hrefMessages());
} catch (error) {
const msg = error instanceof ApiError ? error.message : '删除聊天失败';
Alert.alert('错误', msg);

View File

@@ -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>

View File

@@ -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;

View File

@@ -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,
},

View File

@@ -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',
},
};

View File

@@ -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: {

View File

@@ -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 {
// 状态

View File

@@ -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} />
)}