From e7cf7a81e9fb4d32f6442df2875d897f68768035 Mon Sep 17 00:00:00 2001 From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:35:12 -0400 Subject: [PATCH] [MM-57348] Support notification metrics from the Desktop App client (#2998) * [MM-57348] Support notification metrics from the Desktop App client * Add timeout in case promise never resolves --- .eslintignore | 3 +- api-types/index.ts | 2 +- api-types/lib/index.d.ts | 14 ++++--- api-types/lib/index.js | 7 +--- api-types/package.json | 2 +- api-types/tsconfig.json | 3 +- src/main/app/initialize.ts | 2 +- src/main/app/intercom.ts | 4 +- src/main/notifications/index.ts | 65 +++++++++++++++++++-------------- src/main/preload/externalAPI.ts | 4 +- 10 files changed, 59 insertions(+), 47 deletions(-) diff --git a/.eslintignore b/.eslintignore index 76add878..ae9050f6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ node_modules -dist \ No newline at end of file +dist +api-types/lib \ No newline at end of file diff --git a/api-types/index.ts b/api-types/index.ts index 6918bd70..d96b710a 100644 --- a/api-types/index.ts +++ b/api-types/index.ts @@ -28,7 +28,7 @@ export type DesktopAPI = { ) => void) => () => void; // Unreads/mentions/notifications - sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => void; + sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => Promise<{result: string; reason?: string; data?: string}>; onNotificationClicked: (listener: (channelId: string, teamId: string, url: string) => void) => () => void; setUnreadsAndMentions: (isUnread: boolean, mentionCount: number) => void; diff --git a/api-types/lib/index.d.ts b/api-types/lib/index.d.ts index a92e2b2c..f3e671b0 100644 --- a/api-types/lib/index.d.ts +++ b/api-types/lib/index.d.ts @@ -1,6 +1,4 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -export declare type DesktopSourcesOptions = { +export type DesktopSourcesOptions = { types: Array<'screen' | 'window'>; thumbnailSize?: { height: number; @@ -8,12 +6,12 @@ export declare type DesktopSourcesOptions = { }; fetchWindowIcons?: boolean; }; -export declare type DesktopCaptureSource = { +export type DesktopCaptureSource = { id: string; name: string; thumbnailURL: string; }; -export declare type DesktopAPI = { +export type DesktopAPI = { isDev: () => Promise; getAppInfo: () => Promise<{ name: string; @@ -22,7 +20,11 @@ export declare type DesktopAPI = { reactAppInitialized: () => void; setSessionExpired: (isExpired: boolean) => void; onUserActivityUpdate: (listener: (userIsActive: boolean, idleTime: number, isSystemEvent: boolean) => void) => () => void; - sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => void; + sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => Promise<{ + result: string; + reason?: string; + data?: string; + }>; onNotificationClicked: (listener: (channelId: string, teamId: string, url: string) => void) => () => void; setUnreadsAndMentions: (isUnread: boolean, mentionCount: number) => void; requestBrowserHistoryStatus: () => Promise<{ diff --git a/api-types/lib/index.js b/api-types/lib/index.js index 351eb9fc..96ab4ebb 100644 --- a/api-types/lib/index.js +++ b/api-types/lib/index.js @@ -1,7 +1,4 @@ +"use strict"; // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -'use strict'; - -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. -Object.defineProperty(exports, '__esModule', {value: true}); +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/api-types/package.json b/api-types/package.json index d274c492..4ea5101c 100644 --- a/api-types/package.json +++ b/api-types/package.json @@ -1,6 +1,6 @@ { "name": "@mattermost/desktop-api", - "version": "5.8.0-1", + "version": "5.8.0-3", "description": "Shared types for the Desktop App API provided to the Web App", "keywords": [ "mattermost" diff --git a/api-types/tsconfig.json b/api-types/tsconfig.json index cbfd2e39..a14e8517 100644 --- a/api-types/tsconfig.json +++ b/api-types/tsconfig.json @@ -12,7 +12,8 @@ "jsx": "react", "outDir": "./lib", "rootDir": ".", - "composite": true + "composite": true, + "types": [] }, "include": [ "./index.ts" diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index ce6720a9..96716604 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -248,7 +248,7 @@ function initializeBeforeAppReady() { } function initializeInterCommunicationEventListeners() { - ipcMain.on(NOTIFY_MENTION, handleMentionNotification); + ipcMain.handle(NOTIFY_MENTION, handleMentionNotification); ipcMain.handle(GET_APP_INFO, handleAppVersion); ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent); ipcMain.on(FOCUS_BROWSERVIEW, ViewManager.focusCurrentView); diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index ffdb5fc6..d07cd101 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -112,9 +112,9 @@ export function handleWelcomeScreenModal() { } } -export function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) { +export function handleMentionNotification(event: IpcMainInvokeEvent, title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) { log.debug('handleMentionNotification', {title, body, channelId, teamId, url, silent, soundName}); - NotificationManager.displayMention(title, body, channelId, teamId, url, silent, event.sender, soundName); + return NotificationManager.displayMention(title, body, channelId, teamId, url, silent, event.sender, soundName); } export function handleOpenAppMenu() { diff --git a/src/main/notifications/index.ts b/src/main/notifications/index.ts index 093dcdf2..0ce5aefa 100644 --- a/src/main/notifications/index.ts +++ b/src/main/notifications/index.ts @@ -32,19 +32,19 @@ class NotificationManager { if (!Notification.isSupported()) { log.error('notification not supported'); - return; + return {result: 'error', reason: 'notification_api', data: 'notification not supported'}; } if (await getDoNotDisturb()) { - return; + return {result: 'not_sent', reason: 'os_dnd'}; } const view = ViewManager.getViewByWebContentsId(webcontents.id); if (!view) { - return; + return {result: 'error', reason: 'missing_view'}; } if (!view.view.shouldNotify) { - return; + return {result: 'error', reason: 'view_should_not_notify'}; } const serverName = view.view.server.name; @@ -56,32 +56,13 @@ class NotificationManager { }; if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) { - return; + return {result: 'not_sent', reason: 'notifications_permission_disallowed'}; } const mention = new Mention(options, channelId, teamId); const mentionKey = `${mention.teamId}:${mention.channelId}`; this.allActiveNotifications.set(mention.uId, mention); - mention.on('show', () => { - log.debug('displayMention.show'); - - // On Windows, manually dismiss notifications from the same channel and only show the latest one - if (process.platform === 'win32') { - if (this.mentionsPerChannel.has(mentionKey)) { - log.debug(`close ${mentionKey}`); - this.mentionsPerChannel.get(mentionKey)?.close(); - this.mentionsPerChannel.delete(mentionKey); - } - this.mentionsPerChannel.set(mentionKey, mention); - } - const notificationSound = mention.getNotificationSound(); - if (notificationSound) { - MainWindow.sendToRenderer(PLAY_SOUND, notificationSound); - } - flashFrame(true); - }); - mention.on('click', () => { log.debug('notification click', serverName, mention); @@ -97,10 +78,40 @@ class NotificationManager { this.allActiveNotifications.delete(mention.uId); }); - mention.on('failed', () => { - this.allActiveNotifications.delete(mention.uId); + return new Promise((resolve) => { + // If mention never shows somehow, resolve the promise after 10s + const timeout = setTimeout(() => { + resolve({result: 'error', reason: 'notification_timeout'}); + }, 10000); + + mention.on('show', () => { + log.debug('displayMention.show'); + + // On Windows, manually dismiss notifications from the same channel and only show the latest one + if (process.platform === 'win32') { + if (this.mentionsPerChannel.has(mentionKey)) { + log.debug(`close ${mentionKey}`); + this.mentionsPerChannel.get(mentionKey)?.close(); + this.mentionsPerChannel.delete(mentionKey); + } + this.mentionsPerChannel.set(mentionKey, mention); + } + const notificationSound = mention.getNotificationSound(); + if (notificationSound) { + MainWindow.sendToRenderer(PLAY_SOUND, notificationSound); + } + flashFrame(true); + clearTimeout(timeout); + resolve({result: 'success'}); + }); + + mention.on('failed', (_, error) => { + this.allActiveNotifications.delete(mention.uId); + clearTimeout(timeout); + resolve({result: 'error', reason: 'electron_notification_failed', data: error}); + }); + mention.show(); }); - mention.show(); } public async displayDownloadCompleted(fileName: string, path: string, serverName: string) { diff --git a/src/main/preload/externalAPI.ts b/src/main/preload/externalAPI.ts index ada74db5..5d7e69de 100644 --- a/src/main/preload/externalAPI.ts +++ b/src/main/preload/externalAPI.ts @@ -72,7 +72,7 @@ const desktopAPI: DesktopAPI = { // Unreads/mentions/notifications sendNotification: (title, body, channelId, teamId, url, silent, soundName) => - ipcRenderer.send(NOTIFY_MENTION, title, body, channelId, teamId, url, silent, soundName), + ipcRenderer.invoke(NOTIFY_MENTION, title, body, channelId, teamId, url, silent, soundName), onNotificationClicked: (listener) => createListener(NOTIFICATION_CLICKED, listener), setUnreadsAndMentions: (isUnread, mentionCount) => ipcRenderer.send(UNREADS_AND_MENTIONS, isUnread, mentionCount), @@ -286,7 +286,7 @@ window.addEventListener('message', ({origin, data = {}}: {origin?: string; data? case 'dispatch-notification': { const {title, body, channel, teamId, url, silent, data: messageData} = message; channels.set(channel.id, channel); - ipcRenderer.send(NOTIFY_MENTION, title, body, channel.id, teamId, url, silent, messageData.soundName); + ipcRenderer.invoke(NOTIFY_MENTION, title, body, channel.id, teamId, url, silent, messageData.soundName); break; } case BROWSER_HISTORY_PUSH: {