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:
@@ -1,23 +1,26 @@
|
||||
/**
|
||||
* 后台同步管理器
|
||||
*
|
||||
* 整合前台服务、后台同步、消息状态管理
|
||||
* 参考 Element Android 的 BackgroundSyncStarter 设计
|
||||
* 后台推送策略:
|
||||
* - 前台:WebSocket 实时通信
|
||||
* - 后台:断开 WebSocket,由 JPush 负责推送通知
|
||||
* - 回到前台:重连 WebSocket + 同步离线消息
|
||||
*
|
||||
* 前台服务仅用于通话保活,不再用于消息同步。
|
||||
*/
|
||||
|
||||
import { AppState, AppStateStatus, Platform } from 'react-native';
|
||||
import { ForegroundServiceModule } from './ForegroundServiceModule';
|
||||
import { wsService } from '../core/wsService';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
/**
|
||||
* 后台同步模式
|
||||
*/
|
||||
export enum BackgroundSyncMode {
|
||||
/** 省电模式:使用 expo-background-task(最小15分钟间隔) */
|
||||
/** 省电模式:仅依赖 JPush 推送 */
|
||||
BATTERY_SAVER = 'battery_saver',
|
||||
|
||||
/** 实时模式:前台服务保活 + 持续同步 */
|
||||
/** 实时模式:前台服务保活(仅用于通话) */
|
||||
REALTIME = 'realtime',
|
||||
|
||||
/** 禁用后台同步 */
|
||||
@@ -25,12 +28,10 @@ export enum BackgroundSyncMode {
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'background_sync_mode';
|
||||
const SYNC_INTERVAL_MS = 30 * 1000; // 30 秒同步间隔
|
||||
|
||||
class BackgroundSyncManager {
|
||||
private mode: BackgroundSyncMode = BackgroundSyncMode.BATTERY_SAVER;
|
||||
private appStateSubscription: ReturnType<typeof AppState.addEventListener> | null = null;
|
||||
private syncInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private isInitialized: boolean = false;
|
||||
private lastSyncAt: number = 0;
|
||||
private syncMessagesCallback: (() => Promise<void>) | null = null;
|
||||
@@ -49,19 +50,14 @@ class BackgroundSyncManager {
|
||||
async initialize(): Promise<void> {
|
||||
if (this.isInitialized) return;
|
||||
|
||||
// 加载保存的模式
|
||||
const savedMode = await this.loadMode();
|
||||
this.mode = savedMode;
|
||||
|
||||
// 监听 AppState 变化
|
||||
this.appStateSubscription = AppState.addEventListener(
|
||||
'change',
|
||||
this.handleAppStateChange
|
||||
);
|
||||
|
||||
// 监听 WebSocket 断开
|
||||
this.setupWSDisconnectListener();
|
||||
|
||||
this.isInitialized = true;
|
||||
console.log('[BackgroundSyncManager] 初始化完成, 模式:', this.mode);
|
||||
}
|
||||
@@ -70,31 +66,19 @@ class BackgroundSyncManager {
|
||||
* 切换后台同步模式
|
||||
*/
|
||||
async setMode(mode: BackgroundSyncMode): Promise<void> {
|
||||
// 如果当前在后台,先停止旧模式的保活
|
||||
if (AppState.currentState !== 'active') {
|
||||
this.stopPeriodicSync();
|
||||
if (this.mode === BackgroundSyncMode.REALTIME) {
|
||||
await ForegroundServiceModule.stop();
|
||||
}
|
||||
if (AppState.currentState !== 'active' && this.mode === BackgroundSyncMode.REALTIME) {
|
||||
await ForegroundServiceModule.stop();
|
||||
}
|
||||
|
||||
// 更新模式
|
||||
this.mode = mode;
|
||||
await this.saveMode(mode);
|
||||
|
||||
// 根据新模式立即执行相应操作
|
||||
if (mode === BackgroundSyncMode.REALTIME) {
|
||||
// 切换到实时模式:立即启动前台服务(无论前台还是后台)
|
||||
const started = await ForegroundServiceModule.start({
|
||||
await ForegroundServiceModule.start({
|
||||
title: '威友',
|
||||
body: '正在后台同步消息',
|
||||
body: '正在后台运行',
|
||||
});
|
||||
if (started) {
|
||||
this.startPeriodicSync();
|
||||
}
|
||||
} else {
|
||||
// 切换到其他模式:立即停止前台服务和定时同步
|
||||
this.stopPeriodicSync();
|
||||
await ForegroundServiceModule.stop();
|
||||
}
|
||||
|
||||
@@ -114,110 +98,30 @@ class BackgroundSyncManager {
|
||||
private handleAppStateChange = async (nextState: AppStateStatus) => {
|
||||
console.log('[BackgroundSyncManager] AppState 变化:', nextState);
|
||||
|
||||
if (nextState === 'background' || nextState === 'inactive') {
|
||||
// 进入后台
|
||||
await this.startForBackground();
|
||||
} else if (nextState === 'active') {
|
||||
// 回到前台
|
||||
await this.stopForForeground();
|
||||
if (nextState === 'active') {
|
||||
await this.onForeground();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 进入后台时的处理
|
||||
*/
|
||||
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> {
|
||||
console.log('[BackgroundSyncManager] 回到前台');
|
||||
|
||||
// 停止定时同步(前台通过 WebSocket 实时同步,不需要轮询)
|
||||
this.stopPeriodicSync();
|
||||
|
||||
// 实时模式下:前台服务保持运行,只更新通知文字
|
||||
if (this.mode === BackgroundSyncMode.REALTIME) {
|
||||
await ForegroundServiceModule.update('威友', '正在后台同步消息');
|
||||
}
|
||||
|
||||
// 立即同步一次(获取离线消息)
|
||||
private async onForeground(): Promise<void> {
|
||||
console.log('[BackgroundSyncManager] 回到前台,同步离线消息');
|
||||
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> {
|
||||
const now = Date.now();
|
||||
|
||||
// 防止短时间内重复同步(最小 10 秒间隔)
|
||||
if (now - this.lastSyncAt < 10000) {
|
||||
if (now - this.lastSyncAt < 5000) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastSyncAt = now;
|
||||
|
||||
// 调用外部注入的同步回调
|
||||
if (this.syncMessagesCallback) {
|
||||
try {
|
||||
await this.syncMessagesCallback();
|
||||
@@ -228,7 +132,7 @@ class BackgroundSyncManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新前台服务通知内容
|
||||
* 更新前台服务通知内容(仅通话场景使用)
|
||||
*/
|
||||
async updateNotification(totalUnread: number): Promise<void> {
|
||||
if (this.mode !== BackgroundSyncMode.REALTIME) return;
|
||||
@@ -241,14 +145,11 @@ class BackgroundSyncManager {
|
||||
} else {
|
||||
await ForegroundServiceModule.update(
|
||||
'威友',
|
||||
'正在后台同步消息'
|
||||
'正在后台运行'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存模式到存储
|
||||
*/
|
||||
private async saveMode(mode: BackgroundSyncMode): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(STORAGE_KEY, mode);
|
||||
@@ -257,9 +158,6 @@ class BackgroundSyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从存储加载模式
|
||||
*/
|
||||
private async loadMode(): Promise<BackgroundSyncMode> {
|
||||
try {
|
||||
const saved = await AsyncStorage.getItem(STORAGE_KEY);
|
||||
@@ -273,7 +171,6 @@ class BackgroundSyncManager {
|
||||
* 清理资源
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
this.stopPeriodicSync();
|
||||
this.appStateSubscription?.remove();
|
||||
this.appStateSubscription = null;
|
||||
await ForegroundServiceModule.stop();
|
||||
|
||||
@@ -1101,7 +1101,14 @@ class WebSocketService {
|
||||
if (this.appStateSubscription) return;
|
||||
this.lastAppState = AppState.currentState;
|
||||
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()) {
|
||||
console.log('[WebSocket] App returning to foreground, reconnecting');
|
||||
this.reconnectAttempts = 0;
|
||||
this.connect();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Platform, NativeModules } from 'react-native';
|
||||
import { pushService } from './pushService';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
|
||||
type JPushNotificationMessage = {
|
||||
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 {
|
||||
this.isInitialized = false;
|
||||
this.registrationID = null;
|
||||
|
||||
Reference in New Issue
Block a user