refactor: 重构消息通知模式,将免打扰相关的逻辑集成到核心代码中

避免怪异的用户体验
pull/70/head
moonrailgun 2 years ago
parent 184daa3e73
commit 66c1038970

@ -47,6 +47,16 @@ export interface SharedEventMap {
*/ */
readMessage: (payload: ChatMessage | null) => void; readMessage: (payload: ChatMessage | null) => void;
/**
* ()
*/
receiveMessage: (payload: ChatMessage) => void;
/**
*
*/
receiveUnmutedMessage: (payload: ChatMessage) => void;
/** /**
* *
*/ */

@ -0,0 +1,23 @@
import { sharedEvent, useSharedEventHandler } from '../../event';
import { useUserNotifyMute } from './useUserSettings';
/**
*
*
*
* {receiveMessage} -> -> {receiveUnmutedMessage}
* -> ,
*/
export function useMessageNotifyEventFilter() {
const { checkIsMuted } = useUserNotifyMute();
useSharedEventHandler('receiveMessage', (payload) => {
if (!payload) {
return;
}
if (!checkIsMuted(payload.converseId, payload.groupId)) {
sharedEvent.emit('receiveUnmutedMessage', payload);
}
});
}

@ -7,6 +7,8 @@ import {
UserSettings, UserSettings,
} from '../../model/user'; } from '../../model/user';
import { useAsyncRequest } from '../useAsyncRequest'; import { useAsyncRequest } from '../useAsyncRequest';
import { useMemoizedFn } from '../useMemoizedFn';
import _without from 'lodash/without';
/** /**
* hooks * hooks
@ -17,7 +19,7 @@ export function useUserSettings() {
[CacheKey], [CacheKey],
() => getUserSettings(), () => getUserSettings(),
{ {
staleTime: 1 * 60 * 1000, // 缓存1分钟 staleTime: 10 * 60 * 1000, // 缓存10分钟
} }
); );
@ -56,3 +58,48 @@ export function useSingleUserSetting<K extends keyof UserSettings>(
loading, loading,
}; };
} }
/**
*
*/
export function useUserNotifyMute() {
const { value: list = [], setValue: setList } = useSingleUserSetting(
'messageNotificationMuteList',
[]
);
const mute = useMemoizedFn((converseOrGroupId: string) => {
setList([...list, converseOrGroupId]);
});
const unmute = useMemoizedFn((converseOrGroupId: string) => {
setList(_without(list, converseOrGroupId));
});
const toggleMute = useMemoizedFn((converseOrGroupId) => {
if (list.includes(converseOrGroupId)) {
unmute(converseOrGroupId);
} else {
mute(converseOrGroupId);
}
});
/**
*
*/
const checkIsMuted = useMemoizedFn((panelId: string, groupId?: string) => {
if (groupId) {
return list.includes(panelId) || list.includes(groupId);
}
return list.includes(panelId);
});
return {
mutedList: list,
mute,
unmute,
toggleMute,
checkIsMuted,
};
}

@ -54,11 +54,13 @@ export { useLanguage } from './i18n/language';
// hooks // hooks
export { createUseStorageState } from './hooks/factory/createUseStorageState'; export { createUseStorageState } from './hooks/factory/createUseStorageState';
export { useAvailableServices } from './hooks/model/useAvailableServices'; export { useAvailableServices } from './hooks/model/useAvailableServices';
export { useMessageNotifyEventFilter } from './hooks/model/useMessageNotifyEventFilter';
export { useUserInfoList } from './hooks/model/useUserInfoList'; export { useUserInfoList } from './hooks/model/useUserInfoList';
export { useUsernames } from './hooks/model/useUsernames'; export { useUsernames } from './hooks/model/useUsernames';
export { export {
useUserSettings, useUserSettings,
useSingleUserSetting, useSingleUserSetting,
useUserNotifyMute,
} from './hooks/model/useUserSettings'; } from './hooks/model/useUserSettings';
export { useAlphaMode } from './hooks/useAlphaMode'; export { useAlphaMode } from './hooks/useAlphaMode';
export { useAsync } from './hooks/useAsync'; export { useAsync } from './hooks/useAsync';

@ -32,6 +32,11 @@ export interface UserSettings {
*/ */
messageListVirtualization?: boolean; messageListVirtualization?: boolean;
/**
* ()
*/
messageNotificationMuteList?: string[];
/** /**
* *
*/ */

@ -200,6 +200,8 @@ function listenNotify(socket: AppSocket, store: AppStore) {
]) ])
); );
} }
sharedEvent.emit('receiveMessage', message); // 推送到通知中心
}); });
socket.listen<ChatMessage>('chat.message.update', (message) => { socket.listen<ChatMessage>('chat.message.update', (message) => {

@ -1,16 +1,7 @@
import { import { regPluginSettings, showToasts } from '@capital/common';
regGroupPanelBadge,
regPluginGroupTextPanelExtraMenu,
regPluginSettings,
sharedEvent,
showToasts,
} from '@capital/common';
import { Icon } from '@capital/component';
import React from 'react';
import { appendSilent, hasSilent, removeSilent } from './silent';
import { initNotify } from './notify'; import { initNotify } from './notify';
import { Translate } from './translate'; import { Translate } from './translate';
import { PLUGIN_NAME, PLUGIN_SYSTEM_SETTINGS_DISABLED_SOUND } from './const'; import { PLUGIN_SYSTEM_SETTINGS_DISABLED_SOUND } from './const';
if ('Notification' in window) { if ('Notification' in window) {
initNotify(); initNotify();
@ -19,28 +10,6 @@ if ('Notification' in window) {
console.warn(Translate.nosupport); console.warn(Translate.nosupport);
} }
regPluginGroupTextPanelExtraMenu({
name: `${PLUGIN_NAME}/grouppanelmenu`,
label: Translate.slient,
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;
},
});
regPluginSettings({ regPluginSettings({
name: PLUGIN_SYSTEM_SETTINGS_DISABLED_SOUND, name: PLUGIN_SYSTEM_SETTINGS_DISABLED_SOUND,
label: Translate.disabledSound, label: Translate.disabledSound,

@ -1,5 +1,4 @@
import { import {
regSocketEventListener,
getGlobalState, getGlobalState,
getCachedUserInfo, getCachedUserInfo,
getCachedBaseGroupInfo, getCachedBaseGroupInfo,
@ -9,7 +8,6 @@ import {
getCachedUserSettings, getCachedUserSettings,
} from '@capital/common'; } from '@capital/common';
import { Translate } from './translate'; import { Translate } from './translate';
import { hasSilent } from './silent';
import { incBubble, setBubble } from './bubble'; import { incBubble, setBubble } from './bubble';
import _get from 'lodash/get'; import _get from 'lodash/get';
import { PLUGIN_SYSTEM_SETTINGS_DISABLED_SOUND } from './const'; import { PLUGIN_SYSTEM_SETTINGS_DISABLED_SOUND } from './const';
@ -39,17 +37,9 @@ export function initNotify() {
}); });
window.addEventListener('blur', () => (isBlur = true)); window.addEventListener('blur', () => (isBlur = true));
regSocketEventListener({ sharedEvent.on('receiveUnmutedMessage', (message) => {
eventName: 'chat.message.add',
eventFn: (message) => {
const converseId = message.converseId;
const currentUserId = getGlobalState()?.user.info._id; const currentUserId = getGlobalState()?.user.info._id;
if (hasSilent(converseId)) {
// 手动设置了当前会话免打扰
return;
}
if (currentUserId === message.author) { if (currentUserId === message.author) {
// 忽略本人消息 // 忽略本人消息
return; return;
@ -99,7 +89,6 @@ export function initNotify() {
incBubble(); incBubble();
} }
tryPlayNotificationSound(); // 不管当前是不是处于活跃状态,都发出提示音 tryPlayNotificationSound(); // 不管当前是不是处于活跃状态,都发出提示音
},
}); });
} }

@ -1,39 +0,0 @@
/**
*
*/
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, Space, Typography } from 'antd'; import { Badge, BadgeProps, Space, Typography } from 'antd';
import clsx from 'clsx'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
@ -12,10 +12,12 @@ export const GroupPanelItem: React.FC<{
name: string; name: string;
icon: React.ReactNode; icon: React.ReactNode;
to: string; to: string;
dimmed?: boolean; // 颜色暗淡
badge?: boolean; badge?: boolean;
badgeProps?: BadgeProps;
extraBadge?: React.ReactNode[]; extraBadge?: React.ReactNode[];
}> = React.memo((props) => { }> = React.memo((props) => {
const { icon, name, to, badge } = props; const { icon, name, to, dimmed = false, badge } = props;
const location = useLocation(); const location = useLocation();
const isActive = location.pathname.startsWith(to); const isActive = location.pathname.startsWith(to);
@ -26,24 +28,26 @@ export const GroupPanelItem: React.FC<{
'w-full hover:bg-black hover:bg-opacity-20 dark:hover:bg-white dark:hover:bg-opacity-20 cursor-pointer text-gray-900 dark:text-white rounded px-1 h-8 flex items-center text-base group', 'w-full hover:bg-black hover:bg-opacity-20 dark:hover:bg-white dark:hover:bg-opacity-20 cursor-pointer text-gray-900 dark:text-white rounded px-1 h-8 flex items-center text-base group',
{ {
'bg-black bg-opacity-20 dark:bg-white dark:bg-opacity-20': isActive, 'bg-black bg-opacity-20 dark:bg-white dark:bg-opacity-20': isActive,
} },
dimmed && 'text-opacity-40 dark:text-opacity-40'
)} )}
> >
<div className="flex items-center justify-center px-1 mr-1">{icon}</div> <div className={clsx('flex items-center justify-center px-1 mr-1')}>
{icon}
</div>
<Typography.Text <Typography.Text
className="flex-1 text-gray-900 dark:text-white" className={clsx(
'flex-1 text-gray-900 dark:text-white',
dimmed && 'text-opacity-40 dark:text-opacity-40'
)}
ellipsis={true} ellipsis={true}
> >
{name} {name}
</Typography.Text> </Typography.Text>
<Space> <Space>
{badge === true ? ( {badge === true && <Badge status="error" {...props.badgeProps} />}
<Badge status="error" />
) : (
<Badge count={Number(badge) || 0} />
)}
{props.extraBadge} {props.extraBadge}
</Space> </Space>

@ -9,10 +9,11 @@ import {
useAppDispatch, useAppDispatch,
useConverseAck, useConverseAck,
useGroupInfo, useGroupInfo,
useUserNotifyMute,
} from 'tailchat-shared'; } from 'tailchat-shared';
import { GroupPanelItem } from '@/components/GroupPanelItem'; import { GroupPanelItem } from '@/components/GroupPanelItem';
import { GroupTextPanelItem } from './TextPanelItem'; import { GroupTextPanelItem } from './TextPanelItem';
import { Dropdown, Menu, MenuProps } from 'antd'; import { Dropdown, MenuProps } from 'antd';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { usePanelWindow } from '@/hooks/usePanelWindow'; import { usePanelWindow } from '@/hooks/usePanelWindow';
import { LoadingSpinner } from '@/components/LoadingSpinner'; import { LoadingSpinner } from '@/components/LoadingSpinner';
@ -37,6 +38,7 @@ export const SidebarItem: React.FC<{
const { markConverseAllAck } = useConverseAck(panelId); const { markConverseAllAck } = useConverseAck(panelId);
const extraMenuItems = useExtraMenuItems(panel); const extraMenuItems = useExtraMenuItems(panel);
const extraBadge = useGroupPanelExtraBadge(groupId, panelId); const extraBadge = useGroupPanelExtraBadge(groupId, panelId);
const { checkIsMuted, toggleMute } = useUserNotifyMute();
if (!groupInfo) { if (!groupInfo) {
return <LoadingSpinner />; return <LoadingSpinner />;
@ -95,6 +97,12 @@ export const SidebarItem: React.FC<{
icon: <Icon icon="mdi:message-badge-outline" />, icon: <Icon icon="mdi:message-badge-outline" />,
onClick: markConverseAllAck, onClick: markConverseAllAck,
}, },
panel.type === GroupPanelType.TEXT && {
key: 'mute',
label: checkIsMuted(panelId, groupId) ? t('取消免打扰') : t('免打扰'),
icon: <Icon icon="mdi:bell-off-outline" />,
onClick: () => toggleMute(panelId),
},
...extraMenuItems, ...extraMenuItems,
]), ]),
}; };

@ -1,6 +1,10 @@
import { GroupPanelItem } from '@/components/GroupPanelItem'; import { GroupPanelItem } from '@/components/GroupPanelItem';
import React from 'react'; import React, { useMemo } from 'react';
import { GroupPanel, useGroupTextPanelUnread } from 'tailchat-shared'; import {
GroupPanel,
useGroupTextPanelUnread,
useUserNotifyMute,
} from 'tailchat-shared';
import { useGroupPanelExtraBadge } from './utils'; import { useGroupPanelExtraBadge } from './utils';
interface GroupTextPanelItemProps { interface GroupTextPanelItemProps {
@ -18,13 +22,22 @@ export const GroupTextPanelItem: React.FC<GroupTextPanelItemProps> = React.memo(
const panelId = panel.id; const panelId = panel.id;
const hasUnread = useGroupTextPanelUnread(panelId); const hasUnread = useGroupTextPanelUnread(panelId);
const extraBadge = useGroupPanelExtraBadge(groupId, panelId); const extraBadge = useGroupPanelExtraBadge(groupId, panelId);
const { checkIsMuted } = useUserNotifyMute();
const isMuted = useMemo(
() => checkIsMuted(panelId, groupId),
[groupId, panelId]
);
return ( return (
<GroupPanelItem <GroupPanelItem
name={panel.name} name={panel.name}
icon={props.icon} icon={props.icon}
to={`/main/group/${groupId}/${panel.id}`} to={`/main/group/${groupId}/${panel.id}`}
dimmed={isMuted}
badge={hasUnread} badge={hasUnread}
badgeProps={{
status: isMuted ? 'default' : 'error',
}}
extraBadge={extraBadge} extraBadge={extraBadge}
/> />
); );

@ -1,6 +1,7 @@
import { GlobalTemporaryTip } from '@/components/GlobalTemporaryTip'; import { GlobalTemporaryTip } from '@/components/GlobalTemporaryTip';
import { useRecordMeasure } from '@/utils/measure-helper'; import { useRecordMeasure } from '@/utils/measure-helper';
import React from 'react'; import React from 'react';
import { useMessageNotifyEventFilter } from 'tailchat-shared';
import { MainContent } from './Content'; import { MainContent } from './Content';
import { Navbar } from './Navbar'; import { Navbar } from './Navbar';
import { MainProvider } from './Provider'; import { MainProvider } from './Provider';
@ -9,6 +10,7 @@ import { useShortcuts } from './useShortcuts';
const MainRoute: React.FC = React.memo(() => { const MainRoute: React.FC = React.memo(() => {
useRecordMeasure('appMainRenderStart'); useRecordMeasure('appMainRenderStart');
useShortcuts(); useShortcuts();
useMessageNotifyEventFilter();
return ( return (
<MainProvider> <MainProvider>

Loading…
Cancel
Save