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
115 changes: 99 additions & 16 deletions packages/frontend/src/system-integration/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,38 @@ 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, false)
})
BackendRemote.on('IncomingWebxdcNotify', (accountId, { msgId, text }) => {
// we don't have the chatId yet, but it will be fetched in flushNotifications
incomingMessageHandler(accountId, -1, msgId, true, text)
})
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
eventText?: string
}

let queuedNotifications: {
Expand All @@ -26,7 +51,9 @@ let queuedNotifications: {
function incomingMessageHandler(
accountId: number,
chatId: number,
messageId: number
messageId: number,
isWebxdcInfo: boolean,
eventText?: string
) {
log.debug('incomingMessageHandler: ', { chatId, messageId })

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

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

Expand All @@ -81,11 +115,50 @@ async function showNotification(
try {
const notificationInfo =
await BackendRemote.rpc.getMessageNotificationInfo(accountId, messageId)
const { chatName, summaryPrefix, summaryText } = notificationInfo
let summaryPrefix = notificationInfo.summaryPrefix
const summaryText = eventText ?? notificationInfo.summaryText
const chatName = notificationInfo.chatName
let icon = getNotificationIcon(notificationInfo)
if (isWebxdcInfo) {
/**
* messageId may refer to a webxdc message OR a wexdc-info-message!
*
* a notification might be sent even when no webxdc-info-message was
* added to the chat; in that case the msg_id refers to the webxdc instance
*/
let message = await BackendRemote.rpc.getMessage(accountId, messageId)
if (
message.systemMessageType === 'WebxdcInfoMessage' &&
message.parentId
) {
// we have to get the parent message
// (the webxdc message which holds the webxdcInfo)
message = await BackendRemote.rpc.getMessage(
accountId,
message.parentId
)
}
if (message.webxdcInfo) {
summaryPrefix = `${message.webxdcInfo.name}`
nicodh marked this conversation as resolved.
Show resolved Hide resolved
if (message.webxdcInfo.icon) {
const iconName = message.webxdcInfo.icon
const iconBlob = await BackendRemote.rpc.getWebxdcBlob(
accountId,
message.id,
iconName
)
// needed for valid dataUrl
const imageExtension = iconName.split('.').pop()
icon = `data:image/${imageExtension};base64,${iconBlob}`
}
} else {
throw new Error(`no webxdcInfo in message with id ${message.id}`)
}
}
runtime.showNotification({
title: chatName,
body: summaryPrefix ? `${summaryPrefix}: ${summaryText}` : summaryText,
icon: getNotificationIcon(notificationInfo),
icon,
chatId,
messageId,
accountId,
Expand Down Expand Up @@ -174,6 +247,14 @@ async function flushNotifications(accountId: number) {
let notifications = [...queuedNotifications[accountId]]
queuedNotifications = []

for await (const n of notifications) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for await is no better than a regular for of here, right?

if (n.chatId === -1) {
// get real chatId of the webxdc message
const message = await BackendRemote.rpc.getMessage(accountId, n.messageId)
n.chatId = message.chatId
}
}

// filter out muted chats:
const uniqueChats = [...new Set(notifications.map(n => n.chatId))]
const mutedChats = (
Expand Down Expand Up @@ -202,8 +283,19 @@ 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,
eventText,
} of notifications) {
await showNotification(
accountId,
chatId,
messageId,
isWebxdcInfo,
eventText
)
}
}
notificationLimit = NORMAL_LIMIT
Expand Down Expand Up @@ -231,12 +323,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) */
setNotificationCallback(
cb: (data: { accountId: number; chatId: number; msgId: number }) => void
): void
Expand Down
5 changes: 4 additions & 1 deletion packages/shared/shared-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ export interface BuildInfo {
export interface DcNotification {
title: string
body: string
/** path to image that should be shown instead of icon */
/**
* path to image that should be shown instead of icon
* (or a data url with base64 encoded data)
*/
icon: string | null
chatId: number
messageId: number
WofWca marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
6 changes: 5 additions & 1 deletion packages/target-browser/runtime-browser/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,11 @@ class BrowserRuntime implements Runtime {
// IDEA: alternatively we could make another route that exposes the file with a random hash without authentification?
// Concern: Also the current method could run into size limits because it loads the whole image, which can be large? like high ram usage in browser?
try {
const response = await fetch(this.transformBlobURL(notificationIcon))
const response = await fetch(
notificationIcon.startsWith('data:')
? notificationIcon
: this.transformBlobURL(notificationIcon)
)
if (!response.ok) {
throw new Error('request failed: code' + response.status)
}
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
53 changes: 38 additions & 15 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 @@ -53,14 +79,24 @@ function onClickNotification(
msgId: number,
_ev: Electron.Event
) {
mainWindow.send('ClickOnNotification', { accountId, chatId, msgId })
mainWindow.send('ClickOnNotification', {
accountId,
chatId,
msgId,
})
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 Down Expand Up @@ -121,19 +157,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
4 changes: 3 additions & 1 deletion packages/target-electron/static/webxdc-preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,14 @@ class RealtimeListener {
const connections = []

contextBridge.exposeInMainWorld('webxdc_internal', {
setup: (selfAddr, selfName) => {
setup: (selfAddr, selfName, sendUpdateInterval, sendUpdateMaxSize) => {
if (is_ready) {
return
}
api.selfAddr = Buffer.from(selfAddr, 'base64').toString('utf-8')
api.selfName = Buffer.from(selfName, 'base64').toString('utf-8')
api.sendUpdateInterval = sendUpdateInterval
api.sendUpdateMaxSize = sendUpdateMaxSize

// be sure that webxdc.js was included
contextBridge.exposeInMainWorld('webxdc', api)
Expand Down
Loading