chore(deps): upgrade Expo SDK from 55 to 56
Migrate from @react-navigation/native to expo-router navigation hooks across all screens. Add polyfills module and apply LiveKit VideoView optional loading for Expo Go compatibility. Improve background sync to skip when user is not logged in. Enhance fetchUnreadCount to return typed totalUnread and systemUnread values.
This commit is contained in:
@@ -9,7 +9,13 @@ import {
|
||||
} from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Track, LocalVideoTrack, RemoteVideoTrack } from 'livekit-client';
|
||||
import { VideoView } from '@livekit/react-native';
|
||||
|
||||
let VideoView: React.ComponentType<any> | null = null;
|
||||
try {
|
||||
VideoView = require('@livekit/react-native').VideoView;
|
||||
} catch {
|
||||
// WebRTC native module not available (e.g. Expo Go)
|
||||
}
|
||||
|
||||
type VideoTrack = LocalVideoTrack | RemoteVideoTrack;
|
||||
import { callStore } from '../../stores/call';
|
||||
@@ -126,7 +132,7 @@ const CallScreen: React.FC = () => {
|
||||
<View style={styles.background} />
|
||||
|
||||
{/* Remote video - full screen */}
|
||||
{showRemoteVideo && remoteVideoTrack && (
|
||||
{showRemoteVideo && remoteVideoTrack && VideoView && (
|
||||
<VideoView
|
||||
videoTrack={remoteVideoTrack}
|
||||
style={styles.fullScreenVideo}
|
||||
@@ -135,7 +141,7 @@ const CallScreen: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Local video - picture in picture */}
|
||||
{showLocalVideo && localVideoTrack && (
|
||||
{showLocalVideo && localVideoTrack && VideoView && (
|
||||
<View style={styles.localVideoContainer}>
|
||||
<VideoView
|
||||
videoTrack={localVideoTrack}
|
||||
|
||||
@@ -9,7 +9,14 @@ import {
|
||||
} from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { Track, LocalVideoTrack, RemoteVideoTrack } from 'livekit-client';
|
||||
import { VideoView } from '@livekit/react-native';
|
||||
|
||||
let VideoView: React.ComponentType<any> | null = null;
|
||||
try {
|
||||
VideoView = require('@livekit/react-native').VideoView;
|
||||
} catch {
|
||||
// @livekit/react-native not available on web
|
||||
}
|
||||
|
||||
import { callStore } from '../../stores/call';
|
||||
import { liveKitService } from '@/services/livekit';
|
||||
|
||||
@@ -119,7 +126,7 @@ const CallScreen: React.FC = () => {
|
||||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||||
<View style={styles.background} />
|
||||
|
||||
{showRemoteVideo && remoteVideoTrack && (
|
||||
{showRemoteVideo && remoteVideoTrack && VideoView && (
|
||||
<VideoView
|
||||
videoTrack={remoteVideoTrack}
|
||||
style={styles.fullScreenVideo}
|
||||
@@ -127,7 +134,7 @@ const CallScreen: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showLocalVideo && localVideoTrack && (
|
||||
{showLocalVideo && localVideoTrack && VideoView && (
|
||||
<View style={styles.localVideoContainer}>
|
||||
<VideoView
|
||||
videoTrack={localVideoTrack}
|
||||
|
||||
8
src/polyfills.ts
Normal file
8
src/polyfills.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
if (typeof globalThis.DOMException === 'undefined') {
|
||||
(globalThis as any).DOMException = class DOMException extends Error {
|
||||
constructor(message?: string, name?: string) {
|
||||
super(message);
|
||||
this.name = name || 'DOMException';
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useFocusEffect } from "expo-router/react-navigation";
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
|
||||
@@ -26,8 +26,8 @@ import {
|
||||
import { FlashList, ListRenderItem } from '@shopify/flash-list';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import { useIsFocused } from "expo-router/react-navigation";
|
||||
import { useBottomTabBarHeight } from "expo-router/js-tabs";
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import {
|
||||
spacing,
|
||||
@@ -842,25 +842,25 @@ export const MessageListScreen: React.FC = () => {
|
||||
<View style={styles.chatArea}>
|
||||
{showNotifications ? (
|
||||
// 显示系统通知页面
|
||||
<NotificationsScreen onBack={stableCloseNotifications} />
|
||||
(<NotificationsScreen onBack={stableCloseNotifications} />)
|
||||
) : selectedConversation ? (
|
||||
// 显示选中会话的聊天内容
|
||||
<ChatScreen
|
||||
(<ChatScreen
|
||||
isEmbedded
|
||||
embeddedConversationId={String(selectedConversation.id)}
|
||||
embeddedIsGroupChat={selectedConversation.type === 'group'}
|
||||
embeddedGroupId={selectedConversation.group ? String(selectedConversation.group.id) : undefined}
|
||||
embeddedGroupName={selectedConversation.group?.name}
|
||||
onEmbeddedBack={stableClearSelectedConversation}
|
||||
/>
|
||||
/>)
|
||||
) : (
|
||||
// 默认占位符
|
||||
<View style={styles.chatPlaceholder}>
|
||||
(<View style={styles.chatPlaceholder}>
|
||||
<View style={styles.chatPlaceholderContent}>
|
||||
<MaterialCommunityIcons name="message-text-outline" size={64} color={colors.chat.iconMuted} />
|
||||
<Text style={styles.chatPlaceholderText}>选择一个会话开始聊天</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>)
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
@@ -873,7 +873,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||
{showNotifications ? (
|
||||
// 显示系统通知页面,传入 onBack 回调
|
||||
<NotificationsScreen onBack={stableCloseNotifications} />
|
||||
(<NotificationsScreen onBack={stableCloseNotifications} />)
|
||||
) : isSearchMode ? (
|
||||
renderSearchMode()
|
||||
) : isWideScreen ? (
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from 'react-native';
|
||||
import { FlashList, ListRenderItem } from '@shopify/flash-list';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useIsFocused } from '@react-navigation/native';
|
||||
import { useIsFocused } from "expo-router/react-navigation";
|
||||
import { useRouter } from 'expo-router';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { spacing, borderRadius, useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useNavigation } from "expo-router/react-navigation";
|
||||
import { useRouter } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
|
||||
@@ -23,7 +23,7 @@ import { runOnJS } from 'react-native-reanimated';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { blurActiveElement } from '../../infrastructure/platform';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useFocusEffect } from "expo-router/react-navigation";
|
||||
import { useRouter } from 'expo-router';
|
||||
import {
|
||||
fontSizes,
|
||||
|
||||
@@ -50,6 +50,11 @@ TaskManager.defineTask(BACKGROUND_SYNC_TASK, async () => {
|
||||
* 执行消息同步
|
||||
*/
|
||||
async function syncMessages(): Promise<void> {
|
||||
// 未登录时跳过同步,避免报"登录已过期"错误
|
||||
const token = await api.getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 同步会话未读数
|
||||
const unreadData = await messageService.getUnreadCount();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import '../../polyfills';
|
||||
import { Room, RoomEvent, Track, ConnectionState, LocalParticipant, RemoteParticipant, RemoteTrackPublication, TrackPublication, Participant as LKParticipant } from 'livekit-client';
|
||||
|
||||
export interface LiveKitServiceConfig {
|
||||
|
||||
@@ -208,7 +208,7 @@ class MessageManager {
|
||||
return this.syncService.loadMoreMessages(conversationId, beforeSeq, limit);
|
||||
}
|
||||
|
||||
async fetchUnreadCount(): Promise<void> {
|
||||
async fetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> {
|
||||
return this.syncService.fetchUnreadCount();
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
/** 正在加载会话列表下一页 */
|
||||
private loadingMoreConversations = false;
|
||||
/** fetchUnreadCount 去重:复用 in-flight promise */
|
||||
private fetchUnreadCountPromise: Promise<void> | null = null;
|
||||
private fetchUnreadCountPromise: Promise<{ totalUnread: number; systemUnread: number }> | null = null;
|
||||
|
||||
constructor(
|
||||
getCurrentUserId: () => string | null,
|
||||
@@ -352,19 +352,20 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
/**
|
||||
* 获取未读数(去重:复用 in-flight promise)
|
||||
*/
|
||||
async fetchUnreadCount(): Promise<void> {
|
||||
async fetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> {
|
||||
if (this.fetchUnreadCountPromise) {
|
||||
return this.fetchUnreadCountPromise;
|
||||
}
|
||||
this.fetchUnreadCountPromise = this.doFetchUnreadCount();
|
||||
try {
|
||||
await this.fetchUnreadCountPromise;
|
||||
const result = await this.fetchUnreadCountPromise;
|
||||
return result;
|
||||
} finally {
|
||||
this.fetchUnreadCountPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async doFetchUnreadCount(): Promise<void> {
|
||||
private async doFetchUnreadCount(): Promise<{ totalUnread: number; systemUnread: number }> {
|
||||
const store = useMessageStore.getState();
|
||||
|
||||
try {
|
||||
@@ -392,8 +393,10 @@ export class MessageSyncService implements IMessageSyncService {
|
||||
|
||||
// 服务端汇总未读为 0:不主动清零本地未读,等 fetchConversations 提供权威数据
|
||||
// 避免后端缓存竞态导致的未读数抖动
|
||||
return { totalUnread, systemUnread };
|
||||
} catch (error) {
|
||||
console.error('[MessageSyncService] 获取未读数失败:', error);
|
||||
return { totalUnread: 0, systemUnread: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user