feat(message): add conversation notification mute feature
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 57s
Frontend CI / ota-android (push) Successful in 10m53s
Frontend CI / build-android-apk (push) Failing after 1h12m12s

Add notification mute functionality for conversations with the following changes:

- Add notification_muted field to ConversationResponse and related DTOs
- Add notificationMutedMap to message store for tracking mute state
- Add setConversationNotificationMuted API method
- Integrate mute toggle in GroupInfoScreen and PrivateChatInfoScreen
- Skip system notifications for muted conversations
- Suppress vibration for muted conversations in WS handler
- Clean up selected mentions when @mentions are removed from text

Chore changes:
- Update PostCard bookmark icon from 'bookmark' to 'star'
- Refine card styles across AppsScreen, MaterialsScreen, and SubjectMaterialsScreen
- Improve week selector scrolling to center selected week in ScheduleScreen
This commit is contained in:
lafay
2026-04-25 21:23:22 +08:00
parent de0afa93a1
commit 5fa5403d6a
15 changed files with 252 additions and 101 deletions

View File

@@ -310,6 +310,21 @@ class MessageManager {
return useMessageStore.getState().setMutedStatus(groupId, isMuted);
}
isNotificationMuted(conversationId: string): boolean {
return useMessageStore.getState().isNotificationMuted(conversationId);
}
setNotificationMuted(conversationId: string, notificationMuted: boolean): void {
return useMessageStore.getState().setNotificationMuted(conversationId, notificationMuted);
}
async toggleNotificationMuted(conversationId: string, notificationMuted: boolean): Promise<void> {
const { messageService } = await import('@/services/message');
await messageService.setConversationNotificationMuted(conversationId, notificationMuted);
this.setNotificationMuted(conversationId, notificationMuted);
this.updateConversation(conversationId, { notification_muted: notificationMuted });
}
// ==================== 活动会话管理 ====================
setActiveConversation(conversationId: string | null): void {

View File

@@ -317,7 +317,8 @@ export class WSMessageHandler implements IWSMessageHandler {
!_isAck;
if (shouldIncrementUnread) {
if (!suppressVibration) {
const isNotificationMuted = store.isNotificationMuted(normalizedConversationId);
if (!suppressVibration && !isNotificationMuted) {
const vibrationType = message.type === 'group_message' ? 'group_message' : 'message';
eventBus.emit({ type: 'VIBRATE', payload: { type: vibrationType } });
}

View File

@@ -55,6 +55,9 @@ export interface MessageState {
// 当前用户的禁言状态 - 按群组ID存储
mutedStatusMap: Map<string, boolean>;
// 会话免打扰状态 - 按会话ID存储
notificationMutedMap: Map<string, boolean>;
}
// ==================== Actions 接口 ====================
@@ -72,6 +75,7 @@ export interface MessageActions {
getTypingUsers: (groupId: string) => string[];
isMuted: (groupId: string) => boolean;
getActiveConversation: () => string | null;
isNotificationMuted: (conversationId: string) => boolean;
// 设置状态
setConversations: (conversations: Map<string, ConversationResponse>) => void;
@@ -88,6 +92,7 @@ export interface MessageActions {
setLoadingMessages: (conversationId: string, loading: boolean) => void;
setTypingUsers: (groupId: string, users: string[]) => void;
setMutedStatus: (groupId: string, isMuted: boolean) => void;
setNotificationMuted: (conversationId: string, notificationMuted: boolean) => void;
setInitialized: (initialized: boolean) => void;
// 重置
@@ -110,6 +115,7 @@ const initialState: MessageState = {
isInitialized: false,
typingUsersMap: new Map(),
mutedStatusMap: new Map(),
notificationMutedMap: new Map(),
};
// ==================== 工具函数 ====================
@@ -177,6 +183,7 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
isInitialized: state.isInitialized,
typingUsersMap: state.typingUsersMap,
mutedStatusMap: state.mutedStatusMap,
notificationMutedMap: state.notificationMutedMap,
};
},
@@ -227,11 +234,25 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
return get().currentConversationId;
},
isNotificationMuted: (conversationId: string) => {
return get().notificationMutedMap.get(conversationId) || false;
},
// ==================== 设置状态 ====================
setConversations: (conversations: Map<string, ConversationResponse>) => {
const conversationList = sortConversationList(conversations);
set({ conversations, conversationList });
const notificationMutedMap = new Map<string, boolean>();
conversations.forEach((conv, id) => {
if (conv.notification_muted) {
notificationMutedMap.set(id, true);
}
});
set(state => ({
conversations,
conversationList,
notificationMutedMap: new Map([...state.notificationMutedMap, ...notificationMutedMap]),
}));
},
updateConversation: (conversation: ConversationResponse) => {
@@ -240,7 +261,13 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
const newConversations = new Map(state.conversations);
newConversations.set(id, { ...conversation, id });
const conversationList = sortConversationList(newConversations);
return { conversations: newConversations, conversationList };
const newNotificationMutedMap = new Map(state.notificationMutedMap);
if (conversation.notification_muted) {
newNotificationMutedMap.set(id, true);
} else {
newNotificationMutedMap.delete(id);
}
return { conversations: newConversations, conversationList, notificationMutedMap: newNotificationMutedMap };
});
},
@@ -374,6 +401,14 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
});
},
setNotificationMuted: (conversationId: string, notificationMuted: boolean) => {
set(state => {
const newNotificationMutedMap = new Map(state.notificationMutedMap);
newNotificationMutedMap.set(conversationId, notificationMuted);
return { notificationMutedMap: newNotificationMutedMap };
});
},
setInitialized: (initialized: boolean) => {
set({ isInitialized: initialized });
},
@@ -388,6 +423,7 @@ export const useMessageStore = create<MessageStore>((set, get) => ({
loadingMessagesSet: new Set(),
typingUsersMap: new Map(),
mutedStatusMap: new Map(),
notificationMutedMap: new Map(),
});
AsyncStorage.removeItem(LAST_SYSTEM_MESSAGE_AT_KEY).catch(() => {});
},