diff --git a/packages/frontend/src/components/Settings/Notifications.tsx b/packages/frontend/src/components/Settings/Notifications.tsx index ca520326f1..47197a5d77 100644 --- a/packages/frontend/src/components/Settings/Notifications.tsx +++ b/packages/frontend/src/components/Settings/Notifications.tsx @@ -22,6 +22,12 @@ export default function Notifications({ desktopSettings }: Props) { <> {tx('pref_current_account')} + {tx('pref_all_accounts')} { - incomingMessageHandler(accountId, chatId, msgId, false) + log.debug('IncomingMsg', { accountId, msgId, chatId }) + incomingMessageHandler(accountId, chatId, msgId, NOTIFICATION_TYPE.MESSAGE) }) + BackendRemote.on( 'IncomingWebxdcNotify', (accountId, { msgId, text, chatId }) => { - incomingMessageHandler(accountId, chatId, msgId, true, text) + incomingMessageHandler( + accountId, + chatId, + msgId, + NOTIFICATION_TYPE.WEBXDC_INFO, + text + ) } ) + + BackendRemote.on( + 'IncomingReaction', + (accountId, { contactId, chatId, msgId, reaction }) => { + log.debug('IncomingReaction', { contactId, chatId, msgId, reaction }) + incomingMessageHandler( + accountId, + chatId, + msgId, + NOTIFICATION_TYPE.REACTION, + reaction, + contactId + ) + } + ) + BackendRemote.on('IncomingMsgBunch', accountId => { flushNotifications(accountId) }) @@ -39,23 +64,25 @@ function isMuted(accountId: number, chatId: number) { return BackendRemote.rpc.isChatMuted(accountId, chatId) } -type queuedNotification = { +type QueuedNotification = { chatId: number messageId: number - isWebxdcInfo: boolean - eventText?: string + notificationType: NOTIFICATION_TYPE + eventText: string // for webxdc-info notifications or reactions + contactId?: number // for reactions } let queuedNotifications: { - [accountId: number]: queuedNotification[] + [accountId: number]: QueuedNotification[] } = {} function incomingMessageHandler( accountId: number, chatId: number, messageId: number, - isWebxdcInfo: boolean, - eventText?: string + notificationType: NOTIFICATION_TYPE, + eventText = '', + contactId?: number ) { log.debug('incomingMessageHandler: ', { chatId, messageId }) @@ -90,8 +117,9 @@ function incomingMessageHandler( queuedNotifications[accountId].push({ chatId, messageId, - isWebxdcInfo, + notificationType, eventText, + contactId, }) } @@ -99,8 +127,9 @@ async function showNotification( accountId: number, chatId: number, messageId: number, - isWebxdcInfo: boolean, - eventText?: string + notificationType: NOTIFICATION_TYPE, + eventText: string, + contactId?: number ) { const tx = window.static_translate @@ -112,16 +141,17 @@ async function showNotification( chatId, messageId, accountId, + notificationType, }) } else { try { const notificationInfo = await BackendRemote.rpc.getMessageNotificationInfo(accountId, messageId) - let summaryPrefix = notificationInfo.summaryPrefix - const summaryText = eventText ?? notificationInfo.summaryText + let summaryPrefix = notificationInfo.summaryPrefix ?? '' + let summaryText = notificationInfo.summaryText ?? '' const chatName = notificationInfo.chatName let icon = getNotificationIcon(notificationInfo) - if (isWebxdcInfo) { + if (notificationType === NOTIFICATION_TYPE.WEBXDC_INFO) { /** * messageId may refer to a webxdc message OR a wexdc-info-message! * @@ -141,6 +171,7 @@ async function showNotification( ) } if (message.webxdcInfo) { + summaryText = eventText summaryPrefix = `${message.webxdcInfo.name}` if (message.webxdcInfo.icon) { const iconName = message.webxdcInfo.icon @@ -156,6 +187,19 @@ async function showNotification( } else { throw new Error(`no webxdcInfo in message with id ${message.id}`) } + } else if (notificationType === NOTIFICATION_TYPE.REACTION) { + if (contactId) { + const reactionSender = await BackendRemote.rpc.getContact( + accountId, + contactId + ) + summaryText = `${tx('reaction_by_other', [ + reactionSender.displayName, + eventText, + summaryText, + ])}` + summaryPrefix = '' // not needed, sender name is included in summaryText + } } runtime.showNotification({ title: chatName, @@ -164,6 +208,7 @@ async function showNotification( chatId, messageId, accountId, + notificationType, }) } catch (error) { log.error('failed to create notification for message: ', messageId, error) @@ -173,7 +218,7 @@ async function showNotification( async function showGroupedNotification( accountId: number, - notifications: queuedNotification[] + notifications: QueuedNotification[] ) { const tx = window.static_translate @@ -185,6 +230,7 @@ async function showGroupedNotification( chatId: 0, messageId: 0, accountId, + notificationType: NOTIFICATION_TYPE.MESSAGE, }) } else { const chatIds = [...new Set(notifications.map(({ chatId }) => chatId))] @@ -211,6 +257,7 @@ async function showGroupedNotification( chatId: chatIds[0], messageId: 0, // just select chat on click, no specific message accountId, + notificationType: NOTIFICATION_TYPE.MESSAGE, }) } else { // messages from diffent chats @@ -227,6 +274,7 @@ async function showGroupedNotification( chatId: 0, messageId: 0, accountId, + notificationType: NOTIFICATION_TYPE.MESSAGE, }) } } catch (error) { @@ -246,7 +294,7 @@ async function flushNotifications(accountId: number) { // make it work even if there is nothing queuedNotifications[accountId] = [] } - let notifications = [...queuedNotifications[accountId]] + const notifications = [...queuedNotifications[accountId]] queuedNotifications = [] // filter out muted chats: @@ -264,37 +312,83 @@ async function flushNotifications(accountId: number) { // some chats are muted log.debug(`ignoring notifications of ${mutedChats.length} muted chats`) } - notifications = notifications.filter(notification => { - if (mutedChats.includes(notification.chatId)) { - // muted chat - log.debug('notification ignored: chat muted', notification) - return false - } else { - return true - } - }) + const filteredNotifications = ( + await Promise.all( + notifications.map(async notification => { + if (mutedChats.includes(notification.chatId)) { + // muted chat - only show if it's a mention and mentions are enabled + const isMention = await notificationIsMention(accountId, notification) + return isMention ? notification : null + } else { + log.debug('notification on not muted chat:', notification) + return notification + } + }) + ) + ).filter(notification => notification !== null) - if (notifications.length > notificationLimit) { + if (filteredNotifications.length > notificationLimit) { showGroupedNotification(accountId, notifications) } else { for (const { chatId, messageId, - isWebxdcInfo, + notificationType, eventText, - } of notifications) { + contactId, + } of filteredNotifications) { await showNotification( accountId, chatId, messageId, - isWebxdcInfo, - eventText + notificationType, + eventText, + contactId ) } } notificationLimit = NORMAL_LIMIT } +/** + * if isMentionsEnabled returns true + * if the notification is a mention + */ +async function notificationIsMention( + accountId: number, + notification: QueuedNotification +) { + if (!SettingsStoreInstance.state?.desktopSettings.isMentionsEnabled) { + log.info('allowIfMention: mentions are disabled') + return false + } else if (notification.notificationType === NOTIFICATION_TYPE.WEBXDC_INFO) { + log.info('mention detected: webxdc-info notification') + return true + } else { + const message = await BackendRemote.rpc.getMessage( + accountId, + notification.messageId + ) + if (notification.notificationType === NOTIFICATION_TYPE.REACTION) { + if (message.sender.id === C.DC_CONTACT_ID_SELF) { + log.info('mention detected: reaction to own message') + return true + } + } + if (message.quote && message.quote.kind === 'WithMessage') { + const quote = await BackendRemote.rpc.getMessage( + accountId, + message.quote.messageId + ) + if (quote.sender.id === C.DC_CONTACT_ID_SELF) { + log.info('mention detected: answer to own message') + return true + } + } + return false + } +} + export function clearNotificationsForChat(accountId: number, chatId: number) { log.debug('clearNotificationsForChat', accountId, chatId) // ask runtime to delete the notifications diff --git a/packages/shared/constants.ts b/packages/shared/constants.ts index 6afb9c5fd3..af7e1c96d2 100644 --- a/packages/shared/constants.ts +++ b/packages/shared/constants.ts @@ -33,3 +33,9 @@ export const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'apng', 'gif', 'webp'] // Videochat Server URLs export const VIDEO_CHAT_INSTANCE_SYSTEMLI = 'https://meet.systemli.org/$ROOM' export const VIDEO_CHAT_INSTANCE_AUTISTICI = 'https://vc.autistici.org/$ROOM' + +export const enum NOTIFICATION_TYPE { + MESSAGE, + REACTION, + WEBXDC_INFO, +} diff --git a/packages/shared/shared-types.d.ts b/packages/shared/shared-types.d.ts index eeab6c814c..b4922994d5 100644 --- a/packages/shared/shared-types.d.ts +++ b/packages/shared/shared-types.d.ts @@ -25,6 +25,7 @@ export interface DesktopSettingsType { locale: string | null notifications: boolean showNotificationContent: boolean + isMentionsEnabled: boolean /** @deprecated isn't used anymore since the move to jsonrpc */ lastChats: { [accountId: number]: number } zoomFactor: number @@ -69,6 +70,7 @@ export interface RC_Config { } import type { T } from '@deltachat/jsonrpc-client' +import { NOTIFICATION_TYPE } from './constants.ts' export type Theme = { name: string @@ -117,6 +119,7 @@ export interface DcNotification { messageId: number // for future accountId: number + notificationType: NOTIFICATION_TYPE } export interface DcOpenWebxdcParameters {