Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts. Made-with: Cursor
225 lines
6.7 KiB
TypeScript
225 lines
6.7 KiB
TypeScript
/**
|
||
* 系统通知服务
|
||
* 使用 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();
|