Files
frontend/src/services/systemNotificationService.ts
lan be84c01abd Migrate frontend realtime messaging to SSE.
Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts.

Made-with: Cursor
2026-03-10 12:58:23 +08:00

225 lines
6.7 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';
// 通知渠道配置
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;
}
// 设置 notification handler确保前台也能显示通知、播放声音和震动
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
shouldShowBanner: true,
shouldShowList: true,
}),
});
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 content: Notifications.NotificationContentInput = {
title: options.title,
body: options.body,
data: options.data as Record<string, string | number | object> | undefined,
// 显式设置震动(仅 Android 生效)
...(Platform.OS === 'android' ? {
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 (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();