This commit is contained in:
2026-04-25 16:36:34 +08:00
commit db90e7579b
1876 changed files with 189777 additions and 0 deletions

View File

@@ -0,0 +1,414 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { ChatConverseInfo } from '../../model/converse';
import type {
ChatMessage,
ChatMessageReaction,
LocalChatMessage,
SendMessagePayload,
} from '../../model/message';
import _uniqBy from 'lodash/uniqBy';
import _orderBy from 'lodash/orderBy';
import _last from 'lodash/last';
import { isLocalMessageId, isValidStr } from '../../utils/string-helper';
import type { InboxItem } from '../../model/inbox';
export interface ChatConverseState extends ChatConverseInfo {
messages: LocalChatMessage[];
hasFetchedHistory: boolean;
/**
* 判定是否还有更多的信息
*/
hasMoreMessage: boolean;
}
export interface ChatState {
currentConverseId: string | null; // 当前活跃的会话id
converses: Record<string, ChatConverseState>; // <会话Id, 会话信息>
ack: Record<string, string>; // <会话Id, 本地最后一条会话Id>
inbox: InboxItem[];
/**
* 会话最新消息mapping
* <会话Id, 远程会话列表最后一条会话Id>
*/
lastMessageMap: Record<string, string>;
}
const initialState: ChatState = {
currentConverseId: null,
converses: {},
ack: {},
inbox: [],
lastMessageMap: {},
};
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {
updateCurrentConverseId(state, action: PayloadAction<string | null>) {
state.currentConverseId = action.payload;
},
/**
* 设置会话信息
*/
setConverseInfo(state, action: PayloadAction<ChatConverseInfo>) {
const converseId = action.payload._id;
const originInfo = state.converses[converseId]
? { ...state.converses[converseId] }
: { messages: [], hasFetchedHistory: false, hasMoreMessage: true };
state.converses[converseId] = {
...originInfo,
...action.payload,
};
},
/**
* 追加消息
* 会根据id进行一次排序以确保顺序
*/
appendConverseMessage(
state,
action: PayloadAction<{
converseId: string;
messages: ChatMessage[];
}>
) {
const { converseId, messages } = action.payload;
if (!state.converses[converseId]) {
// 没有会话信息, 请先设置会话信息
console.error('没有会话信息, 请先设置会话信息');
return;
}
// NOTICE: 按照该规则能确保本地消息一直在最后因为l大于任何ObjectId
const newMessages = _orderBy(
_uniqBy([...state.converses[converseId].messages, ...messages], '_id'),
'_id',
'asc'
);
state.converses[converseId].messages = newMessages;
/**
* 如果在当前会话中,则暂时不更新最后收到的消息的本地状态,避免可能出现的瞬间更新最后消息(出现小红点) 但是会立即已读(小红点消失)
* 所以仅对非当前会话的消息进行更新最后消息
*/
if (state.currentConverseId !== converseId) {
const lastMessageId = _last(
newMessages.filter((m) => !isLocalMessageId(m._id))
)?._id;
if (isValidStr(lastMessageId)) {
state.lastMessageMap[converseId] = lastMessageId;
}
}
},
/**
* 追加本地消息消息
*/
appendLocalMessage(
state,
action: PayloadAction<{
author?: string;
localMessageId: string;
payload: SendMessagePayload;
}>
) {
const { author, localMessageId, payload } = action.payload;
const { converseId, groupId, content, meta } = payload;
if (!state.converses[converseId]) {
// 没有会话信息, 请先设置会话信息
console.error('没有会话信息, 请先设置会话信息');
return;
}
const message: LocalChatMessage = {
_id: localMessageId,
author,
groupId,
converseId,
content,
meta: meta as Record<string, unknown>,
isLocal: true,
};
const newMessages = _orderBy(
_uniqBy([...state.converses[converseId].messages, message], '_id'),
'_id',
'asc'
);
state.converses[converseId].messages = newMessages;
},
/**
* 初始化历史信息
*/
initialHistoryMessage(
state,
action: PayloadAction<{
converseId: string;
historyMessages: ChatMessage[];
}>
) {
const { converseId, historyMessages } = action.payload;
if (!state.converses[converseId]) {
// 没有会话信息, 请先设置会话信息
console.error('没有会话信息, 请先设置会话信息');
return;
}
chatSlice.caseReducers.appendConverseMessage(
state,
chatSlice.actions.appendConverseMessage({
converseId,
messages: [...historyMessages],
})
);
if (historyMessages.length < 50) {
state.converses[converseId].hasMoreMessage = false;
}
state.converses[converseId].hasFetchedHistory = true;
},
/**
* 追加历史信息
*/
appendHistoryMessage(
state,
action: PayloadAction<{
converseId: string;
historyMessages: ChatMessage[];
}>
) {
const { converseId, historyMessages } = action.payload;
if (!state.converses[converseId]) {
// 没有会话信息, 请先设置会话信息
console.error('没有会话信息, 请先设置会话信息');
return;
}
chatSlice.caseReducers.appendConverseMessage(
state,
chatSlice.actions.appendConverseMessage({
converseId,
messages: [...historyMessages],
})
);
if (historyMessages.length < 50) {
state.converses[converseId].hasMoreMessage = false;
}
state.converses[converseId].hasFetchedHistory = true;
},
removeConverse(state, action: PayloadAction<{ converseId: string }>) {
const { converseId } = action.payload;
if (!state.converses[converseId]) {
return;
}
delete state.converses[converseId];
},
/**
* 清理所有会话信息
*/
clearAllConverses(state) {
state.converses = {};
},
/**
* 设置已读消息
*/
setConverseAck(
state,
action: PayloadAction<{
converseId: string;
lastMessageId: string;
}>
) {
const { converseId, lastMessageId } = action.payload;
state.ack[converseId] = lastMessageId;
},
/**
* 更新消息信息
*/
updateMessageInfo(
state,
action: PayloadAction<{
messageId?: string;
message: Partial<LocalChatMessage>;
}>
) {
const { message } = action.payload;
const messageId = action.payload.messageId ?? message._id;
const converseId = message.converseId;
if (!converseId) {
console.warn('Not found converse id,', message);
return;
}
const converse = state.converses[converseId];
if (!converse) {
console.warn('Not found converse,', converseId);
return;
}
const index = converse.messages.findIndex((m) => m._id === messageId);
if (index >= 0) {
converse.messages[index] = {
...converse.messages[index],
...message,
};
}
},
/**
* 删除消息
*/
deleteMessageById(
state,
action: PayloadAction<{
converseId: string;
messageId: string;
}>
) {
const { converseId, messageId } = action.payload;
const converse = state.converses[converseId];
if (!converse) {
console.warn('Not found converse,', converseId);
return;
}
const index = converse.messages.findIndex((m) => m._id === messageId);
if (index >= 0) {
converse.messages.splice(index, 1);
}
},
/**
* 设置远程的最后一条会话的id
*/
setLastMessageMap(
state,
action: PayloadAction<
{
converseId: string;
lastMessageId: string;
}[]
>
) {
const list = action.payload;
if (Array.isArray(list)) {
list.forEach((item) => {
state.lastMessageMap[item.converseId] = item.lastMessageId;
});
}
},
/**
* 追加消息反应
*/
appendMessageReaction(
state,
action: PayloadAction<{
converseId: string;
messageId: string;
reaction: ChatMessageReaction;
}>
) {
const { converseId, messageId, reaction } = action.payload;
const converse = state.converses[converseId];
if (!converse) {
console.warn('Not found converse,', converseId);
return;
}
const message = converse.messages.find((m) => m._id === messageId);
if (!message) {
console.warn('Not found message,', messageId);
return;
}
if (!Array.isArray(message.reactions)) {
message.reactions = [];
}
message.reactions.push(reaction);
},
/**
* 移除消息反应
*/
removeMessageReaction(
state,
action: PayloadAction<{
converseId: string;
messageId: string;
reaction: ChatMessageReaction;
}>
) {
const { converseId, messageId, reaction } = action.payload;
const converse = state.converses[converseId];
if (!converse) {
console.warn('Not found converse,', converseId);
return;
}
const message = converse.messages.find((m) => m._id === messageId);
if (!message) {
console.warn('Not found message,', messageId);
return;
}
if (!Array.isArray(message.reactions)) {
message.reactions = [];
}
const reactionIndex = message.reactions.findIndex(
(r) => r.name === reaction.name && r.author === reaction.author
);
message.reactions.splice(reactionIndex, 1);
},
/**
* 设置收件箱
*/
setInboxList(state, action: PayloadAction<InboxItem[]>) {
const list = action.payload;
state.inbox = list;
},
/**
* 增加收件箱项目
*/
appendInboxItem(state, action: PayloadAction<InboxItem>) {
state.inbox.push(action.payload);
},
/**
* 设置收件箱
*/
setInboxItemAck(state, action: PayloadAction<string>) {
const inboxItemId = action.payload;
const item = state.inbox.find((item) => item._id === inboxItemId);
if (item) {
item.readed = true;
}
},
},
});
export const chatActions = chatSlice.actions;
export const chatReducer = chatSlice.reducer;

View File

@@ -0,0 +1,33 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface GlobalState {
/**
* 网络状态
*/
networkStatus: 'initial' | 'connected' | 'reconnecting' | 'disconnected';
reconnectNum: number;
}
const initialState: GlobalState = {
networkStatus: 'initial',
reconnectNum: 0,
};
const globalSlice = createSlice({
name: 'global',
initialState,
reducers: {
setNetworkStatus(
state,
action: PayloadAction<GlobalState['networkStatus']>
) {
state.networkStatus = action.payload;
},
incReconnectNum(state) {
state.reconnectNum += 1;
},
},
});
export const globalActions = globalSlice.actions;
export const globalReducer = globalSlice.reducer;

View File

@@ -0,0 +1,103 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { GroupInfo } from '../../model/group';
export interface GroupState {
groups: Record<string, GroupInfo>;
}
const initialState: GroupState = {
groups: {},
};
const groupSlice = createSlice({
name: 'group',
initialState,
reducers: {
/**
* 追加或更新群组信息
*/
appendGroups(state, action: PayloadAction<GroupInfo[]>) {
const groups = action.payload;
for (const group of groups) {
state.groups[group._id] = {
...state.groups[group._id],
...group,
};
}
},
updateGroup(state, action: PayloadAction<GroupInfo>) {
const group = action.payload;
const groupId = group._id;
if (state.groups[groupId]) {
// NOTICE: updateGroup 只会去更新,不会去添加新的
state.groups[groupId] = {
...state.groups[groupId],
...group,
};
}
},
removeGroup(state, action: PayloadAction<string>) {
const groupId = action.payload;
delete state.groups[groupId];
},
pinGroupPanel(
state,
action: PayloadAction<{
groupId: string;
panelId: string;
}>
) {
const { groupId, panelId } = action.payload;
if (state.groups[groupId]) {
// NOTICE: updateGroup 只会去更新,不会去添加新的
state.groups[groupId] = {
...state.groups[groupId],
pinnedPanelId: panelId,
};
}
},
unpinGroupPanel(
state,
action: PayloadAction<{
groupId: string;
}>
) {
const { groupId } = action.payload;
if (state.groups[groupId]) {
// NOTICE: updateGroup 只会去更新,不会去添加新的
state.groups[groupId] = {
...state.groups[groupId],
pinnedPanelId: undefined,
};
}
},
updateGroupConfig(
state,
action: PayloadAction<{
groupId: string;
configName: string;
configValue: any;
}>
) {
const { groupId, configName, configValue } = action.payload;
const groupInfo = state.groups[groupId];
if (groupInfo) {
state.groups[groupId] = {
...groupInfo,
config: {
...(groupInfo.config ?? {}),
[configName]: configValue,
},
};
}
},
},
});
export const groupActions = groupSlice.actions;
export const groupReducer = groupSlice.reducer;

View File

@@ -0,0 +1,22 @@
import { combineReducers } from '@reduxjs/toolkit';
import { userReducer } from './user';
import { chatReducer } from './chat';
import { groupReducer } from './group';
import { uiReducer } from './ui';
import { globalReducer } from './global';
export const appReducer = combineReducers({
global: globalReducer,
user: userReducer,
chat: chatReducer,
group: groupReducer,
ui: uiReducer,
});
export type AppState = ReturnType<typeof appReducer>;
export { globalActions } from './global';
export { userActions } from './user';
export { chatActions } from './chat';
export { groupActions } from './group';
export { uiActions } from './ui';

View File

@@ -0,0 +1,28 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface UIState {
panelWinUrls: string[];
}
const initialState: UIState = {
panelWinUrls: [],
};
const uiSlice = createSlice({
name: 'ui',
initialState,
reducers: {
addPanelWindowUrl(state, action: PayloadAction<{ url: string }>) {
const panelUrl = action.payload.url;
state.panelWinUrls.push(panelUrl);
},
deletePanelWindowUrl(state, action: PayloadAction<{ url: string }>) {
const panelUrl = action.payload.url;
const index = state.panelWinUrls.indexOf(panelUrl);
state.panelWinUrls.splice(index, 1);
},
},
});
export const uiActions = uiSlice.actions;
export const uiReducer = uiSlice.reducer;

View File

@@ -0,0 +1,101 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import _set from 'lodash/set';
import type { UserLoginInfo } from '../../model/user';
import type { FriendRequest } from '../../model/friend';
export interface FriendInfo {
id: string;
nickname?: string;
}
export interface UserState {
info: UserLoginInfo | null;
friends: FriendInfo[]; // 好友的id列表
friendRequests: FriendRequest[];
}
const initialState: UserState = {
info: null,
friends: [],
friendRequests: [],
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUserInfo(state, action: PayloadAction<UserLoginInfo>) {
state.info = action.payload;
},
setUserInfoField(
state,
action: PayloadAction<{ fieldName: keyof UserLoginInfo; fieldValue: any }>
) {
const { fieldName, fieldValue } = action.payload;
if (state.info === null) {
return;
}
_set(state.info, [fieldName], fieldValue);
},
setUserInfoExtra(
state,
action: PayloadAction<{ fieldName: string; fieldValue: any }>
) {
const { fieldName, fieldValue } = action.payload;
if (state.info === null) {
return;
}
_set(state.info, ['extra', fieldName], fieldValue);
},
setFriendList(state, action: PayloadAction<FriendInfo[]>) {
state.friends = action.payload;
},
setFriendRequests(state, action: PayloadAction<FriendRequest[]>) {
state.friendRequests = action.payload;
},
appendFriend(state, action: PayloadAction<FriendInfo>) {
if (state.friends.some((id) => id === action.payload)) {
return;
}
state.friends.push(action.payload);
},
removeFriend(state, action: PayloadAction<string>) {
const friendId = action.payload;
const index = state.friends.findIndex((item) => item.id === friendId);
if (index >= 0) {
state.friends.splice(index, 1);
}
},
appendFriendRequest(state, action: PayloadAction<FriendRequest>) {
if (state.friendRequests.some(({ _id }) => _id === action.payload._id)) {
return;
}
state.friendRequests.push(action.payload);
},
removeFriendRequest(state, action: PayloadAction<string>) {
const index = state.friendRequests.findIndex(
({ _id }) => _id === action.payload
);
if (index >= 0) {
state.friendRequests.splice(index, 1);
}
},
setFriendNickname(
state,
action: PayloadAction<{ friendId: string; nickname: string }>
) {
const { friendId, nickname } = action.payload;
const target = state.friends.find((f) => f.id === friendId);
if (target) {
target.nickname = nickname;
}
},
},
});
export const userActions = userSlice.actions;
export const userReducer = userSlice.reducer;