chore(deps): upgrade Expo SDK from 55 to 56
Some checks failed
Frontend CI / ota-android (push) Successful in 3m39s
Frontend CI / ota-ios (push) Successful in 3m40s
Frontend CI / build-and-push-web (push) Successful in 11m53s
Frontend CI / build-android-apk (push) Failing after 13m6s

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:
lafay
2026-06-01 22:56:37 +08:00
parent f39288f401
commit ad06881b85
21 changed files with 1893 additions and 2727 deletions

View File

@@ -7,11 +7,6 @@
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"scheme": "withyou",
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"infoPlist": {
@@ -36,7 +31,6 @@
"monochromeImage": "./assets/android-icon-monochrome.png"
},
"predictiveBackGestureEnabled": false,
"jsEngine": "hermes",
"package": "cn.qczlit.withyou",
"versionCode": 7,
"permissions": [
@@ -155,7 +149,8 @@
"channel": "developer-default"
}
],
"./plugins/withHuaweiPush"
"./plugins/withHuaweiPush",
"expo-status-bar"
],
"extra": {
"eas": {

View File

@@ -1,3 +1,4 @@
import '../src/polyfills';
import React, { useEffect, useRef, useState } from 'react';
import { AppState, AppStateStatus, Platform, View, ActivityIndicator } from 'react-native';
import { Stack, useRouter } from 'expo-router';

4225
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,59 +15,56 @@
},
"dependencies": {
"@expo/vector-icons": "^15.1.1",
"@livekit/react-native": "^2.11.0",
"@livekit/react-native-webrtc": "^144.1.0",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-navigation/bottom-tabs": "^7.15.2",
"@react-navigation/material-top-tabs": "^7.4.16",
"@react-navigation/native": "^7.1.31",
"@react-navigation/native-stack": "^7.14.2",
"@shopify/flash-list": "^2.3.1",
"@shopify/flash-list": "2.0.2",
"@tanstack/react-query": "^5.90.21",
"axios": "^1.13.6",
"date-fns": "^4.1.0",
"expo": "~55.0.4",
"expo-background-task": "~55.0.10",
"expo-camera": "^55.0.10",
"expo-constants": "~55.0.7",
"expo-dev-client": "~55.0.10",
"expo-file-system": "~55.0.10",
"expo-font": "~55.0.4",
"expo-haptics": "~55.0.8",
"expo-image": "^55.0.5",
"expo-image-picker": "^55.0.10",
"expo-intent-launcher": "~55.0.9",
"expo-linear-gradient": "^55.0.8",
"expo-media-library": "~55.0.9",
"expo-notifications": "~55.0.0",
"expo-router": "^55.0.4",
"expo-sqlite": "~55.0.10",
"expo-status-bar": "~55.0.4",
"expo-system-ui": "^55.0.9",
"expo-task-manager": "~55.0.9",
"expo-updates": "^55.0.12",
"expo-video": "^55.0.10",
"expo": "^56.0.8",
"expo-background-task": "~56.0.16",
"expo-camera": "~56.0.7",
"expo-constants": "~56.0.16",
"expo-dev-client": "~56.0.18",
"expo-file-system": "~56.0.7",
"expo-font": "~56.0.5",
"expo-haptics": "~56.0.3",
"expo-image": "~56.0.6",
"expo-image-picker": "~56.0.15",
"expo-intent-launcher": "~56.0.4",
"expo-linear-gradient": "~56.0.4",
"expo-linking": "~56.0.13",
"expo-media-library": "~56.0.6",
"expo-notifications": "~56.0.15",
"expo-router": "~56.2.8",
"expo-sqlite": "~56.0.4",
"expo-status-bar": "~56.0.4",
"expo-system-ui": "~56.0.5",
"expo-task-manager": "~56.0.16",
"expo-updates": "~56.0.17",
"expo-video": "~56.1.2",
"jcore-react-native": "^2.3.5",
"jpush-react-native": "^3.2.6",
"katex": "^0.16.42",
"livekit-client": "^2.19.0",
"markdown-it": "^14.1.1",
"pako": "^2.1.0",
"prismjs": "^1.30.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.2",
"react-native-gesture-handler": "^2.30.0",
"react-native-pager-view": "^8.0.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-native": "0.85.3",
"react-native-gesture-handler": "~2.31.1",
"react-native-pager-view": "8.0.1",
"react-native-paper": "^5.15.0",
"react-native-reanimated": "^4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-reanimated": "4.3.1",
"react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2",
"react-native-share": "^12.2.6",
"react-native-sse": "^1.2.1",
"react-native-web": "^0.21.0",
"@livekit/react-native": "^2.11.0",
"@livekit/react-native-webrtc": "^144.1.0",
"livekit-client": "^2.19.0",
"react-native-webview": "13.16.0",
"react-native-worklets": "0.7.2",
"react-native-webview": "13.16.1",
"react-native-worklets": "0.8.3",
"zod": "^4.3.6",
"zustand": "^5.0.11"
},
@@ -76,7 +73,12 @@
"@types/pako": "^2.0.4",
"@types/react": "~19.2.2",
"@types/react-native-vector-icons": "^6.4.18",
"typescript": "~5.9.2"
"typescript": "~6.0.3"
},
"private": true
"private": true,
"expo": {
"install": {
"exclude": ["expo-image"]
}
}
}

View File

@@ -0,0 +1,4 @@
plugins {
kotlin("jvm") version "2.1.20" apply false
id("java-gradle-plugin")
}

View File

@@ -0,0 +1,22 @@
[versions]
agp = "8.12.0"
gson = "2.8.9"
guava = "31.0.1-jre"
javapoet = "1.13.0"
junit = "4.13.2"
kotlin = "2.1.20"
assertj = "3.25.1"
ktfmt = "0.22.0"
[libraries]
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" }
junit = {module = "junit:junit", version.ref = "junit" }
assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" }

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -0,0 +1,83 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.ktfmt)
id("java-gradle-plugin")
}
repositories {
google()
mavenCentral()
}
gradlePlugin {
plugins {
create("react") {
id = "com.facebook.react"
implementationClass = "com.facebook.react.ReactPlugin"
}
create("reactrootproject") {
id = "com.facebook.react.rootproject"
implementationClass = "com.facebook.react.ReactRootProjectPlugin"
}
}
}
group = "com.facebook.react"
dependencies {
implementation(project(":shared"))
implementation(gradleApi())
// The KGP/AGP version is defined by React Native Gradle plugin.
// Therefore we specify an implementation dep rather than a compileOnly.
implementation(libs.kotlin.gradle.plugin)
implementation(libs.android.gradle.plugin)
implementation(libs.gson)
implementation(libs.guava)
implementation(libs.javapoet)
testImplementation(libs.junit)
testImplementation(libs.assertj)
testImplementation(project(":shared-testutil"))
}
// We intentionally don't build for Java 17 as users will see a cryptic bytecode version
// error first. Instead we produce a Java 11-compatible Gradle Plugin, so that AGP can print their
// nice message showing that JDK 11 (or 17) is required first
java { targetCompatibility = JavaVersion.VERSION_11 }
kotlin { jvmToolchain(17) }
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
apiVersion.set(KotlinVersion.KOTLIN_1_8)
// See comment above on JDK 11 support
jvmTarget.set(JvmTarget.JVM_11)
allWarningsAsErrors.set(
project.properties["enableWarningsAsErrors"]?.toString()?.toBoolean() ?: false
)
}
}
tasks.withType<Test>().configureEach {
testLogging {
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
}
}

View File

@@ -14,35 +14,35 @@ const withHuaweiPush = (config, options = {}) => {
config = withSettingsGradle(config, (config) => {
const contents = config.modResults.contents;
// Add Huawei Maven repo to pluginManagement.repositories for plugin resolution
// Add Huawei Maven repo to pluginManagement.repositories for buildscript resolution.
// NOTE: We intentionally do NOT add `plugins { id 'com.huawei.agconnect' ... }` here.
// The Huawei agcp plugin transitively depends on agconnect-apms-plugin:1.6.2.300,
// which depends on AGP 4.0.1. That old AGP references BuildCompletionListener (removed
// from Gradle 8.x+), causing NoClassDefFoundError at build time. Instead, the plugin
// is applied via buildscript classpath in build.gradle with the apms-plugin excluded.
if (!contents.includes('developer.huawei.com/repo')) {
if (contents.includes('pluginManagement')) {
const repoLine = ` maven { url 'https://developer.huawei.com/repo/' }`;
const hasPluginManagement = contents.includes('pluginManagement');
const hasRepositoriesInPM = /pluginManagement\s*\{[\s\S]*?repositories\s*\{/.test(contents);
if (hasRepositoriesInPM) {
config.modResults.contents = contents.replace(
/(pluginManagement\s*\{[\s\S]*?repositories\s*\{)/,
`$1\n maven { url 'https://developer.huawei.com/repo/' }`
`$1\n ${repoLine}`
);
} else if (hasPluginManagement) {
config.modResults.contents = contents.replace(
/(pluginManagement\s*\{)/,
`$1\n repositories {\n gradlePluginPortal()\n google()\n mavenCentral()\n ${repoLine}\n }`
);
} else {
// No pluginManagement block at all — create one with the Huawei repo
config.modResults.contents =
`pluginManagement {\n repositories {\n maven { url 'https://developer.huawei.com/repo/' }\n }\n}\n\n` +
`pluginManagement {\n repositories {\n ${repoLine}\n }\n}\n\n` +
config.modResults.contents;
}
}
// Map the plugin ID to its Maven artifact via resolutionStrategy.
// AGConnect is not published as a standard Gradle plugin marker, so Gradle 9+
// cannot resolve it by plugin ID alone. This mapping tells Gradle where to find it.
if (!config.modResults.contents.includes('com.huawei.agconnect:agcp')) {
const pmMatch = config.modResults.contents.match(/pluginManagement\s*\{/);
if (pmMatch) {
config.modResults.contents = config.modResults.contents.replace(
/(pluginManagement\s*\{)/,
`$1\n resolutionStrategy {\n eachPlugin {\n if (requested.id.id == 'com.huawei.agconnect') {\n useModule('com.huawei.agconnect:agcp:1.9.1.301')\n }\n }\n }`
);
}
}
// Ensure Huawei Maven repo also appears in dependencyResolutionManagement (Gradle 9+)
// Ensure Huawei Maven repo in dependencyResolutionManagement (Gradle 9+)
if (!config.modResults.contents.match(/dependencyResolutionManagement[\s\S]*developer\.huawei\.com\/repo/)) {
const depMgmtPattern = /dependencyResolutionManagement\s*\{[\s\S]*?repositories\s*\{/;
if (depMgmtPattern.test(config.modResults.contents)) {
@@ -53,41 +53,6 @@ const withHuaweiPush = (config, options = {}) => {
}
}
// Declare the plugin in a top-level plugins {} block (outside pluginManagement).
// In Gradle 9+, plugins declared with `apply false` in settings.gradle are
// available to all subprojects via `apply plugin:` or the plugins DSL.
if (!config.modResults.contents.includes("id 'com.huawei.agconnect'")) {
const pluginsBlockPattern = /^plugins\s*\{/m;
if (pluginsBlockPattern.test(config.modResults.contents)) {
// Add to existing top-level plugins block
config.modResults.contents = config.modResults.contents.replace(
/^plugins\s*\{/m,
`plugins {\n id 'com.huawei.agconnect' version '1.9.1.301' apply false`
);
} else {
// Insert a top-level plugins block after pluginManagement closing brace,
// or at the very beginning if no pluginManagement block exists.
const pmClosePattern = /\n\}\n/;
if (pmClosePattern.test(config.modResults.contents)) {
const firstClosing = config.modResults.contents.indexOf('}', config.modResults.contents.indexOf('pluginManagement')) + 1;
if (firstClosing > 0) {
const insertPos = firstClosing;
config.modResults.contents =
config.modResults.contents.slice(0, insertPos) +
`\n\nplugins {\n id 'com.huawei.agconnect' version '1.9.1.301' apply false\n}` +
config.modResults.contents.slice(insertPos);
}
} else {
// Fallback: prepend after pluginManagement block
config.modResults.contents =
config.modResults.contents.replace(
/(pluginManagement\s*\{[\s\S]*?\})/,
`$1\n\nplugins {\n id 'com.huawei.agconnect' version '1.9.1.301' apply false\n}`
);
}
}
}
return config;
});
@@ -109,18 +74,29 @@ const withHuaweiPush = (config, options = {}) => {
config = withProjectBuildGradle(config, (config) => {
const contents = config.modResults.contents;
// Add Huawei AGConnect classpath with apms-plugin excluded.
// The apms-plugin transitively depends on AGP 4.0.1 which is incompatible
// with Gradle 8.14+ (references removed BuildCompletionListener class).
if (!contents.includes('com.huawei.agconnect:agcp')) {
if (contents.includes('buildscript')) {
const depsPattern = /(buildscript\s*\{[\s\S]*?dependencies\s*\{)/;
if (depsPattern.test(contents)) {
config.modResults.contents = contents.replace(
depsPattern,
`$1\n classpath 'com.huawei.agconnect:agcp:1.9.1.301'`
`$1\n classpath('com.huawei.agconnect:agcp:1.9.1.301') {\n exclude group: 'com.huawei.agconnect', module: 'agconnect-apms-plugin'\n }`
);
}
}
}
// Set explicit AGP version (needed by Huawei plugin validation)
if (!contents.includes('com.android.tools.build:gradle:8.12.0')) {
config.modResults.contents = config.modResults.contents.replace(
/classpath\(['"]com\.android\.tools\.build:gradle['"]\)/,
"classpath('com.android.tools.build:gradle:8.12.0')"
);
}
if (!config.modResults.contents.includes('developer.huawei.com/repo')) {
const bsRepoPattern = /(buildscript\s*\{[\s\S]*?repositories\s*\{)/;
if (bsRepoPattern.test(config.modResults.contents)) {
@@ -187,6 +163,29 @@ const withHuaweiPush = (config, options = {}) => {
console.warn('[withHuaweiPush] agconnect-services.json not found at project root');
}
// RN 0.85.3 ships Gradle 9.3.1 in its template, but Kotlin 2.1.20 is incompatible
// with Gradle 9.x (CompilerEnvironment ClassNotFoundException). Downgrade to 8.14.5.
const wrapperFile = path.join(config.modRequest.platformProjectRoot, 'gradle', 'wrapper', 'gradle-wrapper.properties');
if (fs.existsSync(wrapperFile)) {
let wrapper = fs.readFileSync(wrapperFile, 'utf8');
if (wrapper.includes('gradle-9.')) {
wrapper = wrapper.replace(/gradle-[\d.]+-bin\.zip/, 'gradle-8.14.5-bin.zip');
fs.writeFileSync(wrapperFile, wrapper);
console.log('[withHuaweiPush] downgraded Gradle to 8.14.5 for Kotlin 2.1.20 compatibility');
}
}
// Ensure local.properties with SDK path exists (gets cleared on prebuild --clean)
const localPropsFile = path.join(config.modRequest.platformProjectRoot, 'local.properties');
if (!fs.existsSync(localPropsFile)) {
const homeDir = process.env.USERPROFILE || process.env.HOME || '';
const sdkPath = path.join(homeDir, 'AppData', 'Local', 'Android', 'Sdk');
if (fs.existsSync(sdkPath)) {
fs.writeFileSync(localPropsFile, `sdk.dir=${sdkPath.replace(/\\/g, '\\\\')}\n`);
console.log('[withHuaweiPush] created local.properties with SDK path');
}
}
return config;
},
]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import '../../polyfills';
import { Room, RoomEvent, Track, ConnectionState, LocalParticipant, RemoteParticipant, RemoteTrackPublication, TrackPublication, Participant as LKParticipant } from 'livekit-client';
export interface LiveKitServiceConfig {

View File

@@ -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();
}

View File

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