From 27e99697969bb56846e39092bb143d00370009bc Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Mon, 3 Oct 2022 17:33:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E7=BE=A4=E7=BB=84?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5=E5=85=8D=E6=89=93=E6=89=B0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/shared/event/index.ts | 5 ++ client/shared/hooks/useWatch.ts | 12 +++ client/shared/index.tsx | 1 + client/web/package.json | 1 + .../web/plugins/com.msgbyte.intro/src/tour.ts | 2 +- .../plugins/com.msgbyte.notify/package.json | 2 +- .../plugins/com.msgbyte.notify/src/index.ts | 7 -- .../plugins/com.msgbyte.notify/src/index.tsx | 39 +++++++++ .../plugins/com.msgbyte.notify/src/notify.ts | 8 ++ .../plugins/com.msgbyte.notify/src/silent.ts | 39 +++++++++ client/web/src/components/GroupPanelItem.tsx | 17 ++-- client/web/src/plugin/common/reg.ts | 30 +++++-- .../routes/Main/Content/Group/SidebarItem.tsx | 53 +++--------- .../Main/Content/Group/TextPanelItem.tsx | 3 + .../src/routes/Main/Content/Group/utils.ts | 16 ---- .../src/routes/Main/Content/Group/utils.tsx | 84 +++++++++++++++++++ client/web/src/styles/antd/dark.less | 3 + pnpm-lock.yaml | 2 + 18 files changed, 245 insertions(+), 79 deletions(-) create mode 100644 client/shared/hooks/useWatch.ts delete mode 100644 client/web/plugins/com.msgbyte.notify/src/index.ts create mode 100644 client/web/plugins/com.msgbyte.notify/src/index.tsx create mode 100644 client/web/plugins/com.msgbyte.notify/src/silent.ts delete mode 100644 client/web/src/routes/Main/Content/Group/utils.ts create mode 100644 client/web/src/routes/Main/Content/Group/utils.tsx diff --git a/client/shared/event/index.ts b/client/shared/event/index.ts index 4e4c0d4c..e5ff0b4b 100644 --- a/client/shared/event/index.ts +++ b/client/shared/event/index.ts @@ -35,6 +35,11 @@ export interface SharedEventMap { * 消息已读(消息出现在界面上) */ readMessage: (payload: ChatMessage | null) => void; + + /** + * 群组面板状态更新 + */ + groupPanelBadgeUpdate: () => void; } export type SharedEventType = keyof SharedEventMap; diff --git a/client/shared/hooks/useWatch.ts b/client/shared/hooks/useWatch.ts new file mode 100644 index 00000000..10346f79 --- /dev/null +++ b/client/shared/hooks/useWatch.ts @@ -0,0 +1,12 @@ +import { DependencyList, useLayoutEffect } from 'react'; +import { useMemoizedFn } from './useMemoizedFn'; + +/** + * 监听变更并触发回调 + */ +export function useWatch(deps: DependencyList, cb: () => void) { + const memoizedFn = useMemoizedFn(cb); + useLayoutEffect(() => { + memoizedFn(); + }, deps); +} diff --git a/client/shared/index.tsx b/client/shared/index.tsx index acae8170..192e05fe 100644 --- a/client/shared/index.tsx +++ b/client/shared/index.tsx @@ -69,6 +69,7 @@ export { useRafState } from './hooks/useRafState'; export { useSearch } from './hooks/useSearch'; export { useShallowObject } from './hooks/useShallowObject'; export { useUpdateRef } from './hooks/useUpdateRef'; +export { useWatch } from './hooks/useWatch'; export { useWhyDidYouUpdate } from './hooks/useWhyDidYouUpdate'; // manager diff --git a/client/web/package.json b/client/web/package.json index bbf93cd3..0d043d88 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@loadable/component": "^5.15.2", + "ahooks": "^3.7.1", "antd": "^4.19.5", "axios": "^0.21.1", "clsx": "^1.1.1", diff --git a/client/web/plugins/com.msgbyte.intro/src/tour.ts b/client/web/plugins/com.msgbyte.intro/src/tour.ts index 10c0c4e0..794d2312 100644 --- a/client/web/plugins/com.msgbyte.intro/src/tour.ts +++ b/client/web/plugins/com.msgbyte.intro/src/tour.ts @@ -2,7 +2,7 @@ import Shepherd from 'shepherd.js'; import { steps } from './steps'; import './style.less'; -const KEY = 'com.msgbyte.intro/hasRun'; +const KEY = 'plugin:com.msgbyte.intro/hasRun'; if (!window.localStorage.getItem(KEY)) { const tour = new Shepherd.Tour({ diff --git a/client/web/plugins/com.msgbyte.notify/package.json b/client/web/plugins/com.msgbyte.notify/package.json index c09958fd..19abc0cb 100644 --- a/client/web/plugins/com.msgbyte.notify/package.json +++ b/client/web/plugins/com.msgbyte.notify/package.json @@ -1,6 +1,6 @@ { "name": "@plugins/com.msgbyte.notify", - "main": "src/index.ts", + "main": "src/index.tsx", "version": "0.0.0", "private": true, "dependencies": {} diff --git a/client/web/plugins/com.msgbyte.notify/src/index.ts b/client/web/plugins/com.msgbyte.notify/src/index.ts deleted file mode 100644 index 6824ed1e..00000000 --- a/client/web/plugins/com.msgbyte.notify/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { initNotify } from './notify'; - -if ('Notification' in window) { - initNotify(); -} else { - console.warn('浏览器不支持 Notification'); -} diff --git a/client/web/plugins/com.msgbyte.notify/src/index.tsx b/client/web/plugins/com.msgbyte.notify/src/index.tsx new file mode 100644 index 00000000..84821359 --- /dev/null +++ b/client/web/plugins/com.msgbyte.notify/src/index.tsx @@ -0,0 +1,39 @@ +import { + regGroupPanelBadge, + regPluginGroupTextPanelExtraMenu, + sharedEvent, +} from '@capital/common'; +import { Icon } from '@capital/component'; +import React from 'react'; +import { appendSilent, hasSilent, removeSilent } from './silent'; +import { initNotify } from './notify'; + +const PLUGIN_NAME = 'com.msgbyte.notify'; + +if ('Notification' in window) { + initNotify(); +} else { + console.warn('浏览器不支持 Notification'); +} + +regPluginGroupTextPanelExtraMenu({ + name: `${PLUGIN_NAME}/grouppanelmenu`, + label: '免打扰', + icon: 'mdi:bell-off-outline', + onClick: (panelInfo) => { + if (hasSilent(panelInfo.id)) { + removeSilent(panelInfo.id); + } else { + appendSilent(panelInfo.id); + } + + sharedEvent.emit('groupPanelBadgeUpdate'); + }, +}); + +regGroupPanelBadge({ + name: `${PLUGIN_NAME}/grouppanelbadge`, + render: (groupId: string, panelId: string) => { + return hasSilent(panelId) ? : null; + }, +}); diff --git a/client/web/plugins/com.msgbyte.notify/src/notify.ts b/client/web/plugins/com.msgbyte.notify/src/notify.ts index 179ada56..048d1392 100644 --- a/client/web/plugins/com.msgbyte.notify/src/notify.ts +++ b/client/web/plugins/com.msgbyte.notify/src/notify.ts @@ -4,6 +4,7 @@ import { getCachedUserInfo, getServiceWorkerRegistration, } from '@capital/common'; +import { hasSilent } from './silent'; export function initNotify() { if (Notification.permission === 'default') { @@ -13,7 +14,14 @@ export function initNotify() { regSocketEventListener({ eventName: 'chat.message.add', eventFn: (message) => { + const converseId = message.converseId; const currentUserId = getGlobalState()?.user.info._id; + + if (hasSilent(converseId)) { + // 免打扰 + return; + } + if (currentUserId !== message.author) { // 创建通知 diff --git a/client/web/plugins/com.msgbyte.notify/src/silent.ts b/client/web/plugins/com.msgbyte.notify/src/silent.ts new file mode 100644 index 00000000..bf29faee --- /dev/null +++ b/client/web/plugins/com.msgbyte.notify/src/silent.ts @@ -0,0 +1,39 @@ +/** + * 免打扰 + */ + +const KEY = 'plugin:com.msgbyte.notify/slientStorage'; + +const silentSet = new Set(loadFromLocalstorage()); + +export function appendSilent(converseId: string) { + silentSet.add(converseId); + saveToLocalstorage(); +} + +export function removeSilent(converseId: string) { + silentSet.delete(converseId); + saveToLocalstorage(); +} + +export function hasSilent(converseId: string): boolean { + return silentSet.has(converseId); +} + +function saveToLocalstorage() { + localStorage.setItem(KEY, JSON.stringify(Array.from(silentSet))); +} + +function loadFromLocalstorage(): string[] { + try { + const arr = JSON.parse(localStorage.getItem(KEY)); + if (Array.isArray(arr)) { + return arr; + } else { + return []; + } + } catch (err) { + console.error(err); + return []; + } +} diff --git a/client/web/src/components/GroupPanelItem.tsx b/client/web/src/components/GroupPanelItem.tsx index 990643cb..58074a46 100644 --- a/client/web/src/components/GroupPanelItem.tsx +++ b/client/web/src/components/GroupPanelItem.tsx @@ -1,4 +1,4 @@ -import { Badge, Typography } from 'antd'; +import { Badge, Space, Typography } from 'antd'; import clsx from 'clsx'; import React from 'react'; import { useLocation } from 'react-router'; @@ -13,6 +13,7 @@ export const GroupPanelItem: React.FC<{ icon: React.ReactNode; to: string; badge?: boolean; + extraBadge?: React.ReactNode[]; }> = React.memo((props) => { const { icon, name, to, badge } = props; const location = useLocation(); @@ -37,11 +38,15 @@ export const GroupPanelItem: React.FC<{ {name} - {badge === true ? ( - - ) : ( - - )} + + {badge === true ? ( + + ) : ( + + )} + + {props.extraBadge} + ); diff --git a/client/web/src/plugin/common/reg.ts b/client/web/src/plugin/common/reg.ts index d6240148..98b54478 100644 --- a/client/web/src/plugin/common/reg.ts +++ b/client/web/src/plugin/common/reg.ts @@ -46,6 +46,13 @@ export interface PluginCustomPanel { export const [pluginCustomPanel, regCustomPanel] = buildRegList(); +export interface PluginPanelMenu { + name: string; + label: string; + icon?: string; + onClick: (panelInfo: GroupPanel) => void; +} + /** * 注册群组面板 */ @@ -79,12 +86,7 @@ export interface PluginGroupPanel { /** * 面板项右键菜单 */ - menus?: { - name: string; - label: string; - icon?: string; - onClick: (panelInfo: GroupPanel) => void; - }[]; + menus?: PluginPanelMenu[]; } export const [pluginGroupPanel, regGroupPanel] = buildRegList(); @@ -212,3 +214,19 @@ export const [pluginPanelActions, regPluginPanelAction] = buildRegList< */ export const [pluginPermission, regPluginPermission] = buildRegList(); + +/** + * 注册自定义群组面板badge + */ +export const [pluginGroupPanelBadges, regGroupPanelBadge] = buildRegList<{ + name: string; + render: (groupId: string, panelId: string) => React.ReactNode; +}>(); + +/** + * 注册自定义群组文本面板项额外操作菜单 + */ +export const [ + pluginGroupTextPanelExtraMenus, + regPluginGroupTextPanelExtraMenu, +] = buildRegList(); diff --git a/client/web/src/routes/Main/Content/Group/SidebarItem.tsx b/client/web/src/routes/Main/Content/Group/SidebarItem.tsx index c996a6d4..b12ad136 100644 --- a/client/web/src/routes/Main/Content/Group/SidebarItem.tsx +++ b/client/web/src/routes/Main/Content/Group/SidebarItem.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { groupActions, GroupPanel, @@ -18,41 +18,7 @@ import { usePanelWindow } from '@/hooks/usePanelWindow'; import { LoadingSpinner } from '@/components/LoadingSpinner'; import _compact from 'lodash/compact'; import { Icon } from 'tailchat-design'; -import { findPluginPanelInfoByName } from '@/utils/plugin-helper'; -import type { ItemType } from 'antd/lib/menu/hooks/useItems'; - -/** - * 获取来自插件的菜单项 - */ -function useExtraMenuItems(panel: GroupPanel): ItemType[] { - const pluginPanelInfo = useMemo( - () => - panel.pluginPanelName && findPluginPanelInfoByName(panel.pluginPanelName), - [panel.pluginPanelName] - ); - if (!pluginPanelInfo) { - return []; - } - - if ( - Array.isArray(pluginPanelInfo.menus) && - pluginPanelInfo.menus.length > 0 - ) { - return [ - { - type: 'divider', - }, - ...pluginPanelInfo.menus.map((item) => ({ - key: item.name, - label: item.label, - icon: item.icon ? : undefined, - onClick: () => item.onClick(panel), - })), - ]; - } - - return []; -} +import { useExtraMenuItems, useGroupPanelExtraBadge } from './utils'; /** * 群组面板侧边栏组件 @@ -62,20 +28,22 @@ export const SidebarItem: React.FC<{ panel: GroupPanel; }> = React.memo((props) => { const { groupId, panel } = props; + const panelId = panel.id; const { hasOpenedPanel, openPanelWindow } = usePanelWindow( - `/panel/group/${groupId}/${panel.id}` + `/panel/group/${groupId}/${panelId}` ); const groupInfo = useGroupInfo(groupId); const dispatch = useAppDispatch(); - const { markConverseAllAck } = useConverseAck(panel.id); + const { markConverseAllAck } = useConverseAck(panelId); const extraMenuItems = useExtraMenuItems(panel); + const extraBadge = useGroupPanelExtraBadge(groupId, panelId); if (!groupInfo) { return ; } const isPinned = - isValidStr(groupInfo.pinnedPanelId) && groupInfo.pinnedPanelId === panel.id; + isValidStr(groupInfo.pinnedPanelId) && groupInfo.pinnedPanelId === panelId; const menu = ( , onClick: () => { - copy(`${location.origin}/main/group/${groupId}/${panel.id}`); + copy(`${location.origin}/main/group/${groupId}/${panelId}`); showToasts(t('已复制到剪切板')); }, }, @@ -117,7 +85,7 @@ export const SidebarItem: React.FC<{ dispatch( groupActions.pinGroupPanel({ groupId, - panelId: panel.id, + panelId: panelId, }) ); }, @@ -144,7 +112,8 @@ export const SidebarItem: React.FC<{ )} diff --git a/client/web/src/routes/Main/Content/Group/TextPanelItem.tsx b/client/web/src/routes/Main/Content/Group/TextPanelItem.tsx index ba62efb4..5c82fd6b 100644 --- a/client/web/src/routes/Main/Content/Group/TextPanelItem.tsx +++ b/client/web/src/routes/Main/Content/Group/TextPanelItem.tsx @@ -1,6 +1,7 @@ import { GroupPanelItem } from '@/components/GroupPanelItem'; import React from 'react'; import { GroupPanel, useGroupTextPanelUnread } from 'tailchat-shared'; +import { useGroupPanelExtraBadge } from './utils'; interface GroupTextPanelItemProps { groupId: string; @@ -16,6 +17,7 @@ export const GroupTextPanelItem: React.FC = React.memo( const { groupId, panel } = props; const panelId = panel.id; const hasUnread = useGroupTextPanelUnread(panelId); + const extraBadge = useGroupPanelExtraBadge(groupId, panelId); return ( = React.memo( icon={props.icon} to={`/main/group/${groupId}/${panel.id}`} badge={hasUnread} + extraBadge={extraBadge} /> ); } diff --git a/client/web/src/routes/Main/Content/Group/utils.ts b/client/web/src/routes/Main/Content/Group/utils.ts deleted file mode 100644 index 7ba58f66..00000000 --- a/client/web/src/routes/Main/Content/Group/utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useParams } from 'react-router'; - -/** - * 获取群组面板的参数 - */ -export function useGroupPanelParams(): { - groupId: string; - panelId: string; -} { - const { groupId = '', panelId = '' } = useParams<{ - groupId: string; - panelId: string; - }>(); - - return { groupId, panelId }; -} diff --git a/client/web/src/routes/Main/Content/Group/utils.tsx b/client/web/src/routes/Main/Content/Group/utils.tsx new file mode 100644 index 00000000..7147c506 --- /dev/null +++ b/client/web/src/routes/Main/Content/Group/utils.tsx @@ -0,0 +1,84 @@ +import { + pluginGroupPanelBadges, + pluginGroupTextPanelExtraMenus, +} from '@/plugin/common'; +import { findPluginPanelInfoByName } from '@/utils/plugin-helper'; +import type { ItemType } from 'antd/lib/menu/hooks/useItems'; +import React, { useMemo } from 'react'; +import { useParams } from 'react-router'; +import { Icon } from 'tailchat-design'; +import { + GroupPanel, + GroupPanelType, + isValidStr, + useSharedEventHandler, +} from 'tailchat-shared'; +import { useUpdate } from 'ahooks'; + +/** + * 获取群组面板的参数 + */ +export function useGroupPanelParams(): { + groupId: string; + panelId: string; +} { + const { groupId = '', panelId = '' } = useParams<{ + groupId: string; + panelId: string; + }>(); + + return { groupId, panelId }; +} + +/** + * 获取来自插件的菜单项 + */ +export function useExtraMenuItems(panel: GroupPanel): ItemType[] { + const extraMenuItems = useMemo(() => { + if (panel.type === GroupPanelType.TEXT) { + return pluginGroupTextPanelExtraMenus; + } else { + return isValidStr(panel.pluginPanelName) + ? findPluginPanelInfoByName(panel.pluginPanelName)?.menus ?? [] + : []; + } + }, [panel.type, panel.pluginPanelName]); + + if (Array.isArray(extraMenuItems) && extraMenuItems.length > 0) { + return [ + { + type: 'divider', + }, + ...extraMenuItems.map((item) => ({ + key: item.name, + label: item.label, + icon: item.icon ? : undefined, + onClick: () => item.onClick(panel), + })), + ]; + } + + return []; +} + +/** + * 获取群组面板额外badge + */ +export function useGroupPanelExtraBadge( + groupId: string, + panelId: string +): React.ReactNode[] { + const update = useUpdate(); + + useSharedEventHandler('groupPanelBadgeUpdate', () => { + update(); + }); + + const extraBadge = pluginGroupPanelBadges.map((item) => ( + + {item.render(groupId, panelId)} + + )); + + return extraBadge; +} diff --git a/client/web/src/styles/antd/dark.less b/client/web/src/styles/antd/dark.less index f5df598b..0322b167 100644 --- a/client/web/src/styles/antd/dark.less +++ b/client/web/src/styles/antd/dark.less @@ -88,6 +88,9 @@ border-top-color: rgba(255, 255, 255, 0.12); color: rgba(255, 255, 255, 0.85); } + .ant-dropdown-menu-item-divider, .ant-dropdown-menu-submenu-title-divider { + background-color: rgba(255, 255, 255, 0.12); + } // 排版 .ant-typography { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 22e7f464..ae999817 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,6 +292,7 @@ importers: '@types/webpack': ^5.28.0 '@types/webpack-bundle-analyzer': ^4.4.1 '@types/webpack-dev-server': ^4.3.1 + ahooks: ^3.7.1 antd: ^4.19.5 autoprefixer: ^10.2.6 axios: ^0.21.1 @@ -371,6 +372,7 @@ importers: yup: ^0.32.9 dependencies: '@loadable/component': 5.15.2_react@18.2.0 + ahooks: 3.7.1_react@18.2.0 antd: 4.22.8_biqbaboplfbrettd7655fr4n2y axios: 0.21.4 clsx: 1.2.1