优化
This commit is contained in:
41
client/shared/redux/hooks/useAckInfo.ts
Normal file
41
client/shared/redux/hooks/useAckInfo.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useAppDispatch, useAppSelector } from './useAppSelector';
|
||||
import { chatActions } from '../slices';
|
||||
import { useEvent } from '../../hooks/useEvent';
|
||||
import { getCachedAckInfo } from '../../cache/cache';
|
||||
|
||||
export function useAckInfoChecker() {
|
||||
const ack = useAppSelector((state) => state.chat.ack);
|
||||
const lastMessageMap = useAppSelector((state) => state.chat.lastMessageMap);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const ensureAckInfo = useEvent((converseId: string) => {
|
||||
if (
|
||||
ack[converseId] === undefined ||
|
||||
lastMessageMap[converseId] === undefined
|
||||
) {
|
||||
getCachedAckInfo(converseId).then((info) => {
|
||||
if (info.ack?.lastMessageId) {
|
||||
dispatch(
|
||||
chatActions.setConverseAck({
|
||||
converseId,
|
||||
lastMessageId: info.ack.lastMessageId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (info.lastMessage?.lastMessageId) {
|
||||
dispatch(
|
||||
chatActions.setLastMessageMap([
|
||||
{
|
||||
converseId,
|
||||
lastMessageId: info.lastMessage.lastMessageId,
|
||||
},
|
||||
])
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { ensureAckInfo };
|
||||
}
|
||||
15
client/shared/redux/hooks/useAppSelector.ts
Normal file
15
client/shared/redux/hooks/useAppSelector.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { AppState } from '../slices';
|
||||
import { useSelector, useDispatch, useStore } from 'react-redux';
|
||||
|
||||
export function useAppSelector<T>(
|
||||
selector: (state: AppState) => T,
|
||||
equalityFn?: (left: T, right: T) => boolean
|
||||
) {
|
||||
return useSelector<AppState, T>(selector, equalityFn);
|
||||
}
|
||||
|
||||
export const useAppDispatch = useDispatch;
|
||||
|
||||
export function useAppStore<AppState>() {
|
||||
return useStore<AppState>();
|
||||
}
|
||||
31
client/shared/redux/hooks/useConverse.ts
Normal file
31
client/shared/redux/hooks/useConverse.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useMemo } from 'react';
|
||||
import { ChatConverseType } from '../../model/converse';
|
||||
import type { ChatConverseState } from '../slices/chat';
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
|
||||
/**
|
||||
* 获取私信会话列表
|
||||
* 并补充一些信息
|
||||
*/
|
||||
export function useDMConverseList(): ChatConverseState[] {
|
||||
const converses = useAppSelector((state) => state.chat.converses);
|
||||
const lastMessageMap = useAppSelector((state) => state.chat.lastMessageMap);
|
||||
|
||||
const filteredConverse = useMemo(
|
||||
() =>
|
||||
Object.entries(converses)
|
||||
.filter(([, info]) =>
|
||||
[ChatConverseType.DM, ChatConverseType.Multi].includes(info.type)
|
||||
)
|
||||
.map(([, info]) => info),
|
||||
[converses]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return filteredConverse.sort((a, b) => {
|
||||
return (lastMessageMap[a._id] ?? '') < (lastMessageMap[b._id] ?? '')
|
||||
? 1
|
||||
: -1;
|
||||
});
|
||||
}, [filteredConverse, lastMessageMap]);
|
||||
}
|
||||
67
client/shared/redux/hooks/useConverseAck.ts
Normal file
67
client/shared/redux/hooks/useConverseAck.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { useRef } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from './useAppSelector';
|
||||
import _debounce from 'lodash/debounce';
|
||||
import { isLocalMessageId, isValidStr } from '../../utils/string-helper';
|
||||
import { chatActions } from '../slices';
|
||||
import { updateAck } from '../../model/converse';
|
||||
import { useMemoizedFn } from '../../hooks/useMemoizedFn';
|
||||
|
||||
const updateAckDebounce = _debounce(
|
||||
(converseId: string, lastMessageId: string) => {
|
||||
updateAck(converseId, lastMessageId);
|
||||
},
|
||||
1000,
|
||||
{ leading: true, trailing: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* 会话已读信息管理
|
||||
*/
|
||||
export function useConverseAck(converseId: string) {
|
||||
const dispatch = useAppDispatch();
|
||||
const converseLastMessage = useAppSelector(
|
||||
(state) => state.chat.lastMessageMap[converseId]
|
||||
);
|
||||
|
||||
const lastMessageIdRef = useRef('');
|
||||
lastMessageIdRef.current = useAppSelector(
|
||||
(state) => state.chat.ack[converseId] ?? ''
|
||||
);
|
||||
|
||||
const setConverseAck = useMemoizedFn(
|
||||
(converseId: string, lastMessageId: string) => {
|
||||
if (isLocalMessageId(lastMessageId)) {
|
||||
// 跳过本地消息
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
isValidStr(lastMessageIdRef.current) &&
|
||||
lastMessageId <= lastMessageIdRef.current
|
||||
) {
|
||||
// 更新的数字比较小,跳过
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(chatActions.setConverseAck({ converseId, lastMessageId }));
|
||||
updateAckDebounce(converseId, lastMessageId);
|
||||
lastMessageIdRef.current = lastMessageId;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 更新会话最新消息
|
||||
*/
|
||||
const updateConverseAck = useMemoizedFn((lastMessageId: string) => {
|
||||
setConverseAck(converseId, lastMessageId);
|
||||
});
|
||||
|
||||
/**
|
||||
* 标记为会话已读
|
||||
*/
|
||||
const markConverseAllAck = useMemoizedFn(() => {
|
||||
updateConverseAck(converseLastMessage);
|
||||
});
|
||||
|
||||
return { updateConverseAck, markConverseAllAck };
|
||||
}
|
||||
227
client/shared/redux/hooks/useConverseMessage.ts
Normal file
227
client/shared/redux/hooks/useConverseMessage.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
import { useEffect } from 'react';
|
||||
import { ensureDMConverse } from '../../helper/converse-helper';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { showErrorToasts } from '../../manager/ui';
|
||||
import {
|
||||
fetchConverseMessage,
|
||||
sendMessage,
|
||||
SendMessagePayload,
|
||||
} from '../../model/message';
|
||||
import { chatActions } from '../slices';
|
||||
import { useAppDispatch, useAppSelector } from './useAppSelector';
|
||||
import _get from 'lodash/get';
|
||||
import _isNil from 'lodash/isNil';
|
||||
import _uniqueId from 'lodash/uniqueId';
|
||||
import {
|
||||
ChatConverseState,
|
||||
isValidStr,
|
||||
t,
|
||||
useAsyncRequest,
|
||||
useChatBoxContext,
|
||||
useEvent,
|
||||
useMemoizedFn,
|
||||
} from '../..';
|
||||
import { MessageHelper } from '../../utils/message-helper';
|
||||
import { ChatConverseType } from '../../model/converse';
|
||||
import { sharedEvent } from '../../event';
|
||||
import { useUpdateRef } from '../../hooks/useUpdateRef';
|
||||
|
||||
const genLocalMessageId = () => _uniqueId('localMessage_');
|
||||
|
||||
function useHandleSendMessage() {
|
||||
const userId = useAppSelector((state) => state.user.info?._id);
|
||||
const dispatch = useAppDispatch();
|
||||
const { hasContext, replyMsg, clearReplyMsg } = useChatBoxContext();
|
||||
const replyMsgRef = useUpdateRef(replyMsg); // NOTICE: 这个是为了修复一个边界case: 当先输入文本再选中消息回复时,直接发送无法带上回复信息
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
const handleSendMessage = useEvent((payload: SendMessagePayload) => {
|
||||
// 输入合法性检测
|
||||
if (payload.content === '') {
|
||||
showErrorToasts(t('无法发送空消息'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasContext === true) {
|
||||
// 如果有上下文, 则组装payload
|
||||
const msgHelper = new MessageHelper(payload);
|
||||
if (!_isNil(replyMsgRef.current)) {
|
||||
msgHelper.setReplyMsg(replyMsgRef.current);
|
||||
clearReplyMsg();
|
||||
}
|
||||
|
||||
payload = msgHelper.generatePayload();
|
||||
}
|
||||
|
||||
const localMessageId = genLocalMessageId();
|
||||
dispatch(
|
||||
chatActions.appendLocalMessage({
|
||||
author: userId,
|
||||
localMessageId,
|
||||
payload,
|
||||
})
|
||||
);
|
||||
|
||||
sendMessage(payload)
|
||||
.then((message) => {
|
||||
dispatch(
|
||||
chatActions.deleteMessageById({
|
||||
converseId: payload.converseId,
|
||||
messageId: localMessageId,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
chatActions.appendConverseMessage({
|
||||
converseId: payload.converseId,
|
||||
messages: [message],
|
||||
})
|
||||
); // 确保删除消息后会立即显示消息,适用于网络卡顿的情况,同时远程的socket会接收到消息广播,在redux中会自动去重
|
||||
|
||||
sharedEvent.emit('sendMessage', payload);
|
||||
})
|
||||
.catch((err) => {
|
||||
showErrorToasts(err);
|
||||
dispatch(
|
||||
chatActions.updateMessageInfo({
|
||||
messageId: localMessageId,
|
||||
message: {
|
||||
sendFailed: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
return handleSendMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话消息管理
|
||||
*/
|
||||
interface ConverseContext {
|
||||
converseId: string;
|
||||
isGroup: boolean;
|
||||
}
|
||||
export function useConverseMessage(context: ConverseContext) {
|
||||
const { converseId, isGroup } = context;
|
||||
const converse = useAppSelector<ChatConverseState | undefined>(
|
||||
(state) => state.chat.converses[converseId]
|
||||
);
|
||||
const reconnectNum = useAppSelector((state) => state.global.reconnectNum);
|
||||
const hasMoreMessage = converse?.hasMoreMessage ?? true;
|
||||
const dispatch = useAppDispatch();
|
||||
const messages = converse?.messages ?? [];
|
||||
const currentUserId = useAppSelector((state) => state.user.info?._id);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(chatActions.updateCurrentConverseId(converseId));
|
||||
|
||||
return () => {
|
||||
dispatch(chatActions.updateCurrentConverseId(null));
|
||||
};
|
||||
}, [converseId]);
|
||||
|
||||
// NOTICE: 该hook只会在converseId变化和重新链接时执行
|
||||
const { loading, error } = useAsync(async () => {
|
||||
if (!currentUserId) {
|
||||
// 如果当前用户不存在则跳过逻辑
|
||||
return;
|
||||
}
|
||||
|
||||
if (!converse) {
|
||||
// 如果是一个新会话(或者当前会话列表中没有)
|
||||
if (!isGroup) {
|
||||
// 如果是私信会话
|
||||
// Step 1. 创建会话 并确保私信列表中存在该会话
|
||||
const converse = await ensureDMConverse(converseId, currentUserId);
|
||||
dispatch(chatActions.setConverseInfo(converse));
|
||||
} else {
|
||||
// 如果是群组会话(文本频道)
|
||||
// Step 1. 确保群组会话存在
|
||||
dispatch(
|
||||
chatActions.setConverseInfo({
|
||||
_id: converseId,
|
||||
name: '',
|
||||
type: ChatConverseType.Group,
|
||||
members: [],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2. 拉取消息
|
||||
const historyMessages = await fetchConverseMessage(converseId);
|
||||
dispatch(
|
||||
chatActions.initialHistoryMessage({
|
||||
converseId,
|
||||
historyMessages,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// 已存在会话
|
||||
if (!converse.hasFetchedHistory) {
|
||||
// 没有获取过历史消息
|
||||
// 拉取历史消息
|
||||
const startId = _isNil(converse.messages[0])
|
||||
? undefined
|
||||
: converse.messages[0]._id;
|
||||
const messages = await fetchConverseMessage(converseId, startId);
|
||||
dispatch(
|
||||
chatActions.initialHistoryMessage({
|
||||
converseId,
|
||||
historyMessages: messages,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [converseId, reconnectNum, currentUserId]);
|
||||
|
||||
// 加载更多消息
|
||||
const [{ loading: isLoadingMore }, _handleFetchMoreMessage] =
|
||||
useAsyncRequest(async () => {
|
||||
const firstMessageId = _get(messages, [0, '_id']);
|
||||
if (!isValidStr(firstMessageId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasMoreMessage === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const olderMessages = await fetchConverseMessage(
|
||||
converseId,
|
||||
firstMessageId
|
||||
);
|
||||
dispatch(
|
||||
chatActions.appendHistoryMessage({
|
||||
converseId,
|
||||
historyMessages: olderMessages,
|
||||
})
|
||||
);
|
||||
}, [converseId, hasMoreMessage, _get(messages, [0, '_id'])]);
|
||||
|
||||
/**
|
||||
* 加载更多
|
||||
* 同一时间只能请求一次
|
||||
*/
|
||||
const handleFetchMoreMessage = useMemoizedFn(async () => {
|
||||
if (isLoadingMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _handleFetchMoreMessage();
|
||||
});
|
||||
|
||||
const handleSendMessage = useHandleSendMessage();
|
||||
|
||||
return {
|
||||
messages,
|
||||
loading,
|
||||
error,
|
||||
isLoadingMore,
|
||||
hasMoreMessage,
|
||||
handleFetchMoreMessage,
|
||||
handleSendMessage,
|
||||
};
|
||||
}
|
||||
23
client/shared/redux/hooks/useDMConverseName.ts
Normal file
23
client/shared/redux/hooks/useDMConverseName.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { getDMConverseName } from '../../helper/converse-helper';
|
||||
import { isValidStr, useAppSelector, useAsync } from '../../index';
|
||||
import type { ChatConverseState } from '../slices/chat';
|
||||
import { useUserId } from './useUserInfo';
|
||||
import type { FriendInfo } from '../slices/user';
|
||||
|
||||
export function useDMConverseName(converse: ChatConverseState | undefined) {
|
||||
const userId = useUserId();
|
||||
const friends: FriendInfo[] = useAppSelector((state) => state.user.friends);
|
||||
const { value: name = '' } = useAsync(async () => {
|
||||
if (!converse) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isValidStr(userId)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return getDMConverseName(userId, converse);
|
||||
}, [userId, converse?.name, converse?.members.join(','), friends]);
|
||||
|
||||
return name;
|
||||
}
|
||||
33
client/shared/redux/hooks/useFriendNickname.ts
Normal file
33
client/shared/redux/hooks/useFriendNickname.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react';
|
||||
import { buildFriendNicknameMap } from '../../helper/converse-helper';
|
||||
import { isValidStr } from '../../utils/string-helper';
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
|
||||
/**
|
||||
* 获取好友自定义的昵称
|
||||
* 用于覆盖原始昵称
|
||||
*
|
||||
* @param userId 用户id
|
||||
*/
|
||||
export function useFriendNickname(userId: string): string | null {
|
||||
const nickname = useAppSelector(
|
||||
(state) => state.user.friends.find((f) => f.id === userId)?.nickname
|
||||
);
|
||||
|
||||
if (isValidStr(nickname)) {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useFriendNicknameMap(): Record<string, string> {
|
||||
const friends = useAppSelector((state) => state.user.friends);
|
||||
|
||||
const friendNicknameMap = useMemo(
|
||||
() => buildFriendNicknameMap(friends),
|
||||
[friends]
|
||||
);
|
||||
|
||||
return friendNicknameMap;
|
||||
}
|
||||
87
client/shared/redux/hooks/useGroup.ts
Normal file
87
client/shared/redux/hooks/useGroup.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useUserInfoList } from '../..';
|
||||
import type { GroupInfo, GroupPanel } from '../../model/group';
|
||||
import type { UserBaseInfo } from '../../model/user';
|
||||
import { isValidStr } from '../../utils/string-helper';
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
import { useUnread } from './useUnread';
|
||||
import { useUserId } from './useUserInfo';
|
||||
import _compact from 'lodash/compact';
|
||||
|
||||
/**
|
||||
* 获取群组信息
|
||||
*/
|
||||
export function useGroupInfo(groupId: string): GroupInfo | null {
|
||||
return useAppSelector((state) => state.group.groups[groupId]) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群组中所有成员的uuid列表
|
||||
*/
|
||||
export function useGroupMemberIds(groupId: string): string[] {
|
||||
const groupInfo = useGroupInfo(groupId);
|
||||
const members = groupInfo?.members ?? [];
|
||||
const groupMemberIds = useMemo(() => members.map((m) => m.userId), [members]);
|
||||
|
||||
return groupMemberIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群组中成员信息的详细列表
|
||||
*/
|
||||
export function useGroupMemberInfos(groupId: string): UserBaseInfo[] {
|
||||
const groupMemberIds = useGroupMemberIds(groupId);
|
||||
const userInfos = useUserInfoList(groupMemberIds);
|
||||
|
||||
return _compact(userInfos); // 开发环境,可能会出现member里面id为不存在的脏数据,生产环境原则上不会出现,兼容一下
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群组面板列表
|
||||
*/
|
||||
export function useGroupPanels(groupId: string): GroupPanel[] {
|
||||
const groupInfo = useGroupInfo(groupId);
|
||||
|
||||
return useMemo(() => groupInfo?.panels ?? [], [groupInfo]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群组面板信息
|
||||
*/
|
||||
export function useGroupPanelInfo(
|
||||
groupId: string,
|
||||
panelId: string
|
||||
): GroupPanel | null {
|
||||
const panels = useGroupPanels(groupId);
|
||||
|
||||
return useMemo(
|
||||
() => panels.find((p) => p.id === panelId) ?? null,
|
||||
[groupId, panelId, panels]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为群组的所有者
|
||||
* @param userId 群组id 必填
|
||||
* @param userId 用户id 不传则为当前用户id
|
||||
*/
|
||||
export function useIsGroupOwner(groupId: string, userId?: string): boolean {
|
||||
const groupInfo = useGroupInfo(groupId);
|
||||
const selfUserId = useUserId();
|
||||
|
||||
if (isValidStr(userId)) {
|
||||
return groupInfo?.owner === userId;
|
||||
} else {
|
||||
return typeof selfUserId === 'string' && groupInfo?.owner === selfUserId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查群组聊天面板是否有未读消息
|
||||
* @param textPanelId 文字面板id
|
||||
*/
|
||||
export function useGroupTextPanelUnread(textPanelId: string): boolean {
|
||||
const unread = useUnread([textPanelId]);
|
||||
|
||||
return unread[0];
|
||||
}
|
||||
31
client/shared/redux/hooks/useGroupAck.ts
Normal file
31
client/shared/redux/hooks/useGroupAck.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useMemoizedFn } from '../../hooks/useMemoizedFn';
|
||||
import { updateAck } from '../../model/converse';
|
||||
import { isConversePanel } from '../../utils/panel-helper';
|
||||
import { chatActions } from '../slices';
|
||||
import { useAppDispatch, useAppSelector } from './useAppSelector';
|
||||
import { useGroupInfo } from './useGroup';
|
||||
|
||||
/**
|
||||
* 群组级别的已读管理
|
||||
*/
|
||||
export function useGroupAck(groupId: string) {
|
||||
const groupInfo = useGroupInfo(groupId);
|
||||
const lastMessageMap = useAppSelector((state) => state.chat.lastMessageMap);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const markGroupAllAck = useMemoizedFn(() => {
|
||||
const conversePanels = (groupInfo?.panels ?? []).filter(isConversePanel);
|
||||
|
||||
for (const converse of conversePanels) {
|
||||
const converseId = converse.id;
|
||||
const lastMessageId = lastMessageMap[converseId];
|
||||
|
||||
if (converseId && lastMessageId) {
|
||||
dispatch(chatActions.setConverseAck({ converseId, lastMessageId }));
|
||||
updateAck(converseId, lastMessageId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { markGroupAllAck };
|
||||
}
|
||||
24
client/shared/redux/hooks/useGroupMemberMute.ts
Normal file
24
client/shared/redux/hooks/useGroupMemberMute.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
|
||||
/**
|
||||
* 获取用户禁言状态
|
||||
* @param groupId 群组ID
|
||||
* @param userId 用户ID
|
||||
* @returns 如果没有禁言状态或者有禁言但是已过期则返回false,否则返回禁言到的时间
|
||||
*/
|
||||
export function useGroupMemberMute(
|
||||
groupId: string,
|
||||
userId: string
|
||||
): string | false {
|
||||
const muteUntil = useAppSelector(
|
||||
(state) =>
|
||||
state.group.groups[groupId]?.members.find((m) => m.userId === userId)
|
||||
?.muteUntil
|
||||
);
|
||||
|
||||
if (!muteUntil || new Date(muteUntil).valueOf() < new Date().valueOf()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return muteUntil;
|
||||
}
|
||||
137
client/shared/redux/hooks/useGroupPermission.ts
Normal file
137
client/shared/redux/hooks/useGroupPermission.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useGroupInfo } from './useGroup';
|
||||
import { useUserId } from './useUserInfo';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _flatten from 'lodash/flatten';
|
||||
import { useDebugValue, useMemo } from 'react';
|
||||
import { getPermissionList } from '../..';
|
||||
|
||||
/**
|
||||
* 获取群组用户的所有权限
|
||||
*/
|
||||
export function useGroupMemberAllPermissions(groupId: string): string[] {
|
||||
const groupInfo = useGroupInfo(groupId);
|
||||
const userId = useUserId();
|
||||
|
||||
if (!groupInfo || !userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (groupInfo.owner === userId) {
|
||||
// 群组管理员拥有一切权限
|
||||
// 返回所有权限
|
||||
return getPermissionList().map((p) => p.key);
|
||||
}
|
||||
|
||||
const members = groupInfo.members;
|
||||
|
||||
const groupRoles = groupInfo.roles;
|
||||
const userRoles = members.find((m) => m.userId === userId)?.roles ?? [];
|
||||
const userPermissions = _uniq([
|
||||
..._flatten(
|
||||
userRoles.map(
|
||||
(roleId) =>
|
||||
groupRoles.find((role) => String(role._id) === roleId)?.permissions ??
|
||||
[]
|
||||
)
|
||||
),
|
||||
...groupInfo.fallbackPermissions,
|
||||
]);
|
||||
|
||||
useDebugValue({
|
||||
groupRoles,
|
||||
userRoles,
|
||||
userPermissions,
|
||||
fallbackPermissions: groupInfo.fallbackPermissions,
|
||||
});
|
||||
|
||||
return userPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取面板的所有权限
|
||||
* 不包含群组本身的权限
|
||||
*/
|
||||
export function useGroupPanelMemberAllPermissions(
|
||||
groupId: string,
|
||||
panelId: string
|
||||
): string[] {
|
||||
const groupInfo = useGroupInfo(groupId);
|
||||
const userId = useUserId();
|
||||
|
||||
if (!groupInfo || !userId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const panelInfo = groupInfo.panels.find((p) => p.id === panelId);
|
||||
if (!panelInfo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fallbackPermissions = panelInfo.fallbackPermissions ?? [];
|
||||
const permissionMap = panelInfo.permissionMap ?? {};
|
||||
const specPermissions = permissionMap[userId] ?? [];
|
||||
|
||||
const userRoles =
|
||||
groupInfo.members.find((m) => m.userId === userId)?.roles ?? []; // 当前用户角色
|
||||
const userPanelPermissions = _uniq([
|
||||
..._flatten(userRoles.map((roleId) => permissionMap[roleId] ?? [])),
|
||||
...specPermissions,
|
||||
...fallbackPermissions,
|
||||
]);
|
||||
|
||||
return userPanelPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否拥有以下权限
|
||||
*/
|
||||
export function useHasGroupPermission(
|
||||
groupId: string,
|
||||
permissions: string[]
|
||||
): boolean[] {
|
||||
const userPermissions = useGroupMemberAllPermissions(groupId);
|
||||
|
||||
const result = useMemo(
|
||||
() => permissions.map((p) => userPermissions.includes(p)),
|
||||
[userPermissions.join(','), permissions.join(',')]
|
||||
);
|
||||
|
||||
useDebugValue({
|
||||
groupId,
|
||||
userPermissions,
|
||||
checkedPermissions: permissions,
|
||||
result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否在某个面板下拥有以下权限
|
||||
* 用于面板权限控制
|
||||
*/
|
||||
export function useHasGroupPanelPermission(
|
||||
groupId: string,
|
||||
panelId: string,
|
||||
permissions: string[]
|
||||
) {
|
||||
const groupPermissions = useGroupMemberAllPermissions(groupId);
|
||||
const panelPermissions = useGroupPanelMemberAllPermissions(groupId, panelId);
|
||||
|
||||
const fullPermissions = _uniq([...groupPermissions, ...panelPermissions]);
|
||||
|
||||
const result = useMemo(
|
||||
() => permissions.map((p) => fullPermissions.includes(p)),
|
||||
[fullPermissions.join(','), permissions.join(',')]
|
||||
);
|
||||
|
||||
useDebugValue({
|
||||
groupId,
|
||||
panelId,
|
||||
fullPermissions,
|
||||
checkedPermissions: permissions,
|
||||
result,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
18
client/shared/redux/hooks/useInbox.ts
Normal file
18
client/shared/redux/hooks/useInbox.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { InboxItem } from '../../model/inbox';
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
|
||||
/**
|
||||
* 返回收件箱列表
|
||||
*/
|
||||
export function useInboxList(): InboxItem[] {
|
||||
return useAppSelector((state) => state.chat.inbox ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回收件箱某一项的值
|
||||
*/
|
||||
export function useInboxItem(inboxItemId: string): InboxItem | null {
|
||||
const list = useInboxList();
|
||||
|
||||
return list.find((item) => item._id === inboxItemId) ?? null;
|
||||
}
|
||||
32
client/shared/redux/hooks/useUnread.ts
Normal file
32
client/shared/redux/hooks/useUnread.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
import { useAckInfoChecker } from './useAckInfo';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* 返回某些会话是否有未读信息
|
||||
*/
|
||||
export function useUnread(converseIds: string[]) {
|
||||
const ack = useAppSelector((state) => state.chat.ack);
|
||||
const lastMessageMap = useAppSelector((state) => state.chat.lastMessageMap);
|
||||
const { ensureAckInfo } = useAckInfoChecker();
|
||||
|
||||
useEffect(() => {
|
||||
converseIds.forEach((converseId) => ensureAckInfo(converseId));
|
||||
}, [converseIds]);
|
||||
|
||||
const unreadList = converseIds.map((converseId) => {
|
||||
if (
|
||||
ack[converseId] === undefined &&
|
||||
lastMessageMap[converseId] !== undefined
|
||||
) {
|
||||
// 远程没有已读记录且获取到了最后一条消息
|
||||
return true;
|
||||
}
|
||||
|
||||
// 当远端最后一条消息的id > 本地已读状态的最后一条消息id,
|
||||
// 则返回true(有未读消息)
|
||||
return lastMessageMap[converseId] > ack[converseId];
|
||||
});
|
||||
|
||||
return unreadList;
|
||||
}
|
||||
16
client/shared/redux/hooks/useUserInfo.ts
Normal file
16
client/shared/redux/hooks/useUserInfo.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { UserLoginInfo } from '../../model/user';
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
|
||||
/**
|
||||
* 获取当前用户基本信息
|
||||
*/
|
||||
export function useUserInfo(): UserLoginInfo | null {
|
||||
return useAppSelector((state) => state.user.info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户基本Id
|
||||
*/
|
||||
export function useUserId(): string | undefined {
|
||||
return useUserInfo()?._id;
|
||||
}
|
||||
Reference in New Issue
Block a user