feat(notification): add foreground notification vibration feedback and Android channel setup
All checks were successful
Frontend CI / ota-android (push) Successful in 1m33s
Frontend CI / build-and-push-web (push) Successful in 2m51s
Frontend CI / build-android-apk (push) Successful in 29m40s

- Add vibration feedback when app is in foreground and notification arrives
- Configure Android notification channel using expo-notifications
- Integrate notification handler to respect user push/sound preferences
- Add platform-specific local notification handling for Android
This commit is contained in:
lafay
2026-04-28 08:53:28 +08:00
parent be0c304711
commit 2432f7bc1e
3 changed files with 77 additions and 5 deletions

View File

@@ -1,6 +1,8 @@
import { Platform, NativeModules } from 'react-native';
import { Platform, NativeModules, AppState } from 'react-native';
import { pushService } from './pushService';
import * as Notifications from 'expo-notifications';
import { getNotificationPreferencesSync } from './notificationPreferences';
import { vibrateOnMessage } from '../platform/messageVibrationService';
type JPushNotificationMessage = {
messageID: string;
@@ -68,6 +70,12 @@ class JPushService {
JPush!.addNotificationListener((result: any) => {
console.log('[JPush] notification:', result);
if (result.notificationEventType === 'notificationArrived') {
const prefs = getNotificationPreferencesSync();
if (prefs.vibrationEnabled && AppState.currentState === 'active') {
vibrateOnMessage('notification').catch(() => {});
}
}
if (this.notificationCallback) {
this.notificationCallback({
messageID: result.messageID || '',
@@ -111,6 +119,10 @@ class JPushService {
production: !__DEV__,
});
if (Platform.OS === 'android') {
this._setupAndroidNotificationChannel();
}
console.log('[JPush] init called, waiting for registration...');
// 轮询获取 RegistrationID间隔逐渐增大
@@ -136,11 +148,37 @@ class JPushService {
}
private _onConnected(): void {
if (this.registrationID) return; // Already have registrationID, skip
if (this.registrationID) return;
console.log('[JPush] connected, fetching registrationID...');
this._fetchRegistrationID();
}
private _setupAndroidNotificationChannel(): void {
if (!JPush) return;
try {
(JPush as any).setChannelAndSound({
channel: '推送通知',
channelId: 'jpush_notification',
});
console.log('[JPush] Android notification channel configured');
} catch (error) {
console.warn('[JPush] setChannelAndSound failed:', error);
}
try {
Notifications.setNotificationChannelAsync('jpush_notification', {
name: '推送通知',
importance: Notifications.AndroidImportance.HIGH,
vibrationPattern: [0, 250, 250, 250],
sound: 'default',
enableLights: true,
showBadge: true,
});
} catch (error) {
console.warn('[JPush] expo-notifications channel setup failed:', error);
}
}
private _fetchRegistrationID(): void {
if (!JPush) return;
JPush.getRegistrationID((result: any) => {
@@ -282,7 +320,15 @@ class JPushService {
}): void {
if (!JPush || !this.isInitialized) return;
try {
JPush.addLocalNotification(params);
if (Platform.OS === 'android') {
const localParams = {
...params,
priority: 1,
};
JPush.addLocalNotification(localParams);
} else {
JPush.addLocalNotification(params);
}
} catch (error) {
console.error('[JPush] addLocalNotification failed:', error);
}

View File

@@ -1,4 +1,6 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from 'react-native';
import * as Notifications from 'expo-notifications';
import { setVibrationEnabled } from '../platform/messageVibrationService';
export const NOTIFICATION_PREF_KEYS = {
@@ -70,6 +72,18 @@ export async function setSoundPreference(enabled: boolean): Promise<void> {
}
export function registerNotificationPresentationHandler(): void {
// JPush handles notification presentation natively.
// Preferences are read from getNotificationPreferencesSync() when needed.
if (Platform.OS === 'web') return;
Notifications.setNotificationHandler({
handleNotification: async () => {
const prefs = getNotificationPreferencesSync();
return {
shouldShowAlert: prefs.pushEnabled,
shouldPlaySound: prefs.soundEnabled,
shouldSetBadge: true,
shouldShowBanner: prefs.pushEnabled,
shouldShowList: prefs.pushEnabled,
};
},
});
}

View File

@@ -191,6 +191,18 @@ class SystemNotificationService {
vibrateOnMessage('notification').catch(() => {});
await this.showWSNotification(notifMsg);
}
} else {
// App is in foreground: trigger vibration feedback
if ('segments' in message) {
const chatMsg = message as WSChatMessage;
const conversationId = normalizeConversationId(chatMsg.conversation_id);
const isNotificationMuted = useMessageStore.getState().isNotificationMuted(conversationId);
if (!isNotificationMuted) {
vibrateOnMessage('chat').catch(() => {});
}
} else {
vibrateOnMessage('notification').catch(() => {});
}
}
}