Initial frontend repository commit.

Include app source and update .gitignore to exclude local release artifacts and signing files.

Made-with: Cursor
This commit is contained in:
2026-03-09 21:29:03 +08:00
commit 3968660048
129 changed files with 55599 additions and 0 deletions

View File

@@ -0,0 +1,230 @@
/**
* 系统通知服务
* 使用 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();