231 lines
7.2 KiB
TypeScript
231 lines
7.2 KiB
TypeScript
|
|
/**
|
|||
|
|
* 系统通知服务
|
|||
|
|
* 使用 expo-notifications 处理原生系统通知
|
|||
|
|
* 支持本地通知,类似 QQ 在通知栏显示消息
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import * as Notifications from 'expo-notifications';
|
|||
|
|
import { Platform, AppState, AppStateStatus } from 'react-native';
|
|||
|
|
import type { WSChatMessage, WSNotificationMessage, WSAnnouncementMessage } from './websocketService';
|
|||
|
|
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') {
|
|||
|
|
console.log('[SystemNotification] 通知权限未授权');
|
|||
|
|
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;
|
|||
|
|
console.log('[SystemNotification] 通知服务初始化成功');
|
|||
|
|
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 表示立即显示
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('[SystemNotification] 通知已显示:', notificationId);
|
|||
|
|
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> {
|
|||
|
|
console.log('[SystemNotification] handleWSMessage 被调用, 当前AppState:', this.currentAppState);
|
|||
|
|
|
|||
|
|
// 仅在后台时显示通知,前台时不显示(用户正在使用应用,可以直接看到消息)
|
|||
|
|
if (this.currentAppState !== 'active') {
|
|||
|
|
// 判断是否是聊天消息(通过 segments 字段)
|
|||
|
|
if ('segments' in message) {
|
|||
|
|
const chatMsg = message as WSChatMessage;
|
|||
|
|
const body = extractTextFromSegments(chatMsg.segments);
|
|||
|
|
console.log('[SystemNotification] 后台模式 - 显示聊天通知:', chatMsg.id, body);
|
|||
|
|
await this.showChatNotification(chatMsg);
|
|||
|
|
} else {
|
|||
|
|
const notifMsg = message as WSNotificationMessage | WSAnnouncementMessage;
|
|||
|
|
console.log('[SystemNotification] 后台模式 - 显示系统通知:', notifMsg.id, notifMsg.content);
|
|||
|
|
await this.showWSNotification(notifMsg);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
console.log('[SystemNotification] 前台模式 - 不显示通知');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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();
|