Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add webxdc notification handler #4400

Merged
merged 12 commits into from
Dec 16, 2024
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ node_modules
packages/target-electron/bundle_out
packages/target-electron/tests/compiled
packages/shared/ts-compiled-for-tests
packages/e2e-tests/test-results/*

packages/target-browser/dist
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## [Unreleased][unreleased]

## Added
- show specific notifications for webxdc events #4400
- expose sendUpdateInterval & sendUpdateMaxSize in webxdc

## Changed
- Update `@deltachat/stdio-rpc-server` and `deltachat/jsonrpc-client` to `1.151.5`
Expand Down
112 changes: 94 additions & 18 deletions packages/frontend/src/system-integration/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,36 @@ import type { T } from '@deltachat/jsonrpc-client'

const log = getLogger('renderer/notifications')

/**
* Notification handling:
*
* - listens for incoming notifications
* - reflects notification settings
* - prepares notification data (DcNotification)
* - queues notifications if needed to avoid "mass" notifications
* - sends notifications to runtime (which invokes ipcBackend)
*/

export function initNotifications() {
BackendRemote.on('IncomingMsg', (accountId, { chatId, msgId }) => {
incomingMessageHandler(accountId, chatId, msgId)
})
BackendRemote.on('IncomingWebxdcNotify', (accountId, { msgId }) => {
incomingWebxdcEventHandler(accountId, msgId)
})
BackendRemote.on('IncomingMsgBunch', accountId => {
flushNotifications(accountId)
})
}

function isMuted(accountId: number, chatId: number) {
return BackendRemote.rpc.isChatMuted(accountId, chatId)
}

type queuedNotification = {
chatId: number
messageId: number
isWebxdcInfo?: boolean
}

let queuedNotifications: {
Expand All @@ -26,7 +49,8 @@ let queuedNotifications: {
function incomingMessageHandler(
accountId: number,
chatId: number,
messageId: number
messageId: number,
isWebxdcInfo: boolean = false
) {
log.debug('incomingMessageHandler: ', { chatId, messageId })

Expand Down Expand Up @@ -58,13 +82,23 @@ function incomingMessageHandler(
if (typeof queuedNotifications[accountId] === 'undefined') {
queuedNotifications[accountId] = []
}
queuedNotifications[accountId].push({ chatId, messageId })
queuedNotifications[accountId].push({ chatId, messageId, isWebxdcInfo })
}

async function incomingWebxdcEventHandler(
accountId: number,
messageId: number
) {
const message = await BackendRemote.rpc.getMessage(accountId, messageId)
const chatId = message.chatId
incomingMessageHandler(accountId, chatId, messageId, true)
}

async function showNotification(
accountId: number,
chatId: number,
messageId: number
messageId: number,
isWebxdcInfo: boolean
) {
const tx = window.static_translate

Expand All @@ -76,19 +110,62 @@ async function showNotification(
chatId,
messageId,
accountId,
isWebxdcInfo,
})
} else {
try {
const notificationInfo =
await BackendRemote.rpc.getMessageNotificationInfo(accountId, messageId)
const { chatName, summaryPrefix, summaryText } = notificationInfo
let chatName = ''
WofWca marked this conversation as resolved.
Show resolved Hide resolved
let summaryPrefix = ''
let summaryText = ''
let notificationInfo: T.MessageNotificationInfo | undefined
WofWca marked this conversation as resolved.
Show resolved Hide resolved
let icon: string | null = null
if (isWebxdcInfo) {
const relatedMessage = await BackendRemote.rpc.getMessage(
accountId,
messageId
)
if (
relatedMessage.systemMessageType === 'WebxdcInfoMessage' &&
relatedMessage.parentId
) {
WofWca marked this conversation as resolved.
Show resolved Hide resolved
summaryText = relatedMessage.text
const webxdcMessage = await BackendRemote.rpc.getMessage(
accountId,
relatedMessage.parentId
)
if (webxdcMessage.webxdcInfo) {
summaryPrefix = `${webxdcMessage.webxdcInfo.name}`
if (webxdcMessage.webxdcInfo.icon) {
const iconName = webxdcMessage.webxdcInfo.icon
const iconBlob = await BackendRemote.rpc.getWebxdcBlob(
accountId,
webxdcMessage.id,
iconName
)
// needed for valid dataUrl
const imageExtension = iconName.split('.').pop()
icon = `data:image/${imageExtension};base64, ${iconBlob}`
WofWca marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
} else {
notificationInfo = await BackendRemote.rpc.getMessageNotificationInfo(
accountId,
messageId
)
WofWca marked this conversation as resolved.
Show resolved Hide resolved
chatName = notificationInfo.chatName
summaryPrefix = notificationInfo.summaryPrefix ?? ''
summaryText = notificationInfo.summaryText ?? ''
icon = getNotificationIcon(notificationInfo)
}
runtime.showNotification({
title: chatName,
body: summaryPrefix ? `${summaryPrefix}: ${summaryText}` : summaryText,
icon: getNotificationIcon(notificationInfo),
icon,
chatId,
messageId,
accountId,
isWebxdcInfo,
})
} catch (error) {
log.error('failed to create notification for message: ', messageId, error)
Expand All @@ -110,6 +187,7 @@ async function showGroupedNotification(
chatId: 0,
messageId: 0,
accountId,
isWebxdcInfo: false,
})
} else {
const chatIds = [...new Set(notifications.map(({ chatId }) => chatId))]
Expand All @@ -136,6 +214,7 @@ async function showGroupedNotification(
chatId: chatIds[0],
messageId: 0, // just select chat on click, no specific message
accountId,
isWebxdcInfo: false, // no way to handle webxdcInfo in grouped notifications
})
} else {
// messages from diffent chats
Expand All @@ -152,6 +231,7 @@ async function showGroupedNotification(
chatId: 0,
messageId: 0,
accountId,
isWebxdcInfo: false,
})
}
} catch (error) {
Expand Down Expand Up @@ -202,8 +282,13 @@ async function flushNotifications(accountId: number) {
if (notifications.length > notificationLimit) {
showGroupedNotification(accountId, notifications)
} else {
for (const { chatId, messageId } of notifications) {
await showNotification(accountId, chatId, messageId)
for (const { chatId, messageId, isWebxdcInfo } of notifications) {
await showNotification(
accountId,
chatId,
messageId,
isWebxdcInfo ?? false
)
}
nicodh marked this conversation as resolved.
Show resolved Hide resolved
}
notificationLimit = NORMAL_LIMIT
Expand Down Expand Up @@ -231,12 +316,3 @@ function getNotificationIcon(
return null
}
}

export function initNotifications() {
BackendRemote.on('IncomingMsg', (accountId, { chatId, msgId }) => {
incomingMessageHandler(accountId, chatId, msgId)
})
BackendRemote.on('IncomingMsgBunch', accountId => {
flushNotifications(accountId)
})
}
4 changes: 3 additions & 1 deletion packages/runtime/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import type { getLogger as getLoggerFunction } from '@deltachat-desktop/shared/l
import type { setLogHandler as setLogHandlerFunction } from '@deltachat-desktop/shared/logger.js'

/**
* Offers an abstraction Layer to make it easier to make browser client in the future
* Offers an abstraction Layer to make it easier to capsulate
* context specific functions (like electron, browser, tauri, etc)
*/
export interface Runtime {
emitUIFullyReady(): void
Expand Down Expand Up @@ -112,6 +113,7 @@ export interface Runtime {
showNotification(data: DcNotification): void
clearAllNotifications(): void
clearNotifications(chatId: number): void
// enables to set a callback (used in frontend RuntimeAdapter)
nicodh marked this conversation as resolved.
Show resolved Hide resolved
setNotificationCallback(
cb: (data: { accountId: number; chatId: number; msgId: number }) => void
): void
Expand Down
1 change: 1 addition & 0 deletions packages/shared/shared-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export interface DcNotification {
messageId: number
WofWca marked this conversation as resolved.
Show resolved Hide resolved
// for future
accountId: number
isWebxdcInfo: boolean
}

export interface DcOpenWebxdcParameters {
Expand Down
3 changes: 3 additions & 0 deletions packages/target-browser/runtime-browser/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ class BrowserRuntime implements Runtime {
accountId: number
chatId: number
msgId: number
isWebxdcInfo: boolean
WofWca marked this conversation as resolved.
Show resolved Hide resolved
}) => void = () => {
this.log.critical('notification click handler not initialized yet')
}
Expand All @@ -401,6 +402,7 @@ class BrowserRuntime implements Runtime {
title,
icon: notificationIcon,
WofWca marked this conversation as resolved.
Show resolved Hide resolved
messageId,
isWebxdcInfo,
} = data
this.log.debug('showNotification', { accountId, chatId, messageId })

Expand Down Expand Up @@ -447,6 +449,7 @@ class BrowserRuntime implements Runtime {
accountId,
chatId,
msgId: messageId,
isWebxdcInfo,
})

if (this.activeNotifications[chatId]) {
Expand Down
1 change: 1 addition & 0 deletions packages/target-electron/runtime-electron/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ class ElectronRuntime implements Runtime {
accountId: number
chatId: number
msgId: number
isWebxdcInfo: boolean
WofWca marked this conversation as resolved.
Show resolved Hide resolved
}) => void = () => {}
setNotificationCallback(
cb: (data: { accountId: number; chatId: number; msgId: number }) => void
Expand Down
5 changes: 4 additions & 1 deletion packages/target-electron/src/deltachat/webxdc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export default class DCWebxdc extends SplitOut {
) => {
const { webxdcInfo, chatName, displayname, accountId, href } = p
const addr = webxdcInfo.selfAddr
const { sendUpdateInterval, sendUpdateMaxSize } = webxdcInfo
let base64EncodedHref = ''
const appURL = `webxdc://${accountId}.${msg_id}.webxdc`
if (href && href !== '') {
Expand Down Expand Up @@ -222,7 +223,9 @@ export default class DCWebxdc extends SplitOut {
// initializes the preload script, the actual implementation of `window.webxdc` is found there: static/webxdc-preload.js
return makeResponse(
Buffer.from(
`window.parent.webxdc_internal.setup("${selfAddr}","${displayName}")
`window.parent.webxdc_internal.setup("${selfAddr}","${displayName}", ${Number(
sendUpdateInterval
)}, ${Number(sendUpdateMaxSize)})
WofWca marked this conversation as resolved.
Show resolved Hide resolved
window.webxdc = window.parent.webxdc
window.webxdc_custom = window.parent.webxdc_custom`
),
Expand Down
63 changes: 47 additions & 16 deletions packages/target-electron/src/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,39 @@ import { getLogger } from '../../shared/logger.js'

import type { NativeImage, IpcMainInvokeEvent } from 'electron'

/**
* Notification related functions to:
* - show notifications in operating system
* - handle click on notification
*
* is triggered from renderer process (!)
* by handling events (ipcMain.handle)
*
* see: frontend/src/system-integration/notifications.ts
*/

const log = getLogger('main/notifications')

const isMac = platform() === 'darwin'

if (Notification.isSupported()) {
ipcMain.handle('notifications.show', showNotification)
ipcMain.handle('notifications.clear', clearNotificationsForChat)
ipcMain.handle('notifications.clearAll', clearAll)
process.on('beforeExit', clearAll)
} else {
// Register no-op handlers for notifications to silently fail when
// no notifications are supported
ipcMain.handle('notifications.show', () => {})
ipcMain.handle('notifications.clear', () => {})
ipcMain.handle('notifications.clearAll', () => {})
}

function createNotification(data: DcNotification): Notification {
let icon: NativeImage | undefined = data.icon
? nativeImage.createFromPath(data.icon)
? data.icon.startsWith('data:')
? nativeImage.createFromDataURL(data.icon)
: nativeImage.createFromPath(data.icon)
: undefined

if (!icon || icon.isEmpty()) {
Expand Down Expand Up @@ -51,16 +77,28 @@ function onClickNotification(
accountId: number,
chatId: number,
msgId: number,
isWebxdcInfo: boolean,
_ev: Electron.Event
) {
mainWindow.send('ClickOnNotification', { accountId, chatId, msgId })
mainWindow.send('ClickOnNotification', {
accountId,
chatId,
msgId,
isWebxdcInfo,
})
mainWindow.show()
app.focus()
mainWindow.window?.focus()
}

const notifications: { [chatId: number]: Notification[] } = {}

/**
* triggers creation of a notification, adds appropriate
* callbacks and shows it via electron Notification API
*
* @param data is passed from renderer process
*/
function showNotification(_event: IpcMainInvokeEvent, data: DcNotification) {
const chatId = data.chatId

Expand All @@ -73,7 +111,13 @@ function showNotification(_event: IpcMainInvokeEvent, data: DcNotification) {
const notify = createNotification(data)

notify.on('click', Event => {
onClickNotification(data.accountId, chatId, data.messageId, Event)
onClickNotification(
WofWca marked this conversation as resolved.
Show resolved Hide resolved
data.accountId,
chatId,
data.messageId,
data.isWebxdcInfo,
Event
)
notifications[chatId] =
notifications[chatId]?.filter(n => n !== notify) || []
notify.close()
Expand Down Expand Up @@ -121,19 +165,6 @@ function clearAll() {
}
}

if (Notification.isSupported()) {
ipcMain.handle('notifications.show', showNotification)
ipcMain.handle('notifications.clear', clearNotificationsForChat)
ipcMain.handle('notifications.clearAll', clearAll)
process.on('beforeExit', clearAll)
} else {
// Register no-op handlers for notifications to silently fail when
// no notifications are supported
ipcMain.handle('notifications.show', () => {})
ipcMain.handle('notifications.clear', () => {})
ipcMain.handle('notifications.clearAll', () => {})
}

// Thanks to Signal for this function
// https://github.com/signalapp/Signal-Desktop/blob/ae9181a4b26264ce553c7d8379a3ee5a07de018b/ts/services/notifications.ts#L485
// it is licensed AGPL-3.0-only
Expand Down
Loading
Loading