diff --git a/client/shared/cache/cache.ts b/client/shared/cache/cache.ts index 8555b5a8..f4d16978 100644 --- a/client/shared/cache/cache.ts +++ b/client/shared/cache/cache.ts @@ -1,5 +1,10 @@ import { ChatConverseInfo, fetchConverseInfo } from '../model/converse'; -import { findGroupInviteByCode, GroupInvite } from '../model/group'; +import { + findGroupInviteByCode, + getGroupBasicInfo, + GroupBasicInfo, + GroupInvite, +} from '../model/group'; import { fetchLocalStaticRegistryPlugins, fetchRegistryPlugins, @@ -41,6 +46,19 @@ export async function getCachedConverseInfo( return data; } +/** + * 获取缓存的邀请码信息 + */ +export async function getCachedBaseGroupInfo( + groupId: string +): Promise { + const data = await queryClient.fetchQuery(['baseGroupInfo', groupId], () => + getGroupBasicInfo(groupId) + ); + + return data; +} + /** * 获取缓存的邀请码信息 */ diff --git a/client/shared/index.tsx b/client/shared/index.tsx index 311260c6..104ef987 100644 --- a/client/shared/index.tsx +++ b/client/shared/index.tsx @@ -9,6 +9,7 @@ export type { AppSocket } from './api/socket'; export { getCachedUserInfo, getCachedConverseInfo, + getCachedBaseGroupInfo, getCachedGroupInviteInfo, getCachedRegistryPlugins, } from './cache/cache'; diff --git a/client/web/plugins/com.msgbyte.notify/assets/sounds_bing.mp3 b/client/web/plugins/com.msgbyte.notify/assets/sounds_bing.mp3 new file mode 100644 index 00000000..2827addd Binary files /dev/null and b/client/web/plugins/com.msgbyte.notify/assets/sounds_bing.mp3 differ diff --git a/client/web/plugins/com.msgbyte.notify/package.json b/client/web/plugins/com.msgbyte.notify/package.json index 19abc0cb..895d7086 100644 --- a/client/web/plugins/com.msgbyte.notify/package.json +++ b/client/web/plugins/com.msgbyte.notify/package.json @@ -3,5 +3,10 @@ "main": "src/index.tsx", "version": "0.0.0", "private": true, - "dependencies": {} + "dependencies": { + "tinycon": "^0.6.8" + }, + "devDependencies": { + "@types/tinycon": "^0.6.3" + } } diff --git a/client/web/plugins/com.msgbyte.notify/src/bubble.ts b/client/web/plugins/com.msgbyte.notify/src/bubble.ts new file mode 100644 index 00000000..ad0b0fd4 --- /dev/null +++ b/client/web/plugins/com.msgbyte.notify/src/bubble.ts @@ -0,0 +1,45 @@ +import tinycon from 'tinycon'; + +/** + * 设置小红点 + * @param num 小红点数 + */ +let bubbleNum = 0; +export function setBubble(num: number) { + bubbleNum = num; + if (num < 0) { + num = 0; + } + tinycon.setBubble(num >= 100 ? 99 : num); +} + +/** + * 增加小红点数量 + */ +export function incBubble() { + setBubble(bubbleNum + 1); +} + +const hiddenProperty = + 'hidden' in document + ? 'hidden' + : 'webkitHidden' in document + ? 'webkitHidden' + : 'mozHidden' in document + ? 'mozHidden' + : null; +const visibilityChangeEvent = hiddenProperty?.replace( + /hidden/i, + 'visibilitychange' +); +const onVisibilityChange = function () { + if (!document[hiddenProperty ?? '']) { + // 显示标签页时清空 + tinycon.setBubble(0); + } else { + // 隐藏标签页 + } +}; +if (typeof visibilityChangeEvent === 'string') { + document.addEventListener(visibilityChangeEvent, onVisibilityChange); +} diff --git a/client/web/plugins/com.msgbyte.notify/src/notify.ts b/client/web/plugins/com.msgbyte.notify/src/notify.ts index 84e2bcf2..7b852de8 100644 --- a/client/web/plugins/com.msgbyte.notify/src/notify.ts +++ b/client/web/plugins/com.msgbyte.notify/src/notify.ts @@ -2,11 +2,16 @@ import { regSocketEventListener, getGlobalState, getCachedUserInfo, + getCachedConverseInfo, + getCachedBaseGroupInfo, getServiceWorkerRegistration, navigate, } from '@capital/common'; import { Translate } from './translate'; import { hasSilent } from './silent'; +import { incBubble, setBubble } from './bubble'; + +const TAG = 'tailchat-message'; export function initNotify() { if (Notification.permission === 'default') { @@ -15,7 +20,6 @@ export function initNotify() { const registration: ServiceWorkerRegistration | null = getServiceWorkerRegistration(); - if (registration) { registration.addEventListener('notificationclick', (e: any) => { const tag = e.notification.tag; @@ -25,6 +29,13 @@ export function initNotify() { }); } + let isBlur = false; + window.addEventListener('focus', () => { + setBubble(0); // 点击时清空 + isBlur = false; + }); + window.addEventListener('blur', () => (isBlur = true)); + regSocketEventListener({ eventName: 'chat.message.add', eventFn: (message) => { @@ -32,46 +43,59 @@ export function initNotify() { const currentUserId = getGlobalState()?.user.info._id; if (hasSilent(converseId)) { - // 免打扰 + // 手动设置了当前会话免打扰 return; } - if (currentUserId !== message.author) { - // 创建通知 + if (currentUserId === message.author) { + // 忽略本人消息 + return; + } + + const hidden = window.document.hidden ?? false; + if (hidden || isBlur) { + // 如果当前不是活跃窗口或处于隐藏状态,则创建通知 - // TODO: 需要增加所在群组 if (Notification.permission === 'granted') { - Promise.all([getCachedUserInfo(message.author)]).then( - ([userInfo]) => { - const nickname = userInfo?.nickname ?? ''; - const icon = userInfo?.avatar ?? undefined; - const content = message.content; - - const title = `${Translate.from} ${nickname}`; - const options: NotificationOptions = { - body: content, - icon, - tag: 'tailchat-message', - renotify: true, - data: message, - }; + // TODO: 需要增加显示所在群组 + Promise.all([ + getCachedUserInfo(message.author), + message.groupId + ? getCachedBaseGroupInfo(message.groupId).then((d) => d.name) + : Promise.resolve(Translate.dm), + ]).then(([userInfo, scopeName]) => { + const nickname = userInfo?.nickname ?? ''; + const icon = userInfo?.avatar ?? undefined; + const content = message.content; - if (registration && registration.showNotification) { - registration.showNotification(title, options); - } else { - // fallback - const notification = new Notification(title, options); - notification.onclick = (e: any) => { - const tag = e.target.tag; - const data = e.target.data; - - handleMessageNotifyClick(tag, data); - }; - } + const title = `${Translate.from} [${scopeName}] ${nickname}`; + const options: NotificationOptions = { + body: content, + icon, + tag: TAG, + renotify: true, + data: message, + silent: true, // 因为有提示音了,所以禁音默认音 + }; + + if (registration && registration.showNotification) { + registration.showNotification(title, options); + } else { + // fallback + const notification = new Notification(title, options); + notification.onclick = (e: any) => { + const tag = e.target.tag; + const data = e.target.data; + + handleMessageNotifyClick(tag, data); + }; } - ); + }); } + + incBubble(); } + tryPlayNotificationSound(); // 不管当前是不是处于活跃状态,都发出提示音 }, }); } @@ -97,6 +121,18 @@ function handleMessageNotifyClick(tag, data) { navigate(`/main/personal/converse/${converseId}`); } } +} - console.log(tag, data); +/** + * 尝试播放通知声音 + */ +function tryPlayNotificationSound() { + try { + const audio = new Audio( + '/plugins/com.msgbyte.notify/assets/sounds_bing.mp3' + ); + audio.play(); + } catch (err) { + console.error(err); + } } diff --git a/client/web/plugins/com.msgbyte.notify/src/translate.ts b/client/web/plugins/com.msgbyte.notify/src/translate.ts index 89adea9c..befd0f3c 100644 --- a/client/web/plugins/com.msgbyte.notify/src/translate.ts +++ b/client/web/plugins/com.msgbyte.notify/src/translate.ts @@ -10,4 +10,8 @@ export const Translate = { 'zh-CN': '来自', 'en-US': 'From', }), + dm: localTrans({ + 'zh-CN': '私信', + 'en-US': 'DM', + }), }; diff --git a/client/web/plugins/com.msgbyte.notify/tsconfig.json b/client/web/plugins/com.msgbyte.notify/tsconfig.json index 465a28b5..6dfff4f0 100644 --- a/client/web/plugins/com.msgbyte.notify/tsconfig.json +++ b/client/web/plugins/com.msgbyte.notify/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "baseUrl": "./src", "esModuleInterop": true, "jsx": "react", "paths": { diff --git a/client/web/src/plugin/common/index.ts b/client/web/src/plugin/common/index.ts index ef0a584e..06ce2a89 100644 --- a/client/web/src/plugin/common/index.ts +++ b/client/web/src/plugin/common/index.ts @@ -37,6 +37,7 @@ export { getServiceUrl, getCachedUserInfo, getCachedConverseInfo, + getCachedBaseGroupInfo, localTrans, getLanguage, sharedEvent, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 225da449..5c105454 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -644,7 +644,13 @@ importers: miao-lang: 1.0.5 client/web/plugins/com.msgbyte.notify: - specifiers: {} + specifiers: + '@types/tinycon': ^0.6.3 + tinycon: ^0.6.8 + dependencies: + tinycon: 0.6.8 + devDependencies: + '@types/tinycon': 0.6.3 client/web/plugins/com.msgbyte.openapi: specifiers: {} @@ -11687,6 +11693,10 @@ packages: dependencies: '@types/node': 18.11.16 + /@types/tinycon/0.6.3: + resolution: {integrity: sha512-TC42m8KAyp3AqyZKRXVkk5Qy+oIU8zo2U3362i16Qan0JgZNzLawO7oYnin4BJOy8FSZfOadYYvUWQnpaXoZwg==} + dev: true + /@types/trusted-types/2.0.2: resolution: {integrity: sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==} dev: true @@ -33655,6 +33665,10 @@ packages: resolution: {integrity: sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==} dev: false + /tinycon/0.6.8: + resolution: {integrity: sha512-bF8Lxm4JUXF6Cw0XlZdugJ44GV575OinZ0Pt8vQPr8ooNqd2yyNkoFdCHzmdpHlgoqfSLfcyk4HDP1EyllT+ug==} + dev: false + /title-case/2.1.1: resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} dependencies: