From 9aec7db82163153ea028776de26a8b401096cc2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 10 Oct 2023 16:13:07 +0200 Subject: [PATCH] Fix notifications not working (#2865) * Fix notifications not working * Transform into a manager * Remove test accessor --- package.json | 3 +- src/main/app/intercom.ts | 4 +- ...utoUpdater.test.js => autoUpdater.test.ts} | 32 ++- src/main/autoUpdater.ts | 6 +- src/main/downloadsManager.ts | 4 +- src/main/notifications/Download.ts | 6 + src/main/notifications/Mention.ts | 4 + .../{index.test.js => index.test.ts} | 165 +++++++----- src/main/notifications/index.ts | 244 ++++++++++-------- 9 files changed, 269 insertions(+), 199 deletions(-) rename src/main/{autoUpdater.test.js => autoUpdater.test.ts} (86%) rename src/main/notifications/{index.test.js => index.test.ts} (69%) diff --git a/package.json b/package.json index 2119ecb5..cd438a43 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "src/main/**/*.ts" ], "testMatch": [ - "**/src/**/*.test.js" + "**/src/**/*.test.js", + "**/src/**/*.test.ts" ], "testPathIgnorePatterns": [ "/node_modules/", diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index 0c09f404..611dad90 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -12,7 +12,7 @@ import {Logger} from 'common/log'; import ServerManager from 'common/servers/serverManager'; import {ping} from 'common/utils/requests'; -import {displayMention} from 'main/notifications'; +import NotificationManager from 'main/notifications'; import {getLocalPreload, getLocalURLString} from 'main/utils'; import ModalManager from 'main/views/modalManager'; import MainWindow from 'main/windows/mainWindow'; @@ -116,7 +116,7 @@ export function handleWelcomeScreenModal() { export function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, data: MentionData) { log.debug('handleMentionNotification', {title, body, channel, teamId, url, silent, data}); - displayMention(title, body, channel, teamId, url, silent, event.sender, data); + NotificationManager.displayMention(title, body, channel, teamId, url, silent, event.sender, data); } export function handleOpenAppMenu() { diff --git a/src/main/autoUpdater.test.js b/src/main/autoUpdater.test.ts similarity index 86% rename from src/main/autoUpdater.test.js rename to src/main/autoUpdater.test.ts index fc883ce3..04207b35 100644 --- a/src/main/autoUpdater.test.js +++ b/src/main/autoUpdater.test.ts @@ -1,13 +1,17 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {ipcMain} from 'electron'; -import {autoUpdater} from 'electron-updater'; +import {ipcMain as notMockedIpcMain} from 'electron'; +import {autoUpdater as notMockedAutoUpdater} from 'electron-updater'; import {CHECK_FOR_UPDATES} from 'common/communication'; +import NotificationManager from 'main/notifications'; + import {UpdateManager} from './autoUpdater'; -import {displayRestartToUpgrade, displayUpgrade} from './notifications'; + +const autoUpdater = jest.mocked(notMockedAutoUpdater); +const ipcMain = jest.mocked(notMockedIpcMain); jest.mock('electron', () => ({ app: { @@ -44,6 +48,7 @@ jest.mock('main/notifications', () => ({ displayUpgrade: jest.fn(), displayRestartToUpgrade: jest.fn(), })); + jest.mock('main/windows/mainWindow', () => ({ sendToRenderer: jest.fn(), })); @@ -55,6 +60,7 @@ jest.mock('main/i18nManager', () => ({ jest.mock('main/downloadsManager', () => ({ removeUpdateBeforeRestart: jest.fn(), })); + describe('main/autoUpdater', () => { describe('constructor', () => { afterEach(() => { @@ -62,11 +68,12 @@ describe('main/autoUpdater', () => { }); it('should notify user on update-available', () => { - let cb; + let cb: any; autoUpdater.on.mockImplementation((event, callback) => { if (event === 'update-available') { cb = callback; } + return autoUpdater; }); const updateManager = new UpdateManager(); @@ -78,11 +85,12 @@ describe('main/autoUpdater', () => { }); it('should notify user on update-downloaded', () => { - let cb; + let cb: any; autoUpdater.on.mockImplementation((event, callback) => { if (event === 'update-downloaded') { cb = callback; } + return autoUpdater; }); const updateManager = new UpdateManager(); @@ -94,11 +102,12 @@ describe('main/autoUpdater', () => { }); it('should check for updates when emitted', () => { - let cb; + let cb: any; ipcMain.on.mockImplementation((event, callback) => { if (event === CHECK_FOR_UPDATES) { cb = callback; } + return ipcMain; }); const updateManager = new UpdateManager(); @@ -133,7 +142,7 @@ describe('main/autoUpdater', () => { updateManager.versionAvailable = '5.1.0'; updateManager.notify(); updateManager.notify = jest.fn(); - expect(displayUpgrade).toHaveBeenCalledWith('5.1.0', expect.any(Function)); + expect(NotificationManager.displayUpgrade).toHaveBeenCalledWith('5.1.0', expect.any(Function)); }); it('should display downloaded upgrade notification', () => { @@ -141,13 +150,13 @@ describe('main/autoUpdater', () => { updateManager.versionDownloaded = '5.1.0'; updateManager.notify(); updateManager.notify = jest.fn(); - expect(displayRestartToUpgrade).toHaveBeenCalledWith('5.1.0', expect.any(Function)); + expect(NotificationManager.displayRestartToUpgrade).toHaveBeenCalledWith('5.1.0', expect.any(Function)); }); }); describe('checkForUpdates', () => { beforeEach(() => { - autoUpdater.checkForUpdates.mockReturnValue(Promise.resolve()); + autoUpdater.checkForUpdates.mockReturnValue(Promise.resolve(null)); jest.useFakeTimers(); }); @@ -164,8 +173,9 @@ describe('main/autoUpdater', () => { it('should show dialog if update is not available', () => { autoUpdater.once.mockImplementation((event, callback) => { if (event === 'update-not-available') { - callback(); + (callback as any)(); } + return autoUpdater; }); const updateManager = new UpdateManager(); @@ -177,7 +187,7 @@ describe('main/autoUpdater', () => { it('should check again at the next interval', () => { const updateManager = new UpdateManager(); - updateManager.checkForUpdates(); + updateManager.checkForUpdates(false); updateManager.checkForUpdates = jest.fn(); jest.runAllTimers(); expect(updateManager.checkForUpdates).toBeCalled(); diff --git a/src/main/autoUpdater.ts b/src/main/autoUpdater.ts index 5a9d397d..967848f4 100644 --- a/src/main/autoUpdater.ts +++ b/src/main/autoUpdater.ts @@ -10,7 +10,7 @@ import {Logger} from 'common/log'; import downloadsManager from 'main/downloadsManager'; import {localizeMessage} from 'main/i18nManager'; -import {displayUpgrade, displayRestartToUpgrade} from 'main/notifications'; +import NotificationManager from 'main/notifications'; import { CANCEL_UPGRADE, @@ -113,12 +113,12 @@ export class UpdateManager { notifyUpgrade = (): void => { ipcMain.emit(UPDATE_AVAILABLE, null, this.versionAvailable); - displayUpgrade(this.versionAvailable || 'unknown', this.handleDownload); + NotificationManager.displayUpgrade(this.versionAvailable || 'unknown', this.handleDownload); } notifyDownloaded = (): void => { ipcMain.emit(UPDATE_DOWNLOADED, null, this.downloadedInfo); - displayRestartToUpgrade(this.versionDownloaded || 'unknown', this.handleUpdate); + NotificationManager.displayRestartToUpgrade(this.versionDownloaded || 'unknown', this.handleUpdate); } handleDownload = (): void => { diff --git a/src/main/downloadsManager.ts b/src/main/downloadsManager.ts index dfbb336b..0467b775 100644 --- a/src/main/downloadsManager.ts +++ b/src/main/downloadsManager.ts @@ -31,7 +31,7 @@ import {APP_UPDATE_KEY, UPDATE_DOWNLOAD_ITEM} from 'common/constants'; import {DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT, DOWNLOADS_DROPDOWN_MAX_ITEMS} from 'common/utils/constants'; import * as Validator from 'common/Validator'; import {localizeMessage} from 'main/i18nManager'; -import {displayDownloadCompleted} from 'main/notifications'; +import NotificationManager from 'main/notifications'; import ViewManager from 'main/views/viewManager'; import MainWindow from 'main/windows/mainWindow'; import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils'; @@ -559,7 +559,7 @@ export class DownloadsManager extends JsonFileManager { log.debug('doneEventController', {state}); if (state === 'completed' && !this.open) { - displayDownloadCompleted(path.basename(item.savePath), item.savePath, ViewManager.getViewByWebContentsId(webContents.id)?.view.server.name ?? ''); + NotificationManager.displayDownloadCompleted(path.basename(item.savePath), item.savePath, ViewManager.getViewByWebContentsId(webContents.id)?.view.server.name ?? ''); } const bookmark = this.bookmarks.get(this.getFileId(item)); diff --git a/src/main/notifications/Download.ts b/src/main/notifications/Download.ts index fbd6fa9f..0fd53dff 100644 --- a/src/main/notifications/Download.ts +++ b/src/main/notifications/Download.ts @@ -4,6 +4,8 @@ import os from 'os'; import path from 'path'; +import {v4 as uuid} from 'uuid'; + import {app, Notification} from 'electron'; import Utils from 'common/utils/util'; @@ -22,6 +24,8 @@ const defaultOptions = { }; export class DownloadNotification extends Notification { + uId: string; + constructor(fileName: string, serverName: string) { const options = {...defaultOptions}; if (process.platform === 'darwin' || (process.platform === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '10.0'))) { @@ -33,5 +37,7 @@ export class DownloadNotification extends Notification { options.body = process.platform === 'win32' ? localizeMessage('main.notifications.download.complete.body', 'Download Complete \n {fileName}', {fileName}) : fileName; super(options); + + this.uId = uuid(); } } diff --git a/src/main/notifications/Mention.ts b/src/main/notifications/Mention.ts index 8dd01019..378946b9 100644 --- a/src/main/notifications/Mention.ts +++ b/src/main/notifications/Mention.ts @@ -4,6 +4,8 @@ import os from 'os'; import path from 'path'; +import {v4 as uuid} from 'uuid'; + import {app, Notification} from 'electron'; import {MentionOptions} from 'types/notification'; @@ -27,6 +29,7 @@ export class Mention extends Notification { customSound: string; channel: {id: string}; // TODO: Channel from mattermost-redux teamId: string; + uId: string; constructor(customOptions: MentionOptions, channel: {id: string}, teamId: string) { const options = {...defaultOptions, ...customOptions}; @@ -44,6 +47,7 @@ export class Mention extends Notification { this.customSound = customSound; this.channel = channel; this.teamId = teamId; + this.uId = uuid(); } getNotificationSound = () => { diff --git a/src/main/notifications/index.test.js b/src/main/notifications/index.test.ts similarity index 69% rename from src/main/notifications/index.test.js rename to src/main/notifications/index.test.ts index d004eeba..1741fc6c 100644 --- a/src/main/notifications/index.test.js +++ b/src/main/notifications/index.test.ts @@ -2,26 +2,35 @@ // See LICENSE.txt for license information. 'use strict'; -import cp from 'child_process'; +import notMockedCP from 'child_process'; -import {Notification, shell, app} from 'electron'; +import {Notification as NotMockedNotification, shell, app, BrowserWindow, WebContents} from 'electron'; -import {getFocusAssist} from 'windows-focus-assist'; -import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state'; +import {getFocusAssist as notMockedGetFocusAssist} from 'windows-focus-assist'; +import {getDoNotDisturb as notMockedGetDarwinDoNotDisturb} from 'macos-notification-state'; import {PLAY_SOUND} from 'common/communication'; -import Config from 'common/config'; +import notMockedConfig from 'common/config'; -import {localizeMessage} from 'main/i18nManager'; -import PermissionsManager from 'main/permissionsManager'; -import MainWindow from 'main/windows/mainWindow'; +import {localizeMessage as notMockedLocalizeMessage} from 'main/i18nManager'; +import notMockedPermissionsManager from 'main/permissionsManager'; +import notMockedMainWindow from 'main/windows/mainWindow'; import ViewManager from 'main/views/viewManager'; import getLinuxDoNotDisturb from './dnd-linux'; -import {displayMention, displayDownloadCompleted, currentNotifications} from './index'; +import NotificationManager from './index'; -const mentions = []; +const Notification = jest.mocked(NotMockedNotification); +const getFocusAssist = jest.mocked(notMockedGetFocusAssist); +const PermissionsManager = jest.mocked(notMockedPermissionsManager); +const getDarwinDoNotDisturb = jest.mocked(notMockedGetDarwinDoNotDisturb); +const Config = jest.mocked(notMockedConfig); +const MainWindow = jest.mocked(notMockedMainWindow); +const localizeMessage = jest.mocked(notMockedLocalizeMessage); +const cp = jest.mocked(notMockedCP); + +const mentions: Array<{body: string; value: any}> = []; jest.mock('child_process', () => ({ execSync: jest.fn(), @@ -29,25 +38,26 @@ jest.mock('child_process', () => ({ jest.mock('electron', () => { class NotificationMock { + callbackMap: Map void>; static isSupported = jest.fn(); static didConstruct = jest.fn(); - constructor(options) { + constructor(options: any) { NotificationMock.didConstruct(); this.callbackMap = new Map(); mentions.push({body: options.body, value: this}); } - on = (event, callback) => { + on = (event: string, callback: () => void) => { this.callbackMap.set(event, callback); } show = jest.fn().mockImplementation(() => { - this.callbackMap.get('show')(); + this.callbackMap.get('show')?.(); }); click = jest.fn().mockImplementation(() => { - this.callbackMap.get('click')(); + this.callbackMap.get('click')?.(); }); close = jest.fn(); @@ -105,35 +115,43 @@ describe('main/notifications', () => { describe('displayMention', () => { const mainWindow = { flashFrame: jest.fn(), - }; + } as unknown as BrowserWindow; beforeEach(() => { PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(true)); Notification.isSupported.mockImplementation(() => true); - getFocusAssist.mockReturnValue({value: false}); + getFocusAssist.mockReturnValue({value: 0, name: ''}); getDarwinDoNotDisturb.mockReturnValue(false); - Config.notifications = {}; + Config.notifications = { + flashWindow: 0, + bounceIcon: false, + bounceIconType: 'informational', + }; MainWindow.get.mockReturnValue(mainWindow); }); afterEach(() => { jest.resetAllMocks(); - Config.notifications = {}; + Config.notifications = { + flashWindow: 0, + bounceIcon: false, + bounceIconType: 'informational', + }; }); it('should do nothing when Notification is not supported', async () => { Notification.isSupported.mockImplementation(() => false); - await displayMention( + await NotificationManager.displayMention( 'test', 'test body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1}, - {}, + {id: 1} as WebContents, + {soundName: ''}, ); - expect(Notification.didConstruct).not.toBeCalled(); + expect(MainWindow.show).not.toBeCalled(); }); it('should do nothing when alarms only is enabled on windows', async () => { @@ -142,18 +160,18 @@ describe('main/notifications', () => { value: 'win32', }); - getFocusAssist.mockReturnValue({value: 2}); - await displayMention( + getFocusAssist.mockReturnValue({value: 2, name: ''}); + await NotificationManager.displayMention( 'test', 'test body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1}, - {}, + {id: 1} as WebContents, + {soundName: ''}, ); - expect(Notification.didConstruct).not.toBeCalled(); + expect(MainWindow.show).not.toBeCalled(); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -167,17 +185,17 @@ describe('main/notifications', () => { }); getDarwinDoNotDisturb.mockReturnValue(true); - await displayMention( + await NotificationManager.displayMention( 'test', 'test body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1}, - {}, + {id: 1} as WebContents, + {soundName: ''}, ); - expect(Notification.didConstruct).not.toBeCalled(); + expect(MainWindow.show).not.toBeCalled(); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -186,28 +204,28 @@ describe('main/notifications', () => { it('should do nothing when the permission check fails', async () => { PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(false)); - await displayMention( + await NotificationManager.displayMention( 'test', 'test body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1}, - {}, + {id: 1} as WebContents, + {soundName: ''}, ); - expect(Notification.didConstruct).not.toBeCalled(); + expect(MainWindow.show).not.toBeCalled(); }); it('should play notification sound when custom sound is provided', async () => { - await displayMention( + await NotificationManager.displayMention( 'test', 'test body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1}, + {id: 1} as WebContents, {soundName: 'test_sound'}, ); expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound'); @@ -219,34 +237,36 @@ describe('main/notifications', () => { value: 'win32', }); - await displayMention( + await NotificationManager.displayMention( 'test', 'test body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1}, - {}, + {id: 1} as WebContents, + {soundName: ''}, ); - expect(currentNotifications.has('team_id:channel_id')).toBe(true); + // convert to any to access private field + const mentionsPerChannel = (NotificationManager as any).mentionsPerChannel; + expect(mentionsPerChannel.has('team_id:channel_id')).toBe(true); - const existingMention = currentNotifications.get('team_id:channel_id'); - currentNotifications.delete = jest.fn(); - await displayMention( + const existingMention = mentionsPerChannel.get('team_id:channel_id'); + mentionsPerChannel.delete = jest.fn(); + await NotificationManager.displayMention( 'test', 'test body 2', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1}, - {}, + {id: 1} as WebContents, + {soundName: ''}, ); - expect(currentNotifications.delete).toHaveBeenCalled(); - expect(existingMention.close).toHaveBeenCalled(); + expect(mentionsPerChannel.delete).toHaveBeenCalled(); + expect(existingMention?.close).toHaveBeenCalled(); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -254,18 +274,18 @@ describe('main/notifications', () => { }); it('should switch view when clicking on notification', async () => { - await displayMention( + await NotificationManager.displayMention( 'click_test', 'mention_click_body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1, send: jest.fn()}, - {}, + {id: 1, send: jest.fn()} as unknown as WebContents, + {soundName: ''}, ); const mention = mentions.find((m) => m.body === 'mention_click_body'); - mention.value.click(); + mention?.value.click(); expect(MainWindow.show).toHaveBeenCalled(); expect(ViewManager.showById).toHaveBeenCalledWith('server_id'); }); @@ -275,15 +295,15 @@ describe('main/notifications', () => { Object.defineProperty(process, 'platform', { value: 'linux', }); - await displayMention( + await NotificationManager.displayMention( 'click_test', 'mention_click_body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1, send: jest.fn()}, - {}, + {id: 1, send: jest.fn()} as unknown as WebContents, + {soundName: ''}, ); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -293,21 +313,23 @@ describe('main/notifications', () => { it('linux/windows - should flash frame when config item is set', async () => { Config.notifications = { - flashWindow: true, + flashWindow: 1, + bounceIcon: false, + bounceIconType: 'informational', }; const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'linux', }); - await displayMention( + await NotificationManager.displayMention( 'click_test', 'mention_click_body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1, send: jest.fn()}, - {}, + {id: 1, send: jest.fn()} as unknown as WebContents, + {soundName: ''}, ); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -320,15 +342,15 @@ describe('main/notifications', () => { Object.defineProperty(process, 'platform', { value: 'darwin', }); - await displayMention( + await NotificationManager.displayMention( 'click_test', 'mention_click_body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1, send: jest.fn()}, - {}, + {id: 1, send: jest.fn()} as unknown as WebContents, + {soundName: ''}, ); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -340,20 +362,21 @@ describe('main/notifications', () => { Config.notifications = { bounceIcon: true, bounceIconType: 'critical', + flashWindow: 0, }; const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'darwin', }); - await displayMention( + await NotificationManager.displayMention( 'click_test', 'mention_click_body', {id: 'channel_id'}, 'team_id', 'http://server-1.com/team_id/channel_id', false, - {id: 1, send: jest.fn()}, - {}, + {id: 1, send: jest.fn()} as unknown as WebContents, + {soundName: ''}, ); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -365,26 +388,26 @@ describe('main/notifications', () => { describe('displayDownloadCompleted', () => { beforeEach(() => { Notification.isSupported.mockImplementation(() => true); - getFocusAssist.mockReturnValue({value: false}); + getFocusAssist.mockReturnValue({value: 0, name: ''}); getDarwinDoNotDisturb.mockReturnValue(false); }); it('should open file when clicked', () => { getDarwinDoNotDisturb.mockReturnValue(false); localizeMessage.mockReturnValue('test_filename'); - displayDownloadCompleted( + NotificationManager.displayDownloadCompleted( 'test_filename', '/path/to/file', 'server_name', ); const mention = mentions.find((m) => m.body.includes('test_filename')); - mention.value.click(); + mention?.value.click(); expect(shell.showItemInFolder).toHaveBeenCalledWith('/path/to/file'); }); }); describe('getLinuxDoNotDisturb', () => { - let originalPlatform; + let originalPlatform: NodeJS.Platform; beforeAll(() => { originalPlatform = process.platform; Object.defineProperty(process, 'platform', { @@ -399,7 +422,7 @@ describe('main/notifications', () => { }); it('should return false', () => { - cp.execSync.mockReturnValue('true'); + cp.execSync.mockReturnValue(Buffer.from('true')); expect(getLinuxDoNotDisturb()).toBe(false); }); @@ -411,7 +434,7 @@ describe('main/notifications', () => { }); it('should return true', () => { - cp.execSync.mockReturnValue('false'); + cp.execSync.mockReturnValue(Buffer.from('false')); expect(getLinuxDoNotDisturb()).toBe(true); }); }); diff --git a/src/main/notifications/index.ts b/src/main/notifications/index.ts index 8b27db2f..8c4c6666 100644 --- a/src/main/notifications/index.ts +++ b/src/main/notifications/index.ts @@ -21,134 +21,157 @@ import {NewVersionNotification, UpgradeNotification} from './Upgrade'; import getLinuxDoNotDisturb from './dnd-linux'; import getWindowsDoNotDisturb from './dnd-windows'; -export const currentNotifications = new Map(); - const log = new Logger('Notifications'); -export async function displayMention(title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, data: MentionData) { - log.debug('displayMention', {title, body, channel, teamId, url, silent, data}); +class NotificationManager { + private mentionsPerChannel: Map = new Map(); + private allActiveNotifications: Map = new Map(); + private upgradeNotification?: NewVersionNotification; + private restartToUpgradeNotification?: UpgradeNotification; - if (!Notification.isSupported()) { - log.error('notification not supported'); - return; - } + public async displayMention(title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, data: MentionData) { + log.debug('displayMention', {title, body, channel, teamId, url, silent, data}); - if (getDoNotDisturb()) { - return; - } + if (!Notification.isSupported()) { + log.error('notification not supported'); + return; + } - const view = ViewManager.getViewByWebContentsId(webcontents.id); - if (!view) { - return; - } - const serverName = view.view.server.name; + if (getDoNotDisturb()) { + return; + } - const options = { - title: `${serverName}: ${title}`, - body, - silent, - data, - }; + const view = ViewManager.getViewByWebContentsId(webcontents.id); + if (!view) { + return; + } + const serverName = view.view.server.name; - if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) { - return; - } + const options = { + title: `${serverName}: ${title}`, + body, + silent, + data, + }; - const mention = new Mention(options, channel, teamId); - const mentionKey = `${mention.teamId}:${mention.channel.id}`; + if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) { + return; + } - mention.on('show', () => { - log.debug('displayMention.show'); + const mention = new Mention(options, channel, teamId); + const mentionKey = `${mention.teamId}:${mention.channel.id}`; + this.allActiveNotifications.set(mention.uId, mention); - // On Windows, manually dismiss notifications from the same channel and only show the latest one - if (process.platform === 'win32') { - if (currentNotifications.has(mentionKey)) { - log.debug(`close ${mentionKey}`); - currentNotifications.get(mentionKey).close(); - currentNotifications.delete(mentionKey); + 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); } - currentNotifications.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); + + this.allActiveNotifications.delete(mention.uId); + MainWindow.show(); + if (serverName) { + ViewManager.showById(view.id); + webcontents.send('notification-clicked', {channel, teamId, url}); + } + }); + + mention.on('close', () => { + this.allActiveNotifications.delete(mention.uId); + }); + + mention.on('failed', () => { + this.allActiveNotifications.delete(mention.uId); + }); + mention.show(); + } + + public displayDownloadCompleted(fileName: string, path: string, serverName: string) { + log.debug('displayDownloadCompleted', {fileName, path, serverName}); + + if (!Notification.isSupported()) { + log.error('notification not supported'); + return; } - const notificationSound = mention.getNotificationSound(); - if (notificationSound) { - MainWindow.sendToRenderer(PLAY_SOUND, notificationSound); + + if (getDoNotDisturb()) { + return; } - flashFrame(true); - }); - mention.on('click', () => { - log.debug('notification click', serverName, mention); - MainWindow.show(); - if (serverName) { - ViewManager.showById(view.id); - webcontents.send('notification-clicked', {channel, teamId, url}); + const download = new DownloadNotification(fileName, serverName); + this.allActiveNotifications.set(download.uId, download); + + download.on('show', () => { + flashFrame(true); + }); + + download.on('click', () => { + shell.showItemInFolder(path.normalize()); + this.allActiveNotifications.delete(download.uId); + }); + + download.on('close', () => { + this.allActiveNotifications.delete(download.uId); + }); + + download.on('failed', () => { + this.allActiveNotifications.delete(download.uId); + }); + download.show(); + } + + public displayUpgrade(version: string, handleUpgrade: () => void): void { + if (!Notification.isSupported()) { + log.error('notification not supported'); + return; + } + if (getDoNotDisturb()) { + return; } - }); - mention.show(); -} -export function displayDownloadCompleted(fileName: string, path: string, serverName: string) { - log.debug('displayDownloadCompleted', {fileName, path, serverName}); - - if (!Notification.isSupported()) { - log.error('notification not supported'); - return; + if (this.upgradeNotification) { + this.upgradeNotification.close(); + } + this.upgradeNotification = new NewVersionNotification(); + this.upgradeNotification.on('click', () => { + log.info(`User clicked to upgrade to ${version}`); + handleUpgrade(); + }); + this.upgradeNotification.show(); } - if (getDoNotDisturb()) { - return; + public displayRestartToUpgrade(version: string, handleUpgrade: () => void): void { + if (!Notification.isSupported()) { + log.error('notification not supported'); + return; + } + if (getDoNotDisturb()) { + return; + } + + this.restartToUpgradeNotification = new UpgradeNotification(); + this.restartToUpgradeNotification.on('click', () => { + log.info(`User requested perform the upgrade now to ${version}`); + handleUpgrade(); + }); + this.restartToUpgradeNotification.show(); } - - const download = new DownloadNotification(fileName, serverName); - - download.on('show', () => { - flashFrame(true); - }); - - download.on('click', () => { - shell.showItemInFolder(path.normalize()); - }); - download.show(); -} - -let upgrade: NewVersionNotification; - -export function displayUpgrade(version: string, handleUpgrade: () => void): void { - if (!Notification.isSupported()) { - log.error('notification not supported'); - return; - } - if (getDoNotDisturb()) { - return; - } - - if (upgrade) { - upgrade.close(); - } - upgrade = new NewVersionNotification(); - upgrade.on('click', () => { - log.info(`User clicked to upgrade to ${version}`); - handleUpgrade(); - }); - upgrade.show(); -} - -let restartToUpgrade; -export function displayRestartToUpgrade(version: string, handleUpgrade: () => void): void { - if (!Notification.isSupported()) { - log.error('notification not supported'); - return; - } - if (getDoNotDisturb()) { - return; - } - - restartToUpgrade = new UpgradeNotification(); - restartToUpgrade.on('click', () => { - log.info(`User requested perform the upgrade now to ${version}`); - handleUpgrade(); - }); - restartToUpgrade.show(); } function getDoNotDisturb() { @@ -177,3 +200,6 @@ function flashFrame(flash: boolean) { app.dock.bounce(Config.notifications.bounceIconType); } } + +const notificationManager = new NotificationManager(); +export default notificationManager;