Skip to content

Commit

Permalink
feat: notify on mentions in muted chat
Browse files Browse the repository at this point in the history
resolves #4461
  • Loading branch information
nicodh committed Jan 23, 2025
1 parent cb39d10 commit c082923
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 31 deletions.
6 changes: 6 additions & 0 deletions packages/frontend/src/components/Settings/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export default function Notifications({ desktopSettings }: Props) {
<>
<SettingsHeading>{tx('pref_current_account')}</SettingsHeading>
<AccountNotificationMutedSwitch label={tx('menu_mute')} />
<DesktopSettingsSwitch
settingsKey='isMentionsEnabled'
label={tx('pref_mention_notifications')}
description={tx('pref_mention_notifications_explain')}
disabled={!desktopSettings['notifications']}
/>
<SettingsSeparator></SettingsSeparator>
<SettingsHeading>{tx('pref_all_accounts')}</SettingsHeading>
<DesktopSettingsSwitch
Expand Down
156 changes: 125 additions & 31 deletions packages/frontend/src/system-integration/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { appName } from '../../../shared/constants'
import { getLogger } from '../../../shared/logger'
import { NOTIFICATION_TYPE } from '../../../shared/constants'
import { BackendRemote } from '../backend-com'
import { isImage } from '../components/attachment/Attachment'
import { runtime } from '@deltachat-desktop/runtime-interface'
import SettingsStoreInstance from '../stores/settings'
import AccountNotificationStoreInstance from '../stores/accountNotifications'

import type { T } from '@deltachat/jsonrpc-client'
import { C, type T } from '@deltachat/jsonrpc-client'

const log = getLogger('renderer/notifications')

Expand All @@ -22,14 +23,38 @@ const log = getLogger('renderer/notifications')

export function initNotifications() {
BackendRemote.on('IncomingMsg', (accountId, { chatId, msgId }) => {
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)
})
Expand All @@ -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 })

Expand Down Expand Up @@ -90,17 +117,19 @@ function incomingMessageHandler(
queuedNotifications[accountId].push({
chatId,
messageId,
isWebxdcInfo,
notificationType,
eventText,
contactId,
})
}

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

Expand All @@ -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!
*
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -164,6 +208,7 @@ async function showNotification(
chatId,
messageId,
accountId,
notificationType,
})
} catch (error) {
log.error('failed to create notification for message: ', messageId, error)
Expand All @@ -173,7 +218,7 @@ async function showNotification(

async function showGroupedNotification(
accountId: number,
notifications: queuedNotification[]
notifications: QueuedNotification[]
) {
const tx = window.static_translate

Expand All @@ -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))]
Expand All @@ -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
Expand All @@ -227,6 +274,7 @@ async function showGroupedNotification(
chatId: 0,
messageId: 0,
accountId,
notificationType: NOTIFICATION_TYPE.MESSAGE,
})
}
} catch (error) {
Expand All @@ -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:
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
3 changes: 3 additions & 0 deletions packages/shared/shared-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -117,6 +119,7 @@ export interface DcNotification {
messageId: number
// for future
accountId: number
notificationType: NOTIFICATION_TYPE
}

export interface DcOpenWebxdcParameters {
Expand Down

0 comments on commit c082923

Please sign in to comment.