Fix notifications not working (#2865)

* Fix notifications not working

* Transform into a manager

* Remove test accessor
This commit is contained in:
Daniel Espino García
2023-10-10 16:13:07 +02:00
committed by GitHub
parent 02261c9de3
commit 9aec7db821
9 changed files with 269 additions and 199 deletions

View File

@@ -96,7 +96,8 @@
"src/main/**/*.ts" "src/main/**/*.ts"
], ],
"testMatch": [ "testMatch": [
"**/src/**/*.test.js" "**/src/**/*.test.js",
"**/src/**/*.test.ts"
], ],
"testPathIgnorePatterns": [ "testPathIgnorePatterns": [
"/node_modules/", "/node_modules/",

View File

@@ -12,7 +12,7 @@ import {Logger} from 'common/log';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import {ping} from 'common/utils/requests'; import {ping} from 'common/utils/requests';
import {displayMention} from 'main/notifications'; import NotificationManager from 'main/notifications';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload, getLocalURLString} from 'main/utils';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import MainWindow from 'main/windows/mainWindow'; 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) { 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}); 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() { export function handleOpenAppMenu() {

View File

@@ -1,13 +1,17 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {ipcMain} from 'electron'; import {ipcMain as notMockedIpcMain} from 'electron';
import {autoUpdater} from 'electron-updater'; import {autoUpdater as notMockedAutoUpdater} from 'electron-updater';
import {CHECK_FOR_UPDATES} from 'common/communication'; import {CHECK_FOR_UPDATES} from 'common/communication';
import NotificationManager from 'main/notifications';
import {UpdateManager} from './autoUpdater'; import {UpdateManager} from './autoUpdater';
import {displayRestartToUpgrade, displayUpgrade} from './notifications';
const autoUpdater = jest.mocked(notMockedAutoUpdater);
const ipcMain = jest.mocked(notMockedIpcMain);
jest.mock('electron', () => ({ jest.mock('electron', () => ({
app: { app: {
@@ -44,6 +48,7 @@ jest.mock('main/notifications', () => ({
displayUpgrade: jest.fn(), displayUpgrade: jest.fn(),
displayRestartToUpgrade: jest.fn(), displayRestartToUpgrade: jest.fn(),
})); }));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
@@ -55,6 +60,7 @@ jest.mock('main/i18nManager', () => ({
jest.mock('main/downloadsManager', () => ({ jest.mock('main/downloadsManager', () => ({
removeUpdateBeforeRestart: jest.fn(), removeUpdateBeforeRestart: jest.fn(),
})); }));
describe('main/autoUpdater', () => { describe('main/autoUpdater', () => {
describe('constructor', () => { describe('constructor', () => {
afterEach(() => { afterEach(() => {
@@ -62,11 +68,12 @@ describe('main/autoUpdater', () => {
}); });
it('should notify user on update-available', () => { it('should notify user on update-available', () => {
let cb; let cb: any;
autoUpdater.on.mockImplementation((event, callback) => { autoUpdater.on.mockImplementation((event, callback) => {
if (event === 'update-available') { if (event === 'update-available') {
cb = callback; cb = callback;
} }
return autoUpdater;
}); });
const updateManager = new UpdateManager(); const updateManager = new UpdateManager();
@@ -78,11 +85,12 @@ describe('main/autoUpdater', () => {
}); });
it('should notify user on update-downloaded', () => { it('should notify user on update-downloaded', () => {
let cb; let cb: any;
autoUpdater.on.mockImplementation((event, callback) => { autoUpdater.on.mockImplementation((event, callback) => {
if (event === 'update-downloaded') { if (event === 'update-downloaded') {
cb = callback; cb = callback;
} }
return autoUpdater;
}); });
const updateManager = new UpdateManager(); const updateManager = new UpdateManager();
@@ -94,11 +102,12 @@ describe('main/autoUpdater', () => {
}); });
it('should check for updates when emitted', () => { it('should check for updates when emitted', () => {
let cb; let cb: any;
ipcMain.on.mockImplementation((event, callback) => { ipcMain.on.mockImplementation((event, callback) => {
if (event === CHECK_FOR_UPDATES) { if (event === CHECK_FOR_UPDATES) {
cb = callback; cb = callback;
} }
return ipcMain;
}); });
const updateManager = new UpdateManager(); const updateManager = new UpdateManager();
@@ -133,7 +142,7 @@ describe('main/autoUpdater', () => {
updateManager.versionAvailable = '5.1.0'; updateManager.versionAvailable = '5.1.0';
updateManager.notify(); updateManager.notify();
updateManager.notify = jest.fn(); 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', () => { it('should display downloaded upgrade notification', () => {
@@ -141,13 +150,13 @@ describe('main/autoUpdater', () => {
updateManager.versionDownloaded = '5.1.0'; updateManager.versionDownloaded = '5.1.0';
updateManager.notify(); updateManager.notify();
updateManager.notify = jest.fn(); 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', () => { describe('checkForUpdates', () => {
beforeEach(() => { beforeEach(() => {
autoUpdater.checkForUpdates.mockReturnValue(Promise.resolve()); autoUpdater.checkForUpdates.mockReturnValue(Promise.resolve(null));
jest.useFakeTimers(); jest.useFakeTimers();
}); });
@@ -164,8 +173,9 @@ describe('main/autoUpdater', () => {
it('should show dialog if update is not available', () => { it('should show dialog if update is not available', () => {
autoUpdater.once.mockImplementation((event, callback) => { autoUpdater.once.mockImplementation((event, callback) => {
if (event === 'update-not-available') { if (event === 'update-not-available') {
callback(); (callback as any)();
} }
return autoUpdater;
}); });
const updateManager = new UpdateManager(); const updateManager = new UpdateManager();
@@ -177,7 +187,7 @@ describe('main/autoUpdater', () => {
it('should check again at the next interval', () => { it('should check again at the next interval', () => {
const updateManager = new UpdateManager(); const updateManager = new UpdateManager();
updateManager.checkForUpdates(); updateManager.checkForUpdates(false);
updateManager.checkForUpdates = jest.fn(); updateManager.checkForUpdates = jest.fn();
jest.runAllTimers(); jest.runAllTimers();
expect(updateManager.checkForUpdates).toBeCalled(); expect(updateManager.checkForUpdates).toBeCalled();

View File

@@ -10,7 +10,7 @@ import {Logger} from 'common/log';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import {displayUpgrade, displayRestartToUpgrade} from 'main/notifications'; import NotificationManager from 'main/notifications';
import { import {
CANCEL_UPGRADE, CANCEL_UPGRADE,
@@ -113,12 +113,12 @@ export class UpdateManager {
notifyUpgrade = (): void => { notifyUpgrade = (): void => {
ipcMain.emit(UPDATE_AVAILABLE, null, this.versionAvailable); ipcMain.emit(UPDATE_AVAILABLE, null, this.versionAvailable);
displayUpgrade(this.versionAvailable || 'unknown', this.handleDownload); NotificationManager.displayUpgrade(this.versionAvailable || 'unknown', this.handleDownload);
} }
notifyDownloaded = (): void => { notifyDownloaded = (): void => {
ipcMain.emit(UPDATE_DOWNLOADED, null, this.downloadedInfo); ipcMain.emit(UPDATE_DOWNLOADED, null, this.downloadedInfo);
displayRestartToUpgrade(this.versionDownloaded || 'unknown', this.handleUpdate); NotificationManager.displayRestartToUpgrade(this.versionDownloaded || 'unknown', this.handleUpdate);
} }
handleDownload = (): void => { handleDownload = (): void => {

View File

@@ -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 {DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT, DOWNLOADS_DROPDOWN_MAX_ITEMS} from 'common/utils/constants';
import * as Validator from 'common/Validator'; import * as Validator from 'common/Validator';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import {displayDownloadCompleted} from 'main/notifications'; import NotificationManager from 'main/notifications';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils'; import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils';
@@ -559,7 +559,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
log.debug('doneEventController', {state}); log.debug('doneEventController', {state});
if (state === 'completed' && !this.open) { 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)); const bookmark = this.bookmarks.get(this.getFileId(item));

View File

@@ -4,6 +4,8 @@
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import {v4 as uuid} from 'uuid';
import {app, Notification} from 'electron'; import {app, Notification} from 'electron';
import Utils from 'common/utils/util'; import Utils from 'common/utils/util';
@@ -22,6 +24,8 @@ const defaultOptions = {
}; };
export class DownloadNotification extends Notification { export class DownloadNotification extends Notification {
uId: string;
constructor(fileName: string, serverName: string) { constructor(fileName: string, serverName: string) {
const options = {...defaultOptions}; const options = {...defaultOptions};
if (process.platform === 'darwin' || (process.platform === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '10.0'))) { 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; options.body = process.platform === 'win32' ? localizeMessage('main.notifications.download.complete.body', 'Download Complete \n {fileName}', {fileName}) : fileName;
super(options); super(options);
this.uId = uuid();
} }
} }

View File

@@ -4,6 +4,8 @@
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import {v4 as uuid} from 'uuid';
import {app, Notification} from 'electron'; import {app, Notification} from 'electron';
import {MentionOptions} from 'types/notification'; import {MentionOptions} from 'types/notification';
@@ -27,6 +29,7 @@ export class Mention extends Notification {
customSound: string; customSound: string;
channel: {id: string}; // TODO: Channel from mattermost-redux channel: {id: string}; // TODO: Channel from mattermost-redux
teamId: string; teamId: string;
uId: string;
constructor(customOptions: MentionOptions, channel: {id: string}, teamId: string) { constructor(customOptions: MentionOptions, channel: {id: string}, teamId: string) {
const options = {...defaultOptions, ...customOptions}; const options = {...defaultOptions, ...customOptions};
@@ -44,6 +47,7 @@ export class Mention extends Notification {
this.customSound = customSound; this.customSound = customSound;
this.channel = channel; this.channel = channel;
this.teamId = teamId; this.teamId = teamId;
this.uId = uuid();
} }
getNotificationSound = () => { getNotificationSound = () => {

View File

@@ -2,26 +2,35 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
'use strict'; '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 {getFocusAssist as notMockedGetFocusAssist} from 'windows-focus-assist';
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state'; import {getDoNotDisturb as notMockedGetDarwinDoNotDisturb} from 'macos-notification-state';
import {PLAY_SOUND} from 'common/communication'; import {PLAY_SOUND} from 'common/communication';
import Config from 'common/config'; import notMockedConfig from 'common/config';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage as notMockedLocalizeMessage} from 'main/i18nManager';
import PermissionsManager from 'main/permissionsManager'; import notMockedPermissionsManager from 'main/permissionsManager';
import MainWindow from 'main/windows/mainWindow'; import notMockedMainWindow from 'main/windows/mainWindow';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import getLinuxDoNotDisturb from './dnd-linux'; 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', () => ({ jest.mock('child_process', () => ({
execSync: jest.fn(), execSync: jest.fn(),
@@ -29,25 +38,26 @@ jest.mock('child_process', () => ({
jest.mock('electron', () => { jest.mock('electron', () => {
class NotificationMock { class NotificationMock {
callbackMap: Map<string, () => void>;
static isSupported = jest.fn(); static isSupported = jest.fn();
static didConstruct = jest.fn(); static didConstruct = jest.fn();
constructor(options) { constructor(options: any) {
NotificationMock.didConstruct(); NotificationMock.didConstruct();
this.callbackMap = new Map(); this.callbackMap = new Map();
mentions.push({body: options.body, value: this}); mentions.push({body: options.body, value: this});
} }
on = (event, callback) => { on = (event: string, callback: () => void) => {
this.callbackMap.set(event, callback); this.callbackMap.set(event, callback);
} }
show = jest.fn().mockImplementation(() => { show = jest.fn().mockImplementation(() => {
this.callbackMap.get('show')(); this.callbackMap.get('show')?.();
}); });
click = jest.fn().mockImplementation(() => { click = jest.fn().mockImplementation(() => {
this.callbackMap.get('click')(); this.callbackMap.get('click')?.();
}); });
close = jest.fn(); close = jest.fn();
@@ -105,35 +115,43 @@ describe('main/notifications', () => {
describe('displayMention', () => { describe('displayMention', () => {
const mainWindow = { const mainWindow = {
flashFrame: jest.fn(), flashFrame: jest.fn(),
}; } as unknown as BrowserWindow;
beforeEach(() => { beforeEach(() => {
PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(true)); PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(true));
Notification.isSupported.mockImplementation(() => true); Notification.isSupported.mockImplementation(() => true);
getFocusAssist.mockReturnValue({value: false}); getFocusAssist.mockReturnValue({value: 0, name: ''});
getDarwinDoNotDisturb.mockReturnValue(false); getDarwinDoNotDisturb.mockReturnValue(false);
Config.notifications = {}; Config.notifications = {
flashWindow: 0,
bounceIcon: false,
bounceIconType: 'informational',
};
MainWindow.get.mockReturnValue(mainWindow); MainWindow.get.mockReturnValue(mainWindow);
}); });
afterEach(() => { afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
Config.notifications = {}; Config.notifications = {
flashWindow: 0,
bounceIcon: false,
bounceIconType: 'informational',
};
}); });
it('should do nothing when Notification is not supported', async () => { it('should do nothing when Notification is not supported', async () => {
Notification.isSupported.mockImplementation(() => false); Notification.isSupported.mockImplementation(() => false);
await displayMention( await NotificationManager.displayMention(
'test', 'test',
'test body', 'test body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, 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 () => { it('should do nothing when alarms only is enabled on windows', async () => {
@@ -142,18 +160,18 @@ describe('main/notifications', () => {
value: 'win32', value: 'win32',
}); });
getFocusAssist.mockReturnValue({value: 2}); getFocusAssist.mockReturnValue({value: 2, name: ''});
await displayMention( await NotificationManager.displayMention(
'test', 'test',
'test body', 'test body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, false,
{id: 1}, {id: 1} as WebContents,
{}, {soundName: ''},
); );
expect(Notification.didConstruct).not.toBeCalled(); expect(MainWindow.show).not.toBeCalled();
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: originalPlatform, value: originalPlatform,
@@ -167,17 +185,17 @@ describe('main/notifications', () => {
}); });
getDarwinDoNotDisturb.mockReturnValue(true); getDarwinDoNotDisturb.mockReturnValue(true);
await displayMention( await NotificationManager.displayMention(
'test', 'test',
'test body', 'test body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, false,
{id: 1}, {id: 1} as WebContents,
{}, {soundName: ''},
); );
expect(Notification.didConstruct).not.toBeCalled(); expect(MainWindow.show).not.toBeCalled();
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: originalPlatform, value: originalPlatform,
@@ -186,28 +204,28 @@ describe('main/notifications', () => {
it('should do nothing when the permission check fails', async () => { it('should do nothing when the permission check fails', async () => {
PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(false)); PermissionsManager.doPermissionRequest.mockReturnValue(Promise.resolve(false));
await displayMention( await NotificationManager.displayMention(
'test', 'test',
'test body', 'test body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, 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 () => { it('should play notification sound when custom sound is provided', async () => {
await displayMention( await NotificationManager.displayMention(
'test', 'test',
'test body', 'test body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, false,
{id: 1}, {id: 1} as WebContents,
{soundName: 'test_sound'}, {soundName: 'test_sound'},
); );
expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound'); expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound');
@@ -219,34 +237,36 @@ describe('main/notifications', () => {
value: 'win32', value: 'win32',
}); });
await displayMention( await NotificationManager.displayMention(
'test', 'test',
'test body', 'test body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, 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'); const existingMention = mentionsPerChannel.get('team_id:channel_id');
currentNotifications.delete = jest.fn(); mentionsPerChannel.delete = jest.fn();
await displayMention( await NotificationManager.displayMention(
'test', 'test',
'test body 2', 'test body 2',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, false,
{id: 1}, {id: 1} as WebContents,
{}, {soundName: ''},
); );
expect(currentNotifications.delete).toHaveBeenCalled(); expect(mentionsPerChannel.delete).toHaveBeenCalled();
expect(existingMention.close).toHaveBeenCalled(); expect(existingMention?.close).toHaveBeenCalled();
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: originalPlatform, value: originalPlatform,
@@ -254,18 +274,18 @@ describe('main/notifications', () => {
}); });
it('should switch view when clicking on notification', async () => { it('should switch view when clicking on notification', async () => {
await displayMention( await NotificationManager.displayMention(
'click_test', 'click_test',
'mention_click_body', 'mention_click_body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, 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'); const mention = mentions.find((m) => m.body === 'mention_click_body');
mention.value.click(); mention?.value.click();
expect(MainWindow.show).toHaveBeenCalled(); expect(MainWindow.show).toHaveBeenCalled();
expect(ViewManager.showById).toHaveBeenCalledWith('server_id'); expect(ViewManager.showById).toHaveBeenCalledWith('server_id');
}); });
@@ -275,15 +295,15 @@ describe('main/notifications', () => {
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: 'linux', value: 'linux',
}); });
await displayMention( await NotificationManager.displayMention(
'click_test', 'click_test',
'mention_click_body', 'mention_click_body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, false,
{id: 1, send: jest.fn()}, {id: 1, send: jest.fn()} as unknown as WebContents,
{}, {soundName: ''},
); );
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: originalPlatform, value: originalPlatform,
@@ -293,21 +313,23 @@ describe('main/notifications', () => {
it('linux/windows - should flash frame when config item is set', async () => { it('linux/windows - should flash frame when config item is set', async () => {
Config.notifications = { Config.notifications = {
flashWindow: true, flashWindow: 1,
bounceIcon: false,
bounceIconType: 'informational',
}; };
const originalPlatform = process.platform; const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: 'linux', value: 'linux',
}); });
await displayMention( await NotificationManager.displayMention(
'click_test', 'click_test',
'mention_click_body', 'mention_click_body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, false,
{id: 1, send: jest.fn()}, {id: 1, send: jest.fn()} as unknown as WebContents,
{}, {soundName: ''},
); );
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: originalPlatform, value: originalPlatform,
@@ -320,15 +342,15 @@ describe('main/notifications', () => {
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: 'darwin', value: 'darwin',
}); });
await displayMention( await NotificationManager.displayMention(
'click_test', 'click_test',
'mention_click_body', 'mention_click_body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, false,
{id: 1, send: jest.fn()}, {id: 1, send: jest.fn()} as unknown as WebContents,
{}, {soundName: ''},
); );
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: originalPlatform, value: originalPlatform,
@@ -340,20 +362,21 @@ describe('main/notifications', () => {
Config.notifications = { Config.notifications = {
bounceIcon: true, bounceIcon: true,
bounceIconType: 'critical', bounceIconType: 'critical',
flashWindow: 0,
}; };
const originalPlatform = process.platform; const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: 'darwin', value: 'darwin',
}); });
await displayMention( await NotificationManager.displayMention(
'click_test', 'click_test',
'mention_click_body', 'mention_click_body',
{id: 'channel_id'}, {id: 'channel_id'},
'team_id', 'team_id',
'http://server-1.com/team_id/channel_id', 'http://server-1.com/team_id/channel_id',
false, false,
{id: 1, send: jest.fn()}, {id: 1, send: jest.fn()} as unknown as WebContents,
{}, {soundName: ''},
); );
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: originalPlatform, value: originalPlatform,
@@ -365,26 +388,26 @@ describe('main/notifications', () => {
describe('displayDownloadCompleted', () => { describe('displayDownloadCompleted', () => {
beforeEach(() => { beforeEach(() => {
Notification.isSupported.mockImplementation(() => true); Notification.isSupported.mockImplementation(() => true);
getFocusAssist.mockReturnValue({value: false}); getFocusAssist.mockReturnValue({value: 0, name: ''});
getDarwinDoNotDisturb.mockReturnValue(false); getDarwinDoNotDisturb.mockReturnValue(false);
}); });
it('should open file when clicked', () => { it('should open file when clicked', () => {
getDarwinDoNotDisturb.mockReturnValue(false); getDarwinDoNotDisturb.mockReturnValue(false);
localizeMessage.mockReturnValue('test_filename'); localizeMessage.mockReturnValue('test_filename');
displayDownloadCompleted( NotificationManager.displayDownloadCompleted(
'test_filename', 'test_filename',
'/path/to/file', '/path/to/file',
'server_name', 'server_name',
); );
const mention = mentions.find((m) => m.body.includes('test_filename')); const mention = mentions.find((m) => m.body.includes('test_filename'));
mention.value.click(); mention?.value.click();
expect(shell.showItemInFolder).toHaveBeenCalledWith('/path/to/file'); expect(shell.showItemInFolder).toHaveBeenCalledWith('/path/to/file');
}); });
}); });
describe('getLinuxDoNotDisturb', () => { describe('getLinuxDoNotDisturb', () => {
let originalPlatform; let originalPlatform: NodeJS.Platform;
beforeAll(() => { beforeAll(() => {
originalPlatform = process.platform; originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
@@ -399,7 +422,7 @@ describe('main/notifications', () => {
}); });
it('should return false', () => { it('should return false', () => {
cp.execSync.mockReturnValue('true'); cp.execSync.mockReturnValue(Buffer.from('true'));
expect(getLinuxDoNotDisturb()).toBe(false); expect(getLinuxDoNotDisturb()).toBe(false);
}); });
@@ -411,7 +434,7 @@ describe('main/notifications', () => {
}); });
it('should return true', () => { it('should return true', () => {
cp.execSync.mockReturnValue('false'); cp.execSync.mockReturnValue(Buffer.from('false'));
expect(getLinuxDoNotDisturb()).toBe(true); expect(getLinuxDoNotDisturb()).toBe(true);
}); });
}); });

View File

@@ -21,134 +21,157 @@ import {NewVersionNotification, UpgradeNotification} from './Upgrade';
import getLinuxDoNotDisturb from './dnd-linux'; import getLinuxDoNotDisturb from './dnd-linux';
import getWindowsDoNotDisturb from './dnd-windows'; import getWindowsDoNotDisturb from './dnd-windows';
export const currentNotifications = new Map();
const log = new Logger('Notifications'); 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) { class NotificationManager {
log.debug('displayMention', {title, body, channel, teamId, url, silent, data}); private mentionsPerChannel: Map<string, Mention> = new Map();
private allActiveNotifications: Map<string, Notification> = new Map();
private upgradeNotification?: NewVersionNotification;
private restartToUpgradeNotification?: UpgradeNotification;
if (!Notification.isSupported()) { public async displayMention(title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, data: MentionData) {
log.error('notification not supported'); log.debug('displayMention', {title, body, channel, teamId, url, silent, data});
return;
}
if (getDoNotDisturb()) { if (!Notification.isSupported()) {
return; log.error('notification not supported');
} return;
}
const view = ViewManager.getViewByWebContentsId(webcontents.id); if (getDoNotDisturb()) {
if (!view) { return;
return; }
}
const serverName = view.view.server.name;
const options = { const view = ViewManager.getViewByWebContentsId(webcontents.id);
title: `${serverName}: ${title}`, if (!view) {
body, return;
silent, }
data, const serverName = view.view.server.name;
};
if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) { const options = {
return; title: `${serverName}: ${title}`,
} body,
silent,
data,
};
const mention = new Mention(options, channel, teamId); if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) {
const mentionKey = `${mention.teamId}:${mention.channel.id}`; return;
}
mention.on('show', () => { const mention = new Mention(options, channel, teamId);
log.debug('displayMention.show'); 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 mention.on('show', () => {
if (process.platform === 'win32') { log.debug('displayMention.show');
if (currentNotifications.has(mentionKey)) {
log.debug(`close ${mentionKey}`); // On Windows, manually dismiss notifications from the same channel and only show the latest one
currentNotifications.get(mentionKey).close(); if (process.platform === 'win32') {
currentNotifications.delete(mentionKey); 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) { if (getDoNotDisturb()) {
MainWindow.sendToRenderer(PLAY_SOUND, notificationSound); return;
} }
flashFrame(true);
});
mention.on('click', () => { const download = new DownloadNotification(fileName, serverName);
log.debug('notification click', serverName, mention); this.allActiveNotifications.set(download.uId, download);
MainWindow.show();
if (serverName) { download.on('show', () => {
ViewManager.showById(view.id); flashFrame(true);
webcontents.send('notification-clicked', {channel, teamId, url}); });
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) { if (this.upgradeNotification) {
log.debug('displayDownloadCompleted', {fileName, path, serverName}); this.upgradeNotification.close();
}
if (!Notification.isSupported()) { this.upgradeNotification = new NewVersionNotification();
log.error('notification not supported'); this.upgradeNotification.on('click', () => {
return; log.info(`User clicked to upgrade to ${version}`);
handleUpgrade();
});
this.upgradeNotification.show();
} }
if (getDoNotDisturb()) { public displayRestartToUpgrade(version: string, handleUpgrade: () => void): void {
return; 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() { function getDoNotDisturb() {
@@ -177,3 +200,6 @@ function flashFrame(flash: boolean) {
app.dock.bounce(Config.notifications.bounceIconType); app.dock.bounce(Config.notifications.bounceIconType);
} }
} }
const notificationManager = new NotificationManager();
export default notificationManager;