Files
frontend/src/services/systemNotificationService.ts
lafay 583ac64dfd
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 8m28s
Frontend CI / ota-android (push) Successful in 11m6s
Frontend CI / build-android-apk (push) Successful in 1h3m35s
feat(Notification): implement notification preferences management and enhance notification handling
- Introduced a new service for managing notification preferences, including push notifications, sound, and vibration settings.
- Updated the notification handling logic to respect user preferences, ensuring notifications are displayed according to user settings.
- Refactored the App and various screens to integrate the new notification preferences, improving user experience and consistency.
- Enhanced the HomeScreen and NotificationSettingsScreen to load and update notification settings seamlessly.
- Implemented a mechanism to hide the bottom tab bar based on scroll events, improving navigation usability.
2026-03-25 01:30:00 +08:00

219 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 系统通知服务
* 使用 expo-notifications 处理原生系统通知
* 支持本地通知,类似 QQ 在通知栏显示消息
*/
import * as Notifications from 'expo-notifications';
import { Platform, AppState, AppStateStatus } from 'react-native';
import type { WSChatMessage, WSNotificationMessage, WSAnnouncementMessage } from './sseService';
import { extractTextFromSegments } from '../types/dto';
import { getNotificationPreferencesSync } from './notificationPreferences';
// 通知渠道配置
const CHANNEL_ID = 'default';
const CHANNEL_NAME = '默认通知';
const CHANNEL_DESCRIPTION = '接收系统消息、互动通知等';
// 通知类型
export type AppNotificationType =
| 'like_post'
| 'like_comment'
| 'like_reply'
| 'favorite_post'
| 'comment'
| 'reply'
| 'follow'
| 'mention'
| 'system'
| 'announcement'
| 'announce'
| 'chat';
// 获取通知标题
export function getNotificationTitle(type: AppNotificationType, operatorName?: string): string {
const titles: Record<AppNotificationType, string> = {
like_post: '有人赞了你的帖子',
like_comment: '有人赞了你的评论',
like_reply: '有人赞了你的回复',
favorite_post: '有人收藏了你的帖子',
comment: '新评论',
reply: '新回复',
follow: '新粉丝',
mention: '@提及',
system: '系统通知',
announcement: '公告',
announce: '公告',
chat: '新消息',
};
return titles[type] || '新通知';
}
class SystemNotificationService {
private isInitialized: boolean = false;
private appStateSubscription: any = null;
private currentAppState: AppStateStatus = 'active';
async initialize(): Promise<boolean> {
if (this.isInitialized) {
return true;
}
try {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
return false;
}
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync(CHANNEL_ID, {
name: CHANNEL_NAME,
importance: Notifications.AndroidImportance.HIGH, // HIGH 优先级足以触发震动
vibrationPattern: [0, 300, 200, 300], // 等待0ms震动300ms停止200ms震动300ms
lightColor: '#FF6B35',
enableVibrate: true, // 启用震动
enableLights: true, // 启用呼吸灯
});
}
this.currentAppState = AppState.currentState;
this.appStateSubscription = AppState.addEventListener('change', (nextAppState) => {
this.currentAppState = nextAppState;
});
this.isInitialized = true;
return true;
} catch (error) {
console.error('[SystemNotification] 初始化失败:', error);
return false;
}
}
async showNotification(options: {
title: string;
body: string;
data?: Record<string, string | number | object>;
type: AppNotificationType;
}): Promise<string | null> {
if (!this.isInitialized) {
await this.initialize();
}
try {
// 使用 scheduleNotificationAsync 立即显示通知
// 配合 setNotificationHandler 确保前台也能显示
const { vibrationEnabled } = getNotificationPreferencesSync();
// 构建通知内容
const content: Notifications.NotificationContentInput = {
title: options.title,
body: options.body,
data: options.data as Record<string, string | number | object> | undefined,
// 显式设置震动(仅 Android 生效,且尊重「消息震动」开关)
...(Platform.OS === 'android' && vibrationEnabled
? { vibrationPattern: [0, 300, 200, 300] }
: {}),
};
const notificationId = await Notifications.scheduleNotificationAsync({
content,
trigger: null, // null 表示立即显示
});
return notificationId;
} catch (error) {
console.error('[SystemNotification] 显示通知失败:', error);
return null;
}
}
async showChatNotification(message: WSChatMessage): Promise<string | null> {
const body = extractTextFromSegments(message.segments);
return this.showNotification({
title: '新消息',
body,
data: {
type: 'chat',
conversationId: message.conversation_id,
messageId: String(message.id),
senderId: message.sender_id,
},
type: 'chat',
});
}
async showWSNotification(message: WSNotificationMessage | WSAnnouncementMessage): Promise<string | null> {
const type = message.system_type as AppNotificationType;
const title = getNotificationTitle(type);
const body = message.content;
return this.showNotification({
title,
body,
data: {
type: message.type,
id: String(message.id),
senderId: message.sender_id || '',
receiverId: message.receiver_id || '',
systemType: message.system_type || '',
extraData: JSON.stringify(message.extra_data || {}),
},
type,
});
}
async handleWSMessage(message: WSChatMessage | WSNotificationMessage | WSAnnouncementMessage): Promise<void> {
if (!getNotificationPreferencesSync().pushEnabled) {
return;
}
// 仅在后台时显示通知,前台时不显示(用户正在使用应用,可以直接看到消息)
if (this.currentAppState !== 'active') {
// 判断是否是聊天消息(通过 segments 字段)
if ('segments' in message) {
const chatMsg = message as WSChatMessage;
const body = extractTextFromSegments(chatMsg.segments);
void chatMsg;
void body;
await this.showChatNotification(chatMsg);
} else {
const notifMsg = message as WSNotificationMessage | WSAnnouncementMessage;
void notifMsg;
await this.showWSNotification(notifMsg);
}
}
}
async clearAllNotifications(): Promise<void> {
await Notifications.dismissAllNotificationsAsync();
}
async clearBadge(): Promise<void> {
await Notifications.setBadgeCountAsync(0);
}
async setBadgeCount(count: number): Promise<void> {
await Notifications.setBadgeCountAsync(count);
}
cleanup(): void {
if (this.appStateSubscription) {
this.appStateSubscription.remove();
this.appStateSubscription = null;
}
this.isInitialized = false;
}
getAppState(): AppStateStatus {
return this.currentAppState;
}
}
export const systemNotificationService = new SystemNotificationService();