feat(notification): integrate expo-notifications for permission handling and deep linking
- Add expo-notifications dependency and configure with JPush plugin - Request notification permission on app launch for iOS/Android - Handle notification taps to navigate to chat or notifications screen - Add UI to check and request system notification permission in settings - Refactor background sync: WebSocket disconnects in background, JPush handles push - Add checkNotificationPermission() and requestNotificationPermission() to jpushService
This commit is contained in:
11
app.json
11
app.json
@@ -58,7 +58,8 @@
|
|||||||
"android.permission.READ_MEDIA_VIDEO",
|
"android.permission.READ_MEDIA_VIDEO",
|
||||||
"android.permission.READ_MEDIA_AUDIO",
|
"android.permission.READ_MEDIA_AUDIO",
|
||||||
"android.permission.ACCESS_NETWORK_STATE",
|
"android.permission.ACCESS_NETWORK_STATE",
|
||||||
"android.permission.ACCESS_WIFI_STATE"
|
"android.permission.ACCESS_WIFI_STATE",
|
||||||
|
"android.permission.POST_NOTIFICATIONS"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
@@ -136,6 +137,14 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-font",
|
"expo-font",
|
||||||
|
[
|
||||||
|
"expo-notifications",
|
||||||
|
{
|
||||||
|
"color": "#E6F4FE",
|
||||||
|
"defaultChannel": "default",
|
||||||
|
"enableBackgroundRemoteNotifications": true
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"./plugins/withJPush",
|
"./plugins/withJPush",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ function SessionGate({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
function NotificationBootstrap() {
|
function NotificationBootstrap() {
|
||||||
const appState = useRef<AppStateStatus>(AppState.currentState);
|
const appState = useRef<AppStateStatus>(AppState.currentState);
|
||||||
|
const permissionRequested = useRef(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initNotifications = async () => {
|
const initNotifications = async () => {
|
||||||
@@ -145,9 +147,24 @@ function NotificationBootstrap() {
|
|||||||
appState.current = nextAppState;
|
appState.current = nextAppState;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for JPush notification taps
|
// Listen for JPush notification taps — navigate to the relevant screen
|
||||||
jpushService.onNotification((message) => {
|
jpushService.onNotification((message) => {
|
||||||
console.log('[NotificationBootstrap] JPush notification tapped:', message);
|
console.log('[NotificationBootstrap] JPush notification:', message.notificationEventType, message);
|
||||||
|
if (message.notificationEventType !== 'notificationOpened') return;
|
||||||
|
|
||||||
|
const extras = message.extras || {};
|
||||||
|
const notifType = extras.notification_type || message.notificationType;
|
||||||
|
|
||||||
|
if (notifType === 'chat_message' && extras.conversation_id) {
|
||||||
|
const isGroup = extras.conversation_type === 'group';
|
||||||
|
router.push(
|
||||||
|
`/chat/${encodeURIComponent(extras.conversation_id)}${
|
||||||
|
isGroup ? '?isGroupChat=1' : extras.sender_id ? `?userId=${encodeURIComponent(extras.sender_id)}` : ''
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
router.push('/messages/notifications');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -155,7 +172,24 @@ function NotificationBootstrap() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const requestPermissionOnLaunch = async () => {
|
||||||
|
if (permissionRequested.current) return;
|
||||||
|
permissionRequested.current = true;
|
||||||
|
|
||||||
|
if (Platform.OS === 'web') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hasPermission = await jpushService.checkNotificationPermission();
|
||||||
|
if (!hasPermission) {
|
||||||
|
await jpushService.requestNotificationPermission();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[NotificationBootstrap] request permission on launch failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
initNotifications();
|
initNotifications();
|
||||||
|
requestPermissionOnLaunch();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
55
package-lock.json
generated
55
package-lock.json
generated
@@ -31,6 +31,7 @@
|
|||||||
"expo-intent-launcher": "~55.0.9",
|
"expo-intent-launcher": "~55.0.9",
|
||||||
"expo-linear-gradient": "^55.0.8",
|
"expo-linear-gradient": "^55.0.8",
|
||||||
"expo-media-library": "~55.0.9",
|
"expo-media-library": "~55.0.9",
|
||||||
|
"expo-notifications": "~55.0.0",
|
||||||
"expo-router": "^55.0.4",
|
"expo-router": "^55.0.4",
|
||||||
"expo-sqlite": "~55.0.10",
|
"expo-sqlite": "~55.0.10",
|
||||||
"expo-status-bar": "~55.0.4",
|
"expo-status-bar": "~55.0.4",
|
||||||
@@ -1678,17 +1679,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/image-utils": {
|
"node_modules/@expo/image-utils": {
|
||||||
"version": "0.8.12",
|
"version": "0.8.13",
|
||||||
"resolved": "https://registry.npmmirror.com/@expo/image-utils/-/image-utils-0.8.12.tgz",
|
"resolved": "https://registry.npmmirror.com/@expo/image-utils/-/image-utils-0.8.13.tgz",
|
||||||
"integrity": "sha512-3KguH7kyKqq7pNwLb9j6BBdD/bjmNwXZG/HPWT6GWIXbwrvAJt2JNyYTP5agWJ8jbbuys1yuCzmkX+TU6rmI7A==",
|
"integrity": "sha512-1I//yBQeTY6p0u1ihqGNDAr35EbSG8uFEupFrIF0jd++h9EWH33521yZJU1yE+mwGlzCb61g3ehu78siMhXBlA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@expo/require-utils": "^55.0.4",
|
||||||
"@expo/spawn-async": "^1.7.2",
|
"@expo/spawn-async": "^1.7.2",
|
||||||
"chalk": "^4.0.0",
|
"chalk": "^4.0.0",
|
||||||
"getenv": "^2.0.0",
|
"getenv": "^2.0.0",
|
||||||
"jimp-compact": "0.16.1",
|
"jimp-compact": "0.16.1",
|
||||||
"parse-png": "^2.1.0",
|
"parse-png": "^2.1.0",
|
||||||
"resolve-from": "^5.0.0",
|
|
||||||
"semver": "^7.6.0"
|
"semver": "^7.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2053,9 +2054,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@expo/require-utils": {
|
"node_modules/@expo/require-utils": {
|
||||||
"version": "55.0.3",
|
"version": "55.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/@expo/require-utils/-/require-utils-55.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/@expo/require-utils/-/require-utils-55.0.4.tgz",
|
||||||
"integrity": "sha512-TS1m5tW45q4zoaTlt6DwmdYHxvFTIxoLrTHKOFrIirHIqIXnHCzpceg8wumiBi+ZXSaGY2gobTbfv+WVhJY6Fw==",
|
"integrity": "sha512-JAANvXqV7MOysWeVWgaiDzikoyDjJWOV/ulOW60Zb3kXJfrx2oZOtGtDXDFKD1mXuahQgoM5QOjuZhF7gFRNjA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.20.0",
|
"@babel/code-frame": "^7.20.0",
|
||||||
@@ -4358,6 +4359,12 @@
|
|||||||
"@babel/core": "^7.0.0"
|
"@babel/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/badgin": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/badgin/-/badgin-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||||
@@ -5517,6 +5524,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-application": {
|
||||||
|
"version": "55.0.14",
|
||||||
|
"resolved": "https://registry.npmmirror.com/expo-application/-/expo-application-55.0.14.tgz",
|
||||||
|
"integrity": "sha512-NgqDIt3eCf4aVLp1L6AcEanCYoyJeuBsGrgGSzOIvxAsOvp5X3SYKW3ROgpKUnLQEKMWlzwETpjsUGszcqkk8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-asset": {
|
"node_modules/expo-asset": {
|
||||||
"version": "55.0.10",
|
"version": "55.0.10",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-asset/-/expo-asset-55.0.10.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-asset/-/expo-asset-55.0.10.tgz",
|
||||||
@@ -5565,12 +5581,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
"version": "55.0.9",
|
"version": "55.0.15",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-constants/-/expo-constants-55.0.9.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-constants/-/expo-constants-55.0.15.tgz",
|
||||||
"integrity": "sha512-iBiXjZeuU5S/8docQeNzsVvtDy4w0zlmXBpFEi1ypwugceEpdQQab65TVRbusXAcwpNVxCPMpNlDssYp0Pli2g==",
|
"integrity": "sha512-w394fcZLJjeKN+9ZnJzL/HiarE1nwZFDa+3S9frevh6Ur+MAAs9QDrcXhDrV8T3xqRzzYaqsP6Z8TFZ4efWN1A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/config": "~55.0.10",
|
|
||||||
"@expo/env": "~2.1.1"
|
"@expo/env": "~2.1.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -5831,6 +5846,24 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-notifications": {
|
||||||
|
"version": "55.0.20",
|
||||||
|
"resolved": "https://registry.npmmirror.com/expo-notifications/-/expo-notifications-55.0.20.tgz",
|
||||||
|
"integrity": "sha512-ENwHZtr2ApR4VaqwwYluEi+ocip2rIkZfHQVi263fZXW3WWVuPa+VxWKtT0KLcvWYGld8lEqwAHWmFWPS6aG7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/image-utils": "^0.8.13",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"badgin": "^1.1.5",
|
||||||
|
"expo-application": "~55.0.14",
|
||||||
|
"expo-constants": "~55.0.15"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-router": {
|
"node_modules/expo-router": {
|
||||||
"version": "55.0.7",
|
"version": "55.0.7",
|
||||||
"resolved": "https://registry.npmmirror.com/expo-router/-/expo-router-55.0.7.tgz",
|
"resolved": "https://registry.npmmirror.com/expo-router/-/expo-router-55.0.7.tgz",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"expo-intent-launcher": "~55.0.9",
|
"expo-intent-launcher": "~55.0.9",
|
||||||
"expo-linear-gradient": "^55.0.8",
|
"expo-linear-gradient": "^55.0.8",
|
||||||
"expo-media-library": "~55.0.9",
|
"expo-media-library": "~55.0.9",
|
||||||
|
"expo-notifications": "~55.0.0",
|
||||||
"expo-router": "^55.0.4",
|
"expo-router": "^55.0.4",
|
||||||
"expo-sqlite": "~55.0.10",
|
"expo-sqlite": "~55.0.10",
|
||||||
"expo-status-bar": "~55.0.4",
|
"expo-status-bar": "~55.0.4",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
Alert,
|
Alert,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
|
Platform,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
setPushNotificationsPreference,
|
setPushNotificationsPreference,
|
||||||
setSoundPreference,
|
setSoundPreference,
|
||||||
setVibrationPreference,
|
setVibrationPreference,
|
||||||
|
jpushService,
|
||||||
} from '@/services/notification';
|
} from '@/services/notification';
|
||||||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||||
import { backgroundSyncManager, BackgroundSyncMode } from '@/services/background';
|
import { backgroundSyncManager, BackgroundSyncMode } from '@/services/background';
|
||||||
@@ -45,6 +47,7 @@ export const NotificationSettingsScreen: React.FC = () => {
|
|||||||
const [pushEnabled, setPushEnabled] = useState(true);
|
const [pushEnabled, setPushEnabled] = useState(true);
|
||||||
const [soundEnabled, setSoundEnabled] = useState(true);
|
const [soundEnabled, setSoundEnabled] = useState(true);
|
||||||
const [syncMode, setSyncMode] = useState<BackgroundSyncMode>(BackgroundSyncMode.BATTERY_SAVER);
|
const [syncMode, setSyncMode] = useState<BackgroundSyncMode>(BackgroundSyncMode.BATTERY_SAVER);
|
||||||
|
const [systemPushEnabled, setSystemPushEnabled] = useState<boolean | null>(null);
|
||||||
const { isWideScreen, isMobile } = useResponsive();
|
const { isWideScreen, isMobile } = useResponsive();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
@@ -60,6 +63,11 @@ export const NotificationSettingsScreen: React.FC = () => {
|
|||||||
setPushEnabled(prefs.pushEnabled);
|
setPushEnabled(prefs.pushEnabled);
|
||||||
setSoundEnabled(prefs.soundEnabled);
|
setSoundEnabled(prefs.soundEnabled);
|
||||||
setSyncMode(backgroundSyncManager.getMode());
|
setSyncMode(backgroundSyncManager.getMode());
|
||||||
|
|
||||||
|
if (Platform.OS !== 'web') {
|
||||||
|
const enabled = await jpushService.checkNotificationPermission();
|
||||||
|
setSystemPushEnabled(enabled);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载通知设置失败:', error);
|
console.error('加载通知设置失败:', error);
|
||||||
}
|
}
|
||||||
@@ -139,6 +147,21 @@ export const NotificationSettingsScreen: React.FC = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleRequestSystemPermission = async () => {
|
||||||
|
if (Platform.OS === 'web') return;
|
||||||
|
const granted = await jpushService.requestNotificationPermission();
|
||||||
|
setSystemPushEnabled(granted);
|
||||||
|
if (!granted) {
|
||||||
|
Alert.alert(
|
||||||
|
'通知权限未开启',
|
||||||
|
'请在系统设置中手动开启通知权限,以便及时接收消息提醒。',
|
||||||
|
[
|
||||||
|
{ text: '知道了', style: 'cancel' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const settings: NotificationSettingItem[] = [
|
const settings: NotificationSettingItem[] = [
|
||||||
{
|
{
|
||||||
key: 'push',
|
key: 'push',
|
||||||
@@ -207,6 +230,23 @@ export const NotificationSettingsScreen: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 系统通知权限状态(仅移动端) */}
|
||||||
|
{Platform.OS !== 'web' && systemPushEnabled === false && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.tipContainer, { backgroundColor: colors.error.main + '14' }]}
|
||||||
|
onPress={handleRequestSystemPermission}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="alert-circle-outline" size={16} color={colors.error.main} />
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text variant="caption" color={colors.error.main} style={styles.tipText}>
|
||||||
|
系统通知权限未开启,点击前往开启
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<MaterialCommunityIcons name="chevron-right" size={16} color={colors.error.main} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 提示信息 */}
|
{/* 提示信息 */}
|
||||||
<View style={styles.tipContainer}>
|
<View style={styles.tipContainer}>
|
||||||
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.hint} />
|
<MaterialCommunityIcons name="information-outline" size={16} color={colors.text.hint} />
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* 后台同步管理器
|
* 后台同步管理器
|
||||||
*
|
*
|
||||||
* 整合前台服务、后台同步、消息状态管理
|
* 后台推送策略:
|
||||||
* 参考 Element Android 的 BackgroundSyncStarter 设计
|
* - 前台:WebSocket 实时通信
|
||||||
|
* - 后台:断开 WebSocket,由 JPush 负责推送通知
|
||||||
|
* - 回到前台:重连 WebSocket + 同步离线消息
|
||||||
|
*
|
||||||
|
* 前台服务仅用于通话保活,不再用于消息同步。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AppState, AppStateStatus, Platform } from 'react-native';
|
import { AppState, AppStateStatus, Platform } from 'react-native';
|
||||||
import { ForegroundServiceModule } from './ForegroundServiceModule';
|
import { ForegroundServiceModule } from './ForegroundServiceModule';
|
||||||
import { wsService } from '../core/wsService';
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台同步模式
|
* 后台同步模式
|
||||||
*/
|
*/
|
||||||
export enum BackgroundSyncMode {
|
export enum BackgroundSyncMode {
|
||||||
/** 省电模式:使用 expo-background-task(最小15分钟间隔) */
|
/** 省电模式:仅依赖 JPush 推送 */
|
||||||
BATTERY_SAVER = 'battery_saver',
|
BATTERY_SAVER = 'battery_saver',
|
||||||
|
|
||||||
/** 实时模式:前台服务保活 + 持续同步 */
|
/** 实时模式:前台服务保活(仅用于通话) */
|
||||||
REALTIME = 'realtime',
|
REALTIME = 'realtime',
|
||||||
|
|
||||||
/** 禁用后台同步 */
|
/** 禁用后台同步 */
|
||||||
@@ -25,12 +28,10 @@ export enum BackgroundSyncMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const STORAGE_KEY = 'background_sync_mode';
|
const STORAGE_KEY = 'background_sync_mode';
|
||||||
const SYNC_INTERVAL_MS = 30 * 1000; // 30 秒同步间隔
|
|
||||||
|
|
||||||
class BackgroundSyncManager {
|
class BackgroundSyncManager {
|
||||||
private mode: BackgroundSyncMode = BackgroundSyncMode.BATTERY_SAVER;
|
private mode: BackgroundSyncMode = BackgroundSyncMode.BATTERY_SAVER;
|
||||||
private appStateSubscription: ReturnType<typeof AppState.addEventListener> | null = null;
|
private appStateSubscription: ReturnType<typeof AppState.addEventListener> | null = null;
|
||||||
private syncInterval: ReturnType<typeof setInterval> | null = null;
|
|
||||||
private isInitialized: boolean = false;
|
private isInitialized: boolean = false;
|
||||||
private lastSyncAt: number = 0;
|
private lastSyncAt: number = 0;
|
||||||
private syncMessagesCallback: (() => Promise<void>) | null = null;
|
private syncMessagesCallback: (() => Promise<void>) | null = null;
|
||||||
@@ -49,19 +50,14 @@ class BackgroundSyncManager {
|
|||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
if (this.isInitialized) return;
|
if (this.isInitialized) return;
|
||||||
|
|
||||||
// 加载保存的模式
|
|
||||||
const savedMode = await this.loadMode();
|
const savedMode = await this.loadMode();
|
||||||
this.mode = savedMode;
|
this.mode = savedMode;
|
||||||
|
|
||||||
// 监听 AppState 变化
|
|
||||||
this.appStateSubscription = AppState.addEventListener(
|
this.appStateSubscription = AppState.addEventListener(
|
||||||
'change',
|
'change',
|
||||||
this.handleAppStateChange
|
this.handleAppStateChange
|
||||||
);
|
);
|
||||||
|
|
||||||
// 监听 WebSocket 断开
|
|
||||||
this.setupWSDisconnectListener();
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
console.log('[BackgroundSyncManager] 初始化完成, 模式:', this.mode);
|
console.log('[BackgroundSyncManager] 初始化完成, 模式:', this.mode);
|
||||||
}
|
}
|
||||||
@@ -70,31 +66,19 @@ class BackgroundSyncManager {
|
|||||||
* 切换后台同步模式
|
* 切换后台同步模式
|
||||||
*/
|
*/
|
||||||
async setMode(mode: BackgroundSyncMode): Promise<void> {
|
async setMode(mode: BackgroundSyncMode): Promise<void> {
|
||||||
// 如果当前在后台,先停止旧模式的保活
|
if (AppState.currentState !== 'active' && this.mode === BackgroundSyncMode.REALTIME) {
|
||||||
if (AppState.currentState !== 'active') {
|
|
||||||
this.stopPeriodicSync();
|
|
||||||
if (this.mode === BackgroundSyncMode.REALTIME) {
|
|
||||||
await ForegroundServiceModule.stop();
|
await ForegroundServiceModule.stop();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 更新模式
|
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
await this.saveMode(mode);
|
await this.saveMode(mode);
|
||||||
|
|
||||||
// 根据新模式立即执行相应操作
|
|
||||||
if (mode === BackgroundSyncMode.REALTIME) {
|
if (mode === BackgroundSyncMode.REALTIME) {
|
||||||
// 切换到实时模式:立即启动前台服务(无论前台还是后台)
|
await ForegroundServiceModule.start({
|
||||||
const started = await ForegroundServiceModule.start({
|
|
||||||
title: '威友',
|
title: '威友',
|
||||||
body: '正在后台同步消息',
|
body: '正在后台运行',
|
||||||
});
|
});
|
||||||
if (started) {
|
|
||||||
this.startPeriodicSync();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 切换到其他模式:立即停止前台服务和定时同步
|
|
||||||
this.stopPeriodicSync();
|
|
||||||
await ForegroundServiceModule.stop();
|
await ForegroundServiceModule.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,110 +98,30 @@ class BackgroundSyncManager {
|
|||||||
private handleAppStateChange = async (nextState: AppStateStatus) => {
|
private handleAppStateChange = async (nextState: AppStateStatus) => {
|
||||||
console.log('[BackgroundSyncManager] AppState 变化:', nextState);
|
console.log('[BackgroundSyncManager] AppState 变化:', nextState);
|
||||||
|
|
||||||
if (nextState === 'background' || nextState === 'inactive') {
|
if (nextState === 'active') {
|
||||||
// 进入后台
|
await this.onForeground();
|
||||||
await this.startForBackground();
|
|
||||||
} else if (nextState === 'active') {
|
|
||||||
// 回到前台
|
|
||||||
await this.stopForForeground();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 进入后台时的处理
|
|
||||||
*/
|
|
||||||
private async startForBackground(): Promise<void> {
|
|
||||||
if (this.mode === BackgroundSyncMode.DISABLED) return;
|
|
||||||
|
|
||||||
console.log('[BackgroundSyncManager] 进入后台');
|
|
||||||
|
|
||||||
if (this.mode === BackgroundSyncMode.REALTIME) {
|
|
||||||
// 确保前台服务正在运行
|
|
||||||
await ForegroundServiceModule.start({
|
|
||||||
title: '威友',
|
|
||||||
body: '正在后台同步消息',
|
|
||||||
});
|
|
||||||
|
|
||||||
// 启动定时同步
|
|
||||||
this.startPeriodicSync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 回到前台时的处理
|
* 回到前台时的处理
|
||||||
|
* 同步离线期间通过 JPush 推送的消息
|
||||||
*/
|
*/
|
||||||
private async stopForForeground(): Promise<void> {
|
private async onForeground(): Promise<void> {
|
||||||
console.log('[BackgroundSyncManager] 回到前台');
|
console.log('[BackgroundSyncManager] 回到前台,同步离线消息');
|
||||||
|
|
||||||
// 停止定时同步(前台通过 WebSocket 实时同步,不需要轮询)
|
|
||||||
this.stopPeriodicSync();
|
|
||||||
|
|
||||||
// 实时模式下:前台服务保持运行,只更新通知文字
|
|
||||||
if (this.mode === BackgroundSyncMode.REALTIME) {
|
|
||||||
await ForegroundServiceModule.update('威友', '正在后台同步消息');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 立即同步一次(获取离线消息)
|
|
||||||
await this.syncMessages();
|
await this.syncMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动定时同步
|
|
||||||
*/
|
|
||||||
private startPeriodicSync(): void {
|
|
||||||
this.stopPeriodicSync();
|
|
||||||
|
|
||||||
// 立即同步一次
|
|
||||||
this.syncMessages();
|
|
||||||
|
|
||||||
// 启动定时器
|
|
||||||
this.syncInterval = setInterval(() => {
|
|
||||||
this.syncMessages();
|
|
||||||
}, SYNC_INTERVAL_MS);
|
|
||||||
|
|
||||||
console.log('[BackgroundSyncManager] 定时同步已启动');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止定时同步
|
|
||||||
*/
|
|
||||||
private stopPeriodicSync(): void {
|
|
||||||
if (this.syncInterval) {
|
|
||||||
clearInterval(this.syncInterval);
|
|
||||||
this.syncInterval = null;
|
|
||||||
console.log('[BackgroundSyncManager] 定时同步已停止');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 WebSocket 断开监听
|
|
||||||
*/
|
|
||||||
private setupWSDisconnectListener(): void {
|
|
||||||
// 监听 WebSocket 断开事件
|
|
||||||
wsService.onDisconnect(() => {
|
|
||||||
if (AppState.currentState !== 'active' && this.mode === BackgroundSyncMode.REALTIME) {
|
|
||||||
console.log('[BackgroundSyncManager] WebSocket 断开,尝试重连');
|
|
||||||
wsService.connect().catch((e) => {
|
|
||||||
console.error('[BackgroundSyncManager] WebSocket 重连失败:', e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行消息同步
|
* 执行消息同步
|
||||||
*/
|
*/
|
||||||
async syncMessages(): Promise<void> {
|
async syncMessages(): Promise<void> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
if (now - this.lastSyncAt < 5000) {
|
||||||
// 防止短时间内重复同步(最小 10 秒间隔)
|
|
||||||
if (now - this.lastSyncAt < 10000) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.lastSyncAt = now;
|
this.lastSyncAt = now;
|
||||||
|
|
||||||
// 调用外部注入的同步回调
|
|
||||||
if (this.syncMessagesCallback) {
|
if (this.syncMessagesCallback) {
|
||||||
try {
|
try {
|
||||||
await this.syncMessagesCallback();
|
await this.syncMessagesCallback();
|
||||||
@@ -228,7 +132,7 @@ class BackgroundSyncManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新前台服务通知内容
|
* 更新前台服务通知内容(仅通话场景使用)
|
||||||
*/
|
*/
|
||||||
async updateNotification(totalUnread: number): Promise<void> {
|
async updateNotification(totalUnread: number): Promise<void> {
|
||||||
if (this.mode !== BackgroundSyncMode.REALTIME) return;
|
if (this.mode !== BackgroundSyncMode.REALTIME) return;
|
||||||
@@ -241,14 +145,11 @@ class BackgroundSyncManager {
|
|||||||
} else {
|
} else {
|
||||||
await ForegroundServiceModule.update(
|
await ForegroundServiceModule.update(
|
||||||
'威友',
|
'威友',
|
||||||
'正在后台同步消息'
|
'正在后台运行'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存模式到存储
|
|
||||||
*/
|
|
||||||
private async saveMode(mode: BackgroundSyncMode): Promise<void> {
|
private async saveMode(mode: BackgroundSyncMode): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.setItem(STORAGE_KEY, mode);
|
await AsyncStorage.setItem(STORAGE_KEY, mode);
|
||||||
@@ -257,9 +158,6 @@ class BackgroundSyncManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 从存储加载模式
|
|
||||||
*/
|
|
||||||
private async loadMode(): Promise<BackgroundSyncMode> {
|
private async loadMode(): Promise<BackgroundSyncMode> {
|
||||||
try {
|
try {
|
||||||
const saved = await AsyncStorage.getItem(STORAGE_KEY);
|
const saved = await AsyncStorage.getItem(STORAGE_KEY);
|
||||||
@@ -273,7 +171,6 @@ class BackgroundSyncManager {
|
|||||||
* 清理资源
|
* 清理资源
|
||||||
*/
|
*/
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
this.stopPeriodicSync();
|
|
||||||
this.appStateSubscription?.remove();
|
this.appStateSubscription?.remove();
|
||||||
this.appStateSubscription = null;
|
this.appStateSubscription = null;
|
||||||
await ForegroundServiceModule.stop();
|
await ForegroundServiceModule.stop();
|
||||||
|
|||||||
@@ -1101,7 +1101,14 @@ class WebSocketService {
|
|||||||
if (this.appStateSubscription) return;
|
if (this.appStateSubscription) return;
|
||||||
this.lastAppState = AppState.currentState;
|
this.lastAppState = AppState.currentState;
|
||||||
this.appStateSubscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
|
this.appStateSubscription = AppState.addEventListener('change', (nextState: AppStateStatus) => {
|
||||||
|
// 进入后台时断开 WebSocket,由 JPush 负责后台推送通知
|
||||||
|
if (this.lastAppState === 'active' && nextState.match(/inactive|background/)) {
|
||||||
|
console.log('[WebSocket] App entering background, disconnecting');
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
// 回到前台时重连 WebSocket
|
||||||
if (this.lastAppState.match(/inactive|background/) && nextState === 'active' && !this.isConnected()) {
|
if (this.lastAppState.match(/inactive|background/) && nextState === 'active' && !this.isConnected()) {
|
||||||
|
console.log('[WebSocket] App returning to foreground, reconnecting');
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Platform, NativeModules } from 'react-native';
|
import { Platform, NativeModules } from 'react-native';
|
||||||
import { pushService } from './pushService';
|
import { pushService } from './pushService';
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
|
||||||
type JPushNotificationMessage = {
|
type JPushNotificationMessage = {
|
||||||
messageID: string;
|
messageID: string;
|
||||||
@@ -305,6 +306,35 @@ class JPushService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求通知权限(使用 expo-notifications,统一处理 iOS/Android)
|
||||||
|
*/
|
||||||
|
async requestNotificationPermission(): Promise<boolean> {
|
||||||
|
if (Platform.OS === 'web') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { status } = await Notifications.requestPermissionsAsync();
|
||||||
|
return status === 'granted';
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[JPush] request notification permission failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查通知权限是否已开启
|
||||||
|
*/
|
||||||
|
async checkNotificationPermission(): Promise<boolean> {
|
||||||
|
if (Platform.OS === 'web') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { status } = await Notifications.getPermissionsAsync();
|
||||||
|
return status === 'granted';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cleanup(): void {
|
cleanup(): void {
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.registrationID = null;
|
this.registrationID = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user