From 76c898ba92ea5caa108b23578428f763fb47c177 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 27 Aug 2022 20:16:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E5=A4=9A=E7=9A=84=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=82=B9=E4=B8=8E=E5=89=8D=E7=AB=AF=E6=9D=83=E9=99=90?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/shared/index.tsx | 11 ++ client/shared/package.json | 1 + .../shared/redux/hooks/useGroupPermission.ts | 72 +++++++++++++ client/shared/utils/role-helper.ts | 100 ++++++++++++++++++ .../components/ChatBox/ChatInputBox/index.tsx | 3 + .../src/components/Panel/group/TextPanel.tsx | 12 +++ .../web/src/components/modals/CreateGroup.tsx | 2 +- .../CreateGroupInvite/CreateInviteCode.tsx | 9 ++ .../GroupDetail/Role/PermissionItem.tsx | 7 +- .../modals/GroupDetail/Role/index.tsx | 2 +- .../GroupDetail/Role/tabs/permission.tsx | 7 +- .../modals/GroupDetail/Role/useRoleActions.ts | 2 +- .../components/modals/GroupDetail/index.tsx | 19 ++-- .../routes/Main/Content/Group/GroupHeader.tsx | 17 ++- .../Content/Group/useGroupHeaderAction.tsx | 2 +- client/web/src/utils/role-helper.ts | 31 ------ pnpm-lock.yaml | 8 +- 17 files changed, 255 insertions(+), 50 deletions(-) create mode 100644 client/shared/redux/hooks/useGroupPermission.ts create mode 100644 client/shared/utils/role-helper.ts delete mode 100644 client/web/src/utils/role-helper.ts diff --git a/client/shared/index.tsx b/client/shared/index.tsx index 6f811262..8a913737 100644 --- a/client/shared/index.tsx +++ b/client/shared/index.tsx @@ -184,6 +184,10 @@ export { useGroupTextPanelUnread, } from './redux/hooks/useGroup'; export { useGroupMemberMute } from './redux/hooks/useGroupMemberMute'; +export { + useGroupMemberAllPermissions, + useHasGroupPermission, +} from './redux/hooks/useGroupPermission'; export { useUserInfo, useUserId } from './redux/hooks/useUserInfo'; export { useUnread } from './redux/hooks/useUnread'; export { @@ -218,6 +222,13 @@ export { export { isValidStr } from './utils/string-helper'; export { isValidJson } from './utils/json-helper'; export { MessageHelper } from './utils/message-helper'; +export { + PERMISSION, + AllPermission, + permissionList, + getDefaultPermissionList, + applyDefaultFallbackGroupPermission, +} from './utils/role-helper'; export { uploadFile } from './utils/upload-helper'; export type { UploadFileResult } from './utils/upload-helper'; export { parseUrlStr } from './utils/url-helper'; diff --git a/client/shared/package.json b/client/shared/package.json index 41ec6238..b9557303 100644 --- a/client/shared/package.json +++ b/client/shared/package.json @@ -30,6 +30,7 @@ "devDependencies": { "@types/crc": "^3.4.0", "@types/lodash": "^4.14.170", + "@types/react": "^17.0.39", "@types/react-redux": "^7.1.24", "react": "17.0.2" }, diff --git a/client/shared/redux/hooks/useGroupPermission.ts b/client/shared/redux/hooks/useGroupPermission.ts new file mode 100644 index 00000000..a050e2fa --- /dev/null +++ b/client/shared/redux/hooks/useGroupPermission.ts @@ -0,0 +1,72 @@ +import { useGroupInfo } from './useGroup'; +import { useUserId } from './useUserInfo'; +import _uniq from 'lodash/uniq'; +import _flatten from 'lodash/flatten'; +import { useDebugValue, useMemo } from 'react'; +import { permissionList } from '../..'; + +/** + * 获取群组用户的所有权限 + */ +export function useGroupMemberAllPermissions(groupId: string): string[] { + const groupInfo = useGroupInfo(groupId); + const userId = useUserId(); + + if (!groupInfo || !userId) { + return []; + } + + if (groupInfo.owner === userId) { + // 群组管理员拥有一切权限 + // 返回所有权限 + return permissionList.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 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; +} diff --git a/client/shared/utils/role-helper.ts b/client/shared/utils/role-helper.ts new file mode 100644 index 00000000..06fb0935 --- /dev/null +++ b/client/shared/utils/role-helper.ts @@ -0,0 +1,100 @@ +import { model, t } from '..'; + +/** + * 所有人权限 + * 群组最低权限标识 + */ +export const AllPermission = Symbol('AllPermission'); + +interface PermissionItem { + key: string; + title: string; + desc: string; + default: boolean; + required?: string[]; +} + +export const PERMISSION = { + /** + * 非插件的权限点都叫core + */ + core: { + message: 'core.message', + invite: 'core.invite', + unlimitedInvite: 'core.unlimitedInvite', + groupDetail: 'core.groupDetail', + managePanel: 'core.managePanel', + manageInvite: 'core.manageInvite', + manageRoles: 'core.manageRoles', + }, +}; + +/** + * TODO: 后端校验还没做 + */ +export const permissionList: PermissionItem[] = [ + { + key: PERMISSION.core.message, + title: t('发送消息'), + desc: t('允许成员在文字频道发送消息'), + default: true, + }, + { + key: PERMISSION.core.invite, + title: t('邀请链接'), + desc: t('允许成员创建邀请链接'), + default: true, + }, + { + key: PERMISSION.core.unlimitedInvite, + title: t('不限时邀请链接'), + desc: t('允许成员创建不限时邀请链接'), + default: false, + required: [PERMISSION.core.invite], + }, + { + key: PERMISSION.core.groupDetail, + title: t('查看群组详情'), + desc: t('允许成员查看群组详情'), + default: false, + }, + { + key: PERMISSION.core.managePanel, + title: t('允许管理频道'), + desc: t('允许成员查看管理频道'), + default: false, + required: [PERMISSION.core.groupDetail], + }, + { + key: PERMISSION.core.manageInvite, + title: t('允许管理邀请链接'), + desc: t('允许成员管理邀请链接'), + default: false, + required: [PERMISSION.core.groupDetail], + }, + { + key: PERMISSION.core.manageRoles, + title: t('允许管理身份组'), + desc: t('允许成员管理身份组'), + default: false, + required: [PERMISSION.core.groupDetail], + }, +]; + +/** + * 获取默认权限列表 + */ +export function getDefaultPermissionList(): string[] { + return permissionList.filter((p) => p.default).map((p) => p.key); +} + +/** + * 初始化默认所有人身份组权限 + */ +export async function applyDefaultFallbackGroupPermission(groupId: string) { + await model.group.modifyGroupField( + groupId, + 'fallbackPermissions', + getDefaultPermissionList() + ); +} diff --git a/client/web/src/components/ChatBox/ChatInputBox/index.tsx b/client/web/src/components/ChatBox/ChatInputBox/index.tsx index 06ab4c43..1db3f280 100644 --- a/client/web/src/components/ChatBox/ChatInputBox/index.tsx +++ b/client/web/src/components/ChatBox/ChatInputBox/index.tsx @@ -18,6 +18,9 @@ import _uniq from 'lodash/uniq'; interface ChatInputBoxProps { onSendMsg: (msg: string, meta?: SendMessagePayloadMeta) => void; } +/** + * 通用聊天输入框 + */ export const ChatInputBox: React.FC = React.memo((props) => { const inputRef = useRef(null); const [message, setMessage] = useState(''); diff --git a/client/web/src/components/Panel/group/TextPanel.tsx b/client/web/src/components/Panel/group/TextPanel.tsx index 6ee637d7..8ecda401 100644 --- a/client/web/src/components/Panel/group/TextPanel.tsx +++ b/client/web/src/components/Panel/group/TextPanel.tsx @@ -9,6 +9,8 @@ import { t, humanizeMsDuration, useInterval, + useHasGroupPermission, + PERMISSION, } from 'tailchat-shared'; import { GroupPanelWrapper } from './Wrapper'; @@ -18,6 +20,9 @@ import { GroupPanelWrapper } from './Wrapper'; function useChatInputInfo(groupId: string) { const userId = useUserId(); const muteUntil = useGroupMemberMute(groupId, userId ?? ''); + const [hasPermission] = useHasGroupPermission(groupId, [ + PERMISSION.core.message, + ]); const [placeholder, setPlaceholder] = useState(undefined); const updatePlaceholder = useCallback(() => { @@ -44,6 +49,13 @@ function useChatInputInfo(groupId: string) { updatePlaceholder(); }, [muteUntil]); + if (!hasPermission) { + return { + disabled: true, + placeholder: t('没有发送消息的权限, 请联系群组所有者'), + }; + } + return { disabled: Boolean(muteUntil), placeholder, diff --git a/client/web/src/components/modals/CreateGroup.tsx b/client/web/src/components/modals/CreateGroup.tsx index 81c379fe..7cb9114a 100644 --- a/client/web/src/components/modals/CreateGroup.tsx +++ b/client/web/src/components/modals/CreateGroup.tsx @@ -13,7 +13,7 @@ import { Avatar } from '../Avatar'; import { closeModal, ModalWrapper } from '../Modal'; import { Slides, SlidesRef } from '../Slides'; import { useHistory } from 'react-router'; -import { applyDefaultFallbackGroupPermission } from '@/utils/role-helper'; +import { applyDefaultFallbackGroupPermission } from 'tailchat-shared'; const panelTemplate: { key: string; diff --git a/client/web/src/components/modals/CreateGroupInvite/CreateInviteCode.tsx b/client/web/src/components/modals/CreateGroupInvite/CreateInviteCode.tsx index c83bc68c..cb2fcefa 100644 --- a/client/web/src/components/modals/CreateGroupInvite/CreateInviteCode.tsx +++ b/client/web/src/components/modals/CreateGroupInvite/CreateInviteCode.tsx @@ -7,6 +7,8 @@ import { createGroupInviteCode, t, GroupInvite, + PERMISSION, + useHasGroupPermission, } from 'tailchat-shared'; import styles from './CreateInviteCode.module.less'; @@ -30,10 +32,16 @@ export const CreateInviteCode: React.FC = React.memo( }, [groupId, onInviteCreated] ); + const [hasInvitePermission, hasUnlimitedInvitePermission] = + useHasGroupPermission(groupId, [ + PERMISSION.core.invite, + PERMISSION.core.unlimitedInvite, + ]); const menu = ( handleCreateInviteLink(InviteCodeType.Permanent)} > {t('创建永久邀请码')} @@ -61,6 +69,7 @@ export const CreateInviteCode: React.FC = React.memo( className={styles.createInviteBtn} size="large" type="primary" + disabled={!hasInvitePermission} loading={loading} onClick={() => handleCreateInviteLink(InviteCodeType.Normal)} overlay={menu} diff --git a/client/web/src/components/modals/GroupDetail/Role/PermissionItem.tsx b/client/web/src/components/modals/GroupDetail/Role/PermissionItem.tsx index 25973767..32a9e796 100644 --- a/client/web/src/components/modals/GroupDetail/Role/PermissionItem.tsx +++ b/client/web/src/components/modals/GroupDetail/Role/PermissionItem.tsx @@ -4,6 +4,7 @@ import React from 'react'; interface PermissionItemProps { title: string; desc?: string; + disabled?: boolean; checked: boolean; onChange: (checked: boolean) => void; } @@ -18,7 +19,11 @@ export const PermissionItem: React.FC = React.memo( - + diff --git a/client/web/src/components/modals/GroupDetail/Role/index.tsx b/client/web/src/components/modals/GroupDetail/Role/index.tsx index 8f8c0f78..e6e74f9a 100644 --- a/client/web/src/components/modals/GroupDetail/Role/index.tsx +++ b/client/web/src/components/modals/GroupDetail/Role/index.tsx @@ -1,6 +1,6 @@ import { Loading } from '@/components/Loading'; import { PillTabPane, PillTabs } from '@/components/PillTabs'; -import { AllPermission } from '@/utils/role-helper'; +import { AllPermission } from 'tailchat-shared'; import React, { useMemo, useState } from 'react'; import { t, useGroupInfo } from 'tailchat-shared'; import { RoleItem } from './RoleItem'; diff --git a/client/web/src/components/modals/GroupDetail/Role/tabs/permission.tsx b/client/web/src/components/modals/GroupDetail/Role/tabs/permission.tsx index feb91235..2a6ac5e3 100644 --- a/client/web/src/components/modals/GroupDetail/Role/tabs/permission.tsx +++ b/client/web/src/components/modals/GroupDetail/Role/tabs/permission.tsx @@ -1,4 +1,4 @@ -import { AllPermission, permissionList } from '@/utils/role-helper'; +import { AllPermission, permissionList } from 'tailchat-shared'; import { Button } from 'antd'; import React, { useCallback, useMemo } from 'react'; import { model, t } from 'tailchat-shared'; @@ -54,6 +54,11 @@ export const RolePermission: React.FC = React.memo( key={p.key} title={p.title} desc={p.desc} + disabled={ + p.required + ? !p.required.every((r) => editingPermission.includes(r)) + : undefined + } checked={editingPermission.includes(p.key)} onChange={(checked) => handleSwitchPermission(p.key, checked)} /> diff --git a/client/web/src/components/modals/GroupDetail/Role/useRoleActions.ts b/client/web/src/components/modals/GroupDetail/Role/useRoleActions.ts index 0a572323..11e6f688 100644 --- a/client/web/src/components/modals/GroupDetail/Role/useRoleActions.ts +++ b/client/web/src/components/modals/GroupDetail/Role/useRoleActions.ts @@ -1,4 +1,4 @@ -import { AllPermission, getDefaultPermissionList } from '@/utils/role-helper'; +import { AllPermission, getDefaultPermissionList } from 'tailchat-shared'; import { model, t, useAsyncRequest } from 'tailchat-shared'; export function useRoleActions( diff --git a/client/web/src/components/modals/GroupDetail/index.tsx b/client/web/src/components/modals/GroupDetail/index.tsx index fd149fe8..77677387 100644 --- a/client/web/src/components/modals/GroupDetail/index.tsx +++ b/client/web/src/components/modals/GroupDetail/index.tsx @@ -7,11 +7,12 @@ import { import { GroupIdContextProvider } from '@/context/GroupIdContext'; import { pluginCustomPanel } from '@/plugin/common'; import React, { useCallback, useMemo } from 'react'; -import { t } from 'tailchat-shared'; +import { PERMISSION, t, useHasGroupPermission } from 'tailchat-shared'; import { GroupInvite } from './Invite'; import { GroupPanel } from './Panel'; import { GroupRole } from './Role'; import { GroupSummary } from './Summary'; +import _compact from 'lodash/compact'; interface SettingsViewProps { groupId: string; @@ -27,6 +28,12 @@ export const GroupDetail: React.FC = React.memo((props) => { }, [props.onClose] ); + const [allowManagePanel, allowManageInvite, allowManageRoles] = + useHasGroupPermission(groupId, [ + PERMISSION.core.managePanel, + PERMISSION.core.manageInvite, + PERMISSION.core.manageRoles, + ]); const menu: SidebarViewMenuType[] = useMemo(() => { // 内置 @@ -34,29 +41,29 @@ export const GroupDetail: React.FC = React.memo((props) => { { type: 'group', title: t('通用'), - children: [ + children: _compact([ { type: 'item', title: t('概述'), content: , }, - { + allowManagePanel && { type: 'item', title: t('面板'), content: , }, - { + allowManageInvite && { type: 'item', title: t('邀请码'), content: , }, - { + allowManageRoles && { type: 'item', title: t('身份组'), isDev: true, content: , }, - ], + ]), }, ]; diff --git a/client/web/src/routes/Main/Content/Group/GroupHeader.tsx b/client/web/src/routes/Main/Content/Group/GroupHeader.tsx index 64ef5598..016c1d41 100644 --- a/client/web/src/routes/Main/Content/Group/GroupHeader.tsx +++ b/client/web/src/routes/Main/Content/Group/GroupHeader.tsx @@ -1,7 +1,12 @@ import React from 'react'; import { Menu } from 'antd'; import _isNil from 'lodash/isNil'; -import { useGroupInfo, useTranslation } from 'tailchat-shared'; +import { + PERMISSION, + useGroupInfo, + useHasGroupPermission, + useTranslation, +} from 'tailchat-shared'; import { SectionHeader } from '@/components/SectionHeader'; import { useGroupHeaderAction } from './useGroupHeaderAction'; @@ -12,8 +17,12 @@ export const GroupHeader: React.FC = React.memo((props) => { const { groupId } = props; const groupInfo = useGroupInfo(groupId); const { t } = useTranslation(); + const [showGroupDetail, showInvite] = useHasGroupPermission(groupId, [ + PERMISSION.core.groupDetail, + PERMISSION.core.invite, + ]); - const { isOwner, handleShowGroupDetail, handleInviteUser, handleQuitGroup } = + const { handleShowGroupDetail, handleInviteUser, handleQuitGroup } = useGroupHeaderAction(groupId); if (_isNil(groupInfo)) { @@ -22,13 +31,13 @@ export const GroupHeader: React.FC = React.memo((props) => { const menu = ( - {isOwner && ( + {showGroupDetail && ( {t('查看详情')} )} - {isOwner && ( + {showInvite && ( {t('邀请用户')} diff --git a/client/web/src/routes/Main/Content/Group/useGroupHeaderAction.tsx b/client/web/src/routes/Main/Content/Group/useGroupHeaderAction.tsx index cfc01fa3..6b2273d4 100644 --- a/client/web/src/routes/Main/Content/Group/useGroupHeaderAction.tsx +++ b/client/web/src/routes/Main/Content/Group/useGroupHeaderAction.tsx @@ -47,5 +47,5 @@ export function useGroupHeaderAction(groupId: string) { } }); - return { isOwner, handleShowGroupDetail, handleInviteUser, handleQuitGroup }; + return { handleShowGroupDetail, handleInviteUser, handleQuitGroup }; } diff --git a/client/web/src/utils/role-helper.ts b/client/web/src/utils/role-helper.ts deleted file mode 100644 index 5aca4e2b..00000000 --- a/client/web/src/utils/role-helper.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { model, t } from 'tailchat-shared'; - -/** - * 所有人权限 - * 群组最低权限标识 - */ -export const AllPermission = Symbol('AllPermission'); - -export const permissionList = [ - { - key: 'core.message', - title: t('发送消息'), - desc: t('允许成员在文字频道发送消息'), - default: true, - }, -]; - -export function getDefaultPermissionList(): string[] { - return permissionList.filter((p) => p.default).map((p) => p.key); -} - -/** - * 初始化默认所有人身份组权限 - */ -export async function applyDefaultFallbackGroupPermission(groupId: string) { - await model.group.modifyGroupField( - groupId, - 'fallbackPermissions', - getDefaultPermissionList() - ); -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c13416b..b3da61a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,6 +123,7 @@ importers: '@reduxjs/toolkit': ^1.7.1 '@types/crc': ^3.4.0 '@types/lodash': ^4.14.170 + '@types/react': ^17.0.39 '@types/react-redux': ^7.1.24 axios: ^0.21.1 crc: ^3.8.0 @@ -166,6 +167,7 @@ importers: devDependencies: '@types/crc': 3.8.0 '@types/lodash': 4.14.184 + '@types/react': 17.0.48 '@types/react-redux': 7.1.24 react: 17.0.2 @@ -2983,7 +2985,7 @@ packages: react-dom: '*' dependencies: '@docusaurus/types': 2.0.0-beta.18 - '@types/react': 17.0.48 + '@types/react': 18.0.17 '@types/react-router-config': 5.0.6 '@types/react-router-dom': 5.3.3 react: 17.0.2 @@ -3295,7 +3297,7 @@ packages: peerDependencies: react: '*' dependencies: - '@types/react': 17.0.48 + '@types/react': 18.0.17 prop-types: 15.8.1 react: 17.0.2 @@ -15191,7 +15193,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_t4lrjbt3sxauai4t5o275zsepa + ts-node: 10.9.1_bqee57coj3oib6dw4m24wknwqe transitivePeerDependencies: - bufferutil - canvas