Files
frontend/src/services/systemNotificationService.ts

225 lines
6.7 KiB
TypeScript
Raw Normal View History

/**
*
* 使 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();