feat: 增加群组消息通知免打扰功能

pull/56/head
moonrailgun 3 years ago
parent 0050f233a2
commit 27e9969796

@ -35,6 +35,11 @@ export interface SharedEventMap {
* ()
*/
readMessage: (payload: ChatMessage | null) => void;
/**
*
*/
groupPanelBadgeUpdate: () => void;
}
export type SharedEventType = keyof SharedEventMap;

@ -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);
}

@ -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

@ -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",

@ -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({

@ -1,6 +1,6 @@
{
"name": "@plugins/com.msgbyte.notify",
"main": "src/index.ts",
"main": "src/index.tsx",
"version": "0.0.0",
"private": true,
"dependencies": {}

@ -1,7 +0,0 @@
import { initNotify } from './notify';
if ('Notification' in window) {
initNotify();
} else {
console.warn('浏览器不支持 Notification');
}

@ -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) ? <Icon icon="mdi:bell-off-outline" /> : null;
},
});

@ -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) {
// 创建通知

@ -0,0 +1,39 @@
/**
*
*/
const KEY = 'plugin:com.msgbyte.notify/slientStorage';
const silentSet = new Set<string>(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 [];
}
}

@ -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}
</Typography.Text>
<Space>
{badge === true ? (
<Badge status="error" />
) : (
<Badge count={Number(badge) || 0} />
)}
{props.extraBadge}
</Space>
</div>
</Link>
);

@ -46,6 +46,13 @@ export interface PluginCustomPanel {
export const [pluginCustomPanel, regCustomPanel] =
buildRegList<PluginCustomPanel>();
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<PluginGroupPanel>();
@ -212,3 +214,19 @@ export const [pluginPanelActions, regPluginPanelAction] = buildRegList<
*/
export const [pluginPermission, regPluginPermission] =
buildRegList<PermissionItemType>();
/**
* badge
*/
export const [pluginGroupPanelBadges, regGroupPanelBadge] = buildRegList<{
name: string;
render: (groupId: string, panelId: string) => React.ReactNode;
}>();
/**
*
*/
export const [
pluginGroupTextPanelExtraMenus,
regPluginGroupTextPanelExtraMenu,
] = buildRegList<PluginPanelMenu>();

@ -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 ? <Icon 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 <LoadingSpinner />;
}
const isPinned =
isValidStr(groupInfo.pinnedPanelId) && groupInfo.pinnedPanelId === panel.id;
isValidStr(groupInfo.pinnedPanelId) && groupInfo.pinnedPanelId === panelId;
const menu = (
<Menu
@ -85,7 +53,7 @@ export const SidebarItem: React.FC<{
label: t('复制链接'),
icon: <Icon icon="mdi:content-copy" />,
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<{
<GroupPanelItem
name={panel.name}
icon={icon}
to={`/main/group/${groupId}/${panel.id}`}
to={`/main/group/${groupId}/${panelId}`}
extraBadge={extraBadge}
/>
)}
</div>

@ -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<GroupTextPanelItemProps> = React.memo(
const { groupId, panel } = props;
const panelId = panel.id;
const hasUnread = useGroupTextPanelUnread(panelId);
const extraBadge = useGroupPanelExtraBadge(groupId, panelId);
return (
<GroupPanelItem
@ -23,6 +25,7 @@ export const GroupTextPanelItem: React.FC<GroupTextPanelItemProps> = React.memo(
icon={props.icon}
to={`/main/group/${groupId}/${panel.id}`}
badge={hasUnread}
extraBadge={extraBadge}
/>
);
}

@ -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 };
}

@ -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 ? <Icon 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) => (
<React.Fragment key={panelId + item.name}>
{item.render(groupId, panelId)}
</React.Fragment>
));
return extraBadge;
}

@ -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 {

@ -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

Loading…
Cancel
Save