2026-03-31 18:22:24 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 消息状态 Zustand Store
|
|
|
|
|
|
* 使用 zustand 进行状态管理,提供响应式状态更新
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { create } from 'zustand';
|
2026-04-25 15:09:00 +08:00
|
|
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
2026-03-31 18:22:24 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
ConversationResponse,
|
|
|
|
|
|
MessageResponse,
|
|
|
|
|
|
} from '../../types/dto';
|
|
|
|
|
|
|
2026-04-25 15:09:00 +08:00
|
|
|
|
const LAST_SYSTEM_MESSAGE_AT_KEY = 'last_system_message_at';
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// ==================== 状态接口 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 消息状态接口(不包含订阅者,订阅者单独管理)
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface MessageState {
|
|
|
|
|
|
// 会话相关
|
|
|
|
|
|
conversations: Map<string, ConversationResponse>;
|
|
|
|
|
|
conversationList: ConversationResponse[];
|
|
|
|
|
|
|
|
|
|
|
|
// 消息相关 - 按会话ID存储
|
|
|
|
|
|
messagesMap: Map<string, MessageResponse[]>;
|
|
|
|
|
|
|
|
|
|
|
|
// 未读数
|
|
|
|
|
|
totalUnreadCount: number;
|
|
|
|
|
|
systemUnreadCount: number;
|
|
|
|
|
|
|
2026-04-25 00:39:40 +08:00
|
|
|
|
// 最后一条系统通知时间
|
|
|
|
|
|
lastSystemMessageAt: string | null;
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// 连接状态
|
|
|
|
|
|
isWSConnected: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
// 当前活动会话ID(用户正在查看的会话)
|
|
|
|
|
|
currentConversationId: string | null;
|
|
|
|
|
|
|
|
|
|
|
|
// 加载状态
|
|
|
|
|
|
isLoadingConversations: boolean;
|
|
|
|
|
|
loadingMessagesSet: Set<string>;
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化状态
|
|
|
|
|
|
isInitialized: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
// 输入状态 - 按群组ID存储正在输入的用户ID列表
|
|
|
|
|
|
typingUsersMap: Map<string, string[]>;
|
|
|
|
|
|
|
|
|
|
|
|
// 当前用户的禁言状态 - 按群组ID存储
|
|
|
|
|
|
mutedStatusMap: Map<string, boolean>;
|
2026-04-25 21:23:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 会话免打扰状态 - 按会话ID存储
|
|
|
|
|
|
notificationMutedMap: Map<string, boolean>;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== Actions 接口 ====================
|
|
|
|
|
|
|
|
|
|
|
|
export interface MessageActions {
|
|
|
|
|
|
// 获取状态
|
|
|
|
|
|
getState: () => MessageState;
|
|
|
|
|
|
getConversations: () => ConversationResponse[];
|
|
|
|
|
|
getConversation: (conversationId: string) => ConversationResponse | null;
|
|
|
|
|
|
getMessages: (conversationId: string) => MessageResponse[];
|
|
|
|
|
|
getUnreadCount: () => { total: number; system: number };
|
|
|
|
|
|
isConnected: () => boolean;
|
|
|
|
|
|
isLoading: () => boolean;
|
|
|
|
|
|
isLoadingMessages: (conversationId: string) => boolean;
|
|
|
|
|
|
getTypingUsers: (groupId: string) => string[];
|
|
|
|
|
|
isMuted: (groupId: string) => boolean;
|
|
|
|
|
|
getActiveConversation: () => string | null;
|
2026-04-25 21:23:22 +08:00
|
|
|
|
isNotificationMuted: (conversationId: string) => boolean;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
|
|
|
|
|
|
// 设置状态
|
2026-05-12 01:28:46 +08:00
|
|
|
|
setConversationsWithUnread: (
|
|
|
|
|
|
conversations: Map<string, ConversationResponse>,
|
|
|
|
|
|
totalUnread: number,
|
|
|
|
|
|
systemUnread: number,
|
|
|
|
|
|
) => void;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
updateConversation: (conversation: ConversationResponse) => void;
|
|
|
|
|
|
removeConversation: (conversationId: string) => void;
|
|
|
|
|
|
setMessages: (conversationId: string, messages: MessageResponse[]) => void;
|
|
|
|
|
|
setUnreadCount: (total: number, system: number) => void;
|
2026-04-25 00:39:40 +08:00
|
|
|
|
setLastSystemMessageAt: (time: string | null) => void;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
setSSEConnected: (connected: boolean) => void;
|
|
|
|
|
|
setCurrentConversation: (conversationId: string | null) => void;
|
|
|
|
|
|
setLoading: (loading: boolean) => void;
|
|
|
|
|
|
setLoadingMessages: (conversationId: string, loading: boolean) => void;
|
|
|
|
|
|
setTypingUsers: (groupId: string, users: string[]) => void;
|
|
|
|
|
|
setMutedStatus: (groupId: string, isMuted: boolean) => void;
|
2026-04-25 21:23:22 +08:00
|
|
|
|
setNotificationMuted: (conversationId: string, notificationMuted: boolean) => void;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
setInitialized: (initialized: boolean) => void;
|
|
|
|
|
|
|
2026-05-12 18:04:32 +08:00
|
|
|
|
// 原子性更新(避免中间渲染状态)
|
|
|
|
|
|
updateConversationWithUnread: (
|
|
|
|
|
|
conversation: ConversationResponse,
|
|
|
|
|
|
totalUnread: number,
|
|
|
|
|
|
systemUnread: number,
|
|
|
|
|
|
) => void;
|
|
|
|
|
|
incrementUnreadAtomic: (conversationId: string) => void;
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// 重置
|
|
|
|
|
|
reset: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 初始状态 ====================
|
|
|
|
|
|
|
|
|
|
|
|
const initialState: MessageState = {
|
|
|
|
|
|
conversations: new Map(),
|
|
|
|
|
|
conversationList: [],
|
|
|
|
|
|
messagesMap: new Map(),
|
|
|
|
|
|
totalUnreadCount: 0,
|
|
|
|
|
|
systemUnreadCount: 0,
|
2026-04-25 00:39:40 +08:00
|
|
|
|
lastSystemMessageAt: null,
|
2026-03-31 18:22:24 +08:00
|
|
|
|
isWSConnected: false,
|
|
|
|
|
|
currentConversationId: null,
|
|
|
|
|
|
isLoadingConversations: false,
|
|
|
|
|
|
loadingMessagesSet: new Set(),
|
|
|
|
|
|
isInitialized: false,
|
|
|
|
|
|
typingUsersMap: new Map(),
|
|
|
|
|
|
mutedStatusMap: new Map(),
|
2026-04-25 21:23:22 +08:00
|
|
|
|
notificationMutedMap: new Map(),
|
2026-03-31 18:22:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 工具函数 ====================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 规范化会话ID
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function normalizeConversationId(conversationId: string | number | null | undefined): string {
|
|
|
|
|
|
return conversationId == null ? '' : String(conversationId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 更新会话列表排序
|
|
|
|
|
|
* 排序规则:置顶优先,再按最后消息时间排序
|
|
|
|
|
|
*/
|
|
|
|
|
|
function sortConversationList(conversations: Map<string, ConversationResponse>): ConversationResponse[] {
|
|
|
|
|
|
return Array.from(conversations.values()).sort((a, b) => {
|
|
|
|
|
|
const aPinned = a.is_pinned ? 1 : 0;
|
|
|
|
|
|
const bPinned = b.is_pinned ? 1 : 0;
|
|
|
|
|
|
if (aPinned !== bPinned) {
|
|
|
|
|
|
return bPinned - aPinned;
|
|
|
|
|
|
}
|
|
|
|
|
|
const aTime = new Date(a.last_message_at || a.updated_at || 0).getTime();
|
|
|
|
|
|
const bTime = new Date(b.last_message_at || b.updated_at || 0).getTime();
|
2026-04-25 15:25:42 +08:00
|
|
|
|
if (Number.isNaN(aTime) || Number.isNaN(bTime)) return 0;
|
2026-03-31 18:22:24 +08:00
|
|
|
|
return bTime - aTime;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 按 message_id 合并消息并按 seq 排序
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function mergeMessagesById(base: MessageResponse[], incoming: MessageResponse[]): MessageResponse[] {
|
|
|
|
|
|
if (incoming.length === 0) return base;
|
|
|
|
|
|
const merged = new Map<string, MessageResponse>();
|
|
|
|
|
|
[...base, ...incoming].forEach(msg => {
|
|
|
|
|
|
merged.set(String(msg.id), msg);
|
|
|
|
|
|
});
|
|
|
|
|
|
return Array.from(merged.values()).sort((a, b) => a.seq - b.seq);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== Store 创建 ====================
|
|
|
|
|
|
|
|
|
|
|
|
export type MessageStore = MessageState & MessageActions;
|
|
|
|
|
|
|
|
|
|
|
|
export const useMessageStore = create<MessageStore>((set, get) => ({
|
|
|
|
|
|
// ==================== 初始状态 ====================
|
|
|
|
|
|
...initialState,
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== 获取状态 ====================
|
|
|
|
|
|
|
|
|
|
|
|
getState: () => {
|
|
|
|
|
|
const state = get();
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversations: state.conversations,
|
|
|
|
|
|
conversationList: state.conversationList,
|
|
|
|
|
|
messagesMap: state.messagesMap,
|
|
|
|
|
|
totalUnreadCount: state.totalUnreadCount,
|
|
|
|
|
|
systemUnreadCount: state.systemUnreadCount,
|
2026-04-25 00:39:40 +08:00
|
|
|
|
lastSystemMessageAt: state.lastSystemMessageAt,
|
2026-03-31 18:22:24 +08:00
|
|
|
|
isWSConnected: state.isWSConnected,
|
|
|
|
|
|
currentConversationId: state.currentConversationId,
|
|
|
|
|
|
isLoadingConversations: state.isLoadingConversations,
|
|
|
|
|
|
loadingMessagesSet: state.loadingMessagesSet,
|
|
|
|
|
|
isInitialized: state.isInitialized,
|
|
|
|
|
|
typingUsersMap: state.typingUsersMap,
|
|
|
|
|
|
mutedStatusMap: state.mutedStatusMap,
|
2026-04-25 21:23:22 +08:00
|
|
|
|
notificationMutedMap: state.notificationMutedMap,
|
2026-03-31 18:22:24 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getConversations: () => {
|
|
|
|
|
|
return get().conversationList;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getConversation: (conversationId: string) => {
|
|
|
|
|
|
const normalizedId = normalizeConversationId(conversationId);
|
|
|
|
|
|
return get().conversations.get(normalizedId) || null;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getMessages: (conversationId: string) => {
|
|
|
|
|
|
const normalizedId = normalizeConversationId(conversationId);
|
|
|
|
|
|
return get().messagesMap.get(normalizedId) || [];
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getUnreadCount: () => {
|
|
|
|
|
|
const state = get();
|
|
|
|
|
|
return {
|
|
|
|
|
|
total: state.totalUnreadCount,
|
|
|
|
|
|
system: state.systemUnreadCount,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
isConnected: () => {
|
|
|
|
|
|
return get().isWSConnected;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
isLoading: () => {
|
|
|
|
|
|
return get().isLoadingConversations;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
isLoadingMessages: (conversationId: string) => {
|
|
|
|
|
|
const normalizedId = normalizeConversationId(conversationId);
|
|
|
|
|
|
return get().loadingMessagesSet.has(normalizedId);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getTypingUsers: (groupId: string) => {
|
|
|
|
|
|
return get().typingUsersMap.get(groupId) || [];
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
isMuted: (groupId: string) => {
|
|
|
|
|
|
return get().mutedStatusMap.get(groupId) || false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getActiveConversation: () => {
|
|
|
|
|
|
return get().currentConversationId;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-04-25 21:23:22 +08:00
|
|
|
|
isNotificationMuted: (conversationId: string) => {
|
|
|
|
|
|
return get().notificationMutedMap.get(conversationId) || false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// ==================== 设置状态 ====================
|
|
|
|
|
|
|
2026-05-12 01:28:46 +08:00
|
|
|
|
setConversationsWithUnread: (
|
|
|
|
|
|
conversations: Map<string, ConversationResponse>,
|
|
|
|
|
|
totalUnread: number,
|
|
|
|
|
|
systemUnread: number,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
const conversationList = sortConversationList(conversations);
|
|
|
|
|
|
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]),
|
|
|
|
|
|
totalUnreadCount: totalUnread,
|
|
|
|
|
|
systemUnreadCount: systemUnread,
|
|
|
|
|
|
}));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
updateConversation: (conversation: ConversationResponse) => {
|
|
|
|
|
|
const id = normalizeConversationId(conversation.id);
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const newConversations = new Map(state.conversations);
|
|
|
|
|
|
newConversations.set(id, { ...conversation, id });
|
|
|
|
|
|
const conversationList = sortConversationList(newConversations);
|
2026-04-25 21:23:22 +08:00
|
|
|
|
const newNotificationMutedMap = new Map(state.notificationMutedMap);
|
|
|
|
|
|
if (conversation.notification_muted) {
|
|
|
|
|
|
newNotificationMutedMap.set(id, true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newNotificationMutedMap.delete(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
return { conversations: newConversations, conversationList, notificationMutedMap: newNotificationMutedMap };
|
2026-03-31 18:22:24 +08:00
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
removeConversation: (conversationId: string) => {
|
|
|
|
|
|
const normalizedId = normalizeConversationId(conversationId);
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const target = state.conversations.get(normalizedId);
|
|
|
|
|
|
const removedUnread = target?.unread_count || 0;
|
|
|
|
|
|
|
|
|
|
|
|
const newConversations = new Map(state.conversations);
|
|
|
|
|
|
newConversations.delete(normalizedId);
|
|
|
|
|
|
|
|
|
|
|
|
const newMessagesMap = new Map(state.messagesMap);
|
|
|
|
|
|
newMessagesMap.delete(normalizedId);
|
|
|
|
|
|
|
|
|
|
|
|
const newLoadingMessagesSet = new Set(state.loadingMessagesSet);
|
|
|
|
|
|
newLoadingMessagesSet.delete(normalizedId);
|
|
|
|
|
|
|
|
|
|
|
|
const newTotalUnreadCount = Math.max(0, state.totalUnreadCount - removedUnread);
|
|
|
|
|
|
|
|
|
|
|
|
const newCurrentConversationId = state.currentConversationId === normalizedId
|
|
|
|
|
|
? null
|
|
|
|
|
|
: state.currentConversationId;
|
|
|
|
|
|
|
|
|
|
|
|
const conversationList = sortConversationList(newConversations);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversations: newConversations,
|
|
|
|
|
|
conversationList,
|
|
|
|
|
|
messagesMap: newMessagesMap,
|
|
|
|
|
|
totalUnreadCount: newTotalUnreadCount,
|
|
|
|
|
|
currentConversationId: newCurrentConversationId,
|
|
|
|
|
|
loadingMessagesSet: newLoadingMessagesSet,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setMessages: (conversationId: string, messages: MessageResponse[]) => {
|
|
|
|
|
|
const normalizedId = normalizeConversationId(conversationId);
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const newMessagesMap = new Map(state.messagesMap);
|
|
|
|
|
|
newMessagesMap.set(normalizedId, messages);
|
|
|
|
|
|
return { messagesMap: newMessagesMap };
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setUnreadCount: (total: number, system: number) => {
|
|
|
|
|
|
set({ totalUnreadCount: total, systemUnreadCount: system });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-04-25 00:39:40 +08:00
|
|
|
|
setLastSystemMessageAt: (time: string | null) => {
|
|
|
|
|
|
set({ lastSystemMessageAt: time });
|
2026-04-25 15:09:00 +08:00
|
|
|
|
if (time) {
|
|
|
|
|
|
AsyncStorage.setItem(LAST_SYSTEM_MESSAGE_AT_KEY, time).catch(() => {});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
AsyncStorage.removeItem(LAST_SYSTEM_MESSAGE_AT_KEY).catch(() => {});
|
|
|
|
|
|
}
|
2026-04-25 00:39:40 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
setSSEConnected: (connected: boolean) => {
|
|
|
|
|
|
set({ isWSConnected: connected });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setCurrentConversation: (conversationId: string | null) => {
|
|
|
|
|
|
set({ currentConversationId: conversationId });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setLoading: (loading: boolean) => {
|
|
|
|
|
|
set({ isLoadingConversations: loading });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setLoadingMessages: (conversationId: string, loading: boolean) => {
|
|
|
|
|
|
const normalizedId = normalizeConversationId(conversationId);
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const newLoadingMessagesSet = new Set(state.loadingMessagesSet);
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
newLoadingMessagesSet.add(normalizedId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newLoadingMessagesSet.delete(normalizedId);
|
|
|
|
|
|
}
|
|
|
|
|
|
return { loadingMessagesSet: newLoadingMessagesSet };
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setTypingUsers: (groupId: string, users: string[]) => {
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const newTypingUsersMap = new Map(state.typingUsersMap);
|
|
|
|
|
|
newTypingUsersMap.set(groupId, users);
|
|
|
|
|
|
return { typingUsersMap: newTypingUsersMap };
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
setMutedStatus: (groupId: string, isMuted: boolean) => {
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const newMutedStatusMap = new Map(state.mutedStatusMap);
|
|
|
|
|
|
newMutedStatusMap.set(groupId, isMuted);
|
|
|
|
|
|
return { mutedStatusMap: newMutedStatusMap };
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-04-25 21:23:22 +08:00
|
|
|
|
setNotificationMuted: (conversationId: string, notificationMuted: boolean) => {
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const newNotificationMutedMap = new Map(state.notificationMutedMap);
|
|
|
|
|
|
newNotificationMutedMap.set(conversationId, notificationMuted);
|
|
|
|
|
|
return { notificationMutedMap: newNotificationMutedMap };
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
setInitialized: (initialized: boolean) => {
|
|
|
|
|
|
set({ isInitialized: initialized });
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-12 18:04:32 +08:00
|
|
|
|
updateConversationWithUnread: (
|
|
|
|
|
|
conversation: ConversationResponse,
|
|
|
|
|
|
totalUnread: number,
|
|
|
|
|
|
systemUnread: number,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
const id = normalizeConversationId(conversation.id);
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const newConversations = new Map(state.conversations);
|
|
|
|
|
|
newConversations.set(id, { ...conversation, id });
|
|
|
|
|
|
const conversationList = sortConversationList(newConversations);
|
|
|
|
|
|
const newNotificationMutedMap = new Map(state.notificationMutedMap);
|
|
|
|
|
|
if (conversation.notification_muted) {
|
|
|
|
|
|
newNotificationMutedMap.set(id, true);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newNotificationMutedMap.delete(id);
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversations: newConversations,
|
|
|
|
|
|
conversationList,
|
|
|
|
|
|
notificationMutedMap: newNotificationMutedMap,
|
|
|
|
|
|
totalUnreadCount: totalUnread,
|
|
|
|
|
|
systemUnreadCount: systemUnread,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
incrementUnreadAtomic: (conversationId: string) => {
|
|
|
|
|
|
const normalizedId = normalizeConversationId(conversationId);
|
|
|
|
|
|
set(state => {
|
|
|
|
|
|
const conversation = state.conversations.get(normalizedId);
|
|
|
|
|
|
if (!conversation) return state;
|
|
|
|
|
|
|
|
|
|
|
|
const newConversations = new Map(state.conversations);
|
|
|
|
|
|
newConversations.set(normalizedId, {
|
|
|
|
|
|
...conversation,
|
|
|
|
|
|
unread_count: (conversation.unread_count || 0) + 1,
|
|
|
|
|
|
});
|
|
|
|
|
|
const conversationList = sortConversationList(newConversations);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
conversations: newConversations,
|
|
|
|
|
|
conversationList,
|
|
|
|
|
|
totalUnreadCount: state.totalUnreadCount + 1,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-03-31 18:22:24 +08:00
|
|
|
|
// ==================== 重置 ====================
|
|
|
|
|
|
|
|
|
|
|
|
reset: () => {
|
|
|
|
|
|
set({
|
|
|
|
|
|
...initialState,
|
|
|
|
|
|
conversations: new Map(),
|
|
|
|
|
|
messagesMap: new Map(),
|
|
|
|
|
|
loadingMessagesSet: new Set(),
|
|
|
|
|
|
typingUsersMap: new Map(),
|
|
|
|
|
|
mutedStatusMap: new Map(),
|
2026-04-25 21:23:22 +08:00
|
|
|
|
notificationMutedMap: new Map(),
|
2026-03-31 18:22:24 +08:00
|
|
|
|
});
|
2026-04-25 15:09:00 +08:00
|
|
|
|
AsyncStorage.removeItem(LAST_SYSTEM_MESSAGE_AT_KEY).catch(() => {});
|
2026-03-31 18:22:24 +08:00
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2026-04-25 15:09:00 +08:00
|
|
|
|
export async function loadPersistedLastSystemMessageAt(): Promise<void> {
|
|
|
|
|
|
const time = await AsyncStorage.getItem(LAST_SYSTEM_MESSAGE_AT_KEY);
|
|
|
|
|
|
if (time) {
|
|
|
|
|
|
useMessageStore.setState({ lastSystemMessageAt: time });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|