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
app.json
9
app.json
@@ -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": {
|
||||
|
||||
@@ -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
4225
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
4
package/android/expo-gradle-plugin/build.gradle.kts
Normal file
4
package/android/expo-gradle-plugin/build.gradle.kts
Normal file
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "2.1.20" apply false
|
||||
id("java-gradle-plugin")
|
||||
}
|
||||
22
package/gradle/libs.versions.toml
Normal file
22
package/gradle/libs.versions.toml
Normal 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" }
|
||||
7
package/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
package/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||
83
package/react-native-gradle-plugin/build.gradle.kts
Normal file
83
package/react-native-gradle-plugin/build.gradle.kts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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