From 0cab09b7f591d1de5175a986bf01fbf08c8974ca Mon Sep 17 00:00:00 2001 From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:39:46 -0500 Subject: [PATCH] [MM-55152] Add new Desktop API endpoints, improve preload script, some clean-up (#2900) * Add constants for app info, add to API * Migrate history button * Converted calls API over to context bridge, removed some unnecessary logging * Convert to TS, add types for web app to consume * Fix tests, prune * Fix lint * More changes to support the legacy API * Force legacy code off, add support for unreads/mentions/expired through the API * Fix issues with cross-tab login, removed need for log in/log out signalling * Fixed test, typos * Change package name for types * Add some other stuff to the types * PR feedback * More feedback * Use npm package * Change types and API to provide off listeners * Version number * Lock * Fix typo * Add sessionID for calls --- .eslintrc.json | 2 +- .gitignore | 1 + api-types/index.ts | 67 +++ api-types/lib/index.d.ts | 57 +++ api-types/lib/index.js | 4 + api-types/package-lock.json | 21 + api-types/package.json | 33 ++ api-types/tsconfig.json | 20 + package-lock.json | 23 + package.json | 1 + src/app/serverViewState.ts | 8 +- src/common/communication.ts | 15 +- src/common/utils/url.ts | 1 + src/main/app/initialize.ts | 5 +- src/main/app/intercom.ts | 9 +- src/main/authManager.ts | 2 +- src/main/certificateManager.ts | 2 +- src/main/notifications/Mention.ts | 8 +- src/main/notifications/index.test.ts | 49 +- src/main/notifications/index.ts | 19 +- src/main/preload/callsWidget.js | 94 ---- src/main/preload/externalAPI.ts | 470 ++++++++++++++++++ .../preload/{desktopAPI.js => internalAPI.js} | 3 +- src/main/preload/mattermost.js | 396 --------------- src/main/views/MattermostBrowserView.test.js | 1 + src/main/views/MattermostBrowserView.ts | 67 +-- src/main/views/downloadsDropdownMenuView.ts | 8 +- src/main/views/downloadsDropdownView.ts | 10 +- src/main/views/loadingScreen.ts | 2 +- src/main/views/serverDropdownView.ts | 4 +- src/main/views/viewManager.test.js | 11 +- src/main/views/viewManager.ts | 95 ++-- src/main/views/webContentEvents.ts | 5 +- src/main/windows/callsWidgetWindow.test.js | 315 +++--------- src/main/windows/callsWidgetWindow.ts | 213 ++++---- src/main/windows/mainWindow.ts | 2 +- src/main/windows/settingsWindow.ts | 2 +- src/types/calls.ts | 31 -- src/types/externalAPI.ts | 24 + src/types/notification.ts | 6 +- webpack.config.main.js | 5 +- 41 files changed, 1071 insertions(+), 1040 deletions(-) create mode 100644 api-types/index.ts create mode 100644 api-types/lib/index.d.ts create mode 100644 api-types/lib/index.js create mode 100644 api-types/package-lock.json create mode 100644 api-types/package.json create mode 100644 api-types/tsconfig.json delete mode 100644 src/main/preload/callsWidget.js create mode 100644 src/main/preload/externalAPI.ts rename src/main/preload/{desktopAPI.js => internalAPI.js} (99%) delete mode 100644 src/main/preload/mattermost.js create mode 100644 src/types/externalAPI.ts diff --git a/.eslintrc.json b/.eslintrc.json index d5a4e2ab..7b1f79ec 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -103,7 +103,7 @@ "src/main/downloadURL.ts", "src/main/SpellChecker.ts", "src/main/menus/app.ts", - "src/main/preload/mattermost.js", + "src/main/preload/externalAPI.js", "src/renderer/components/RemoveServerModal.tsx", "src/renderer/components/MainPage.tsx", "src/renderer/components/HoveringURL.tsx", diff --git a/.gitignore b/.gitignore index fc93d6fe..eae3922b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ fastlane/README.md fastlane/report.xml *.provisionprofile +*.tsbuildinfo \ No newline at end of file diff --git a/api-types/index.ts b/api-types/index.ts new file mode 100644 index 00000000..d48fd1dc --- /dev/null +++ b/api-types/index.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export type DesktopSourcesOptions = { + types: Array<'screen' | 'window'>; + thumbnailSize?: {height: number; width: number}; + fetchWindowIcons?: boolean; +}; +export type DesktopCaptureSource = { + id: string; + name: string; + thumbnailURL: string; +}; + +export type DesktopAPI = { + + // Initialization + isDev: () => Promise; + getAppInfo: () => Promise<{name: string; version: string}>; + reactAppInitialized: () => void; + + // Session + setSessionExpired: (isExpired: boolean) => void; + onUserActivityUpdate: (listener: ( + userIsActive: boolean, + idleTime: number, + isSystemEvent: boolean, + ) => void) => () => void; + + // Unreads/mentions/notifications + sendNotification: (title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) => void; + onNotificationClicked: (listener: (channelId: string, teamId: string, url: string) => void) => () => void; + setUnreadsAndMentions: (isUnread: boolean, mentionCount: number) => void; + + // Navigation + requestBrowserHistoryStatus: () => Promise<{canGoBack: boolean; canGoForward: boolean}>; + onBrowserHistoryStatusUpdated: (listener: (canGoBack: boolean, canGoForward: boolean) => void) => () => void; + onBrowserHistoryPush: (listener: (pathName: string) => void) => () => void; + sendBrowserHistoryPush: (path: string) => void; + + // Calls widget + openLinkFromCallsWidget: (url: string) => void; + openScreenShareModal: () => void; + onScreenShared: (listener: (sourceID: string, withAudio: boolean) => void) => () => void; + callsWidgetConnected: (callID: string, sessionID: string) => void; + onJoinCallRequest: (listener: (callID: string) => void) => () => void; + resizeCallsWidget: (width: number, height: number) => void; + focusPopout: () => void; + leaveCall: () => void; + sendCallsError: (err: string, callID?: string, errMsg?: string) => void; + + // Calls plugin + getDesktopSources: (opts: DesktopSourcesOptions) => Promise; + onOpenScreenShareModal: (listener: () => void) => () => void; + shareScreen: (sourceID: string, withAudi: boolean) => void; + joinCall: (opts: { + callID: string; + title: string; + rootID: string; + channelURL: string; + }) => Promise<{callID: string; sessionID: string}>; + sendJoinCallRequest: (callId: string) => void; + onCallsError: (listener: (err: string, callID?: string, errMsg?: string) => void) => () => void; + + // Utility + unregister: (channel: string) => void; +} diff --git a/api-types/lib/index.d.ts b/api-types/lib/index.d.ts new file mode 100644 index 00000000..9ebc44a3 --- /dev/null +++ b/api-types/lib/index.d.ts @@ -0,0 +1,57 @@ +export declare type DesktopSourcesOptions = { + types: Array<'screen' | 'window'>; + thumbnailSize?: { + height: number; + width: number; + }; + fetchWindowIcons?: boolean; +}; +export declare type DesktopCaptureSource = { + id: string; + name: string; + thumbnailURL: string; +}; +export declare type DesktopAPI = { + isDev: () => Promise; + getAppInfo: () => Promise<{ + name: string; + version: string; + }>; + 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; + onNotificationClicked: (listener: (channelId: string, teamId: string, url: string) => void) => () => void; + setUnreadsAndMentions: (isUnread: boolean, mentionCount: number) => void; + requestBrowserHistoryStatus: () => Promise<{ + canGoBack: boolean; + canGoForward: boolean; + }>; + onBrowserHistoryStatusUpdated: (listener: (canGoBack: boolean, canGoForward: boolean) => void) => () => void; + onBrowserHistoryPush: (listener: (pathName: string) => void) => () => void; + sendBrowserHistoryPush: (path: string) => void; + openLinkFromCallsWidget: (url: string) => void; + openScreenShareModal: () => void; + onScreenShared: (listener: (sourceID: string, withAudio: boolean) => void) => () => void; + callsWidgetConnected: (callID: string, sessionID: string) => void; + onJoinCallRequest: (listener: (callID: string) => void) => () => void; + resizeCallsWidget: (width: number, height: number) => void; + focusPopout: () => void; + leaveCall: () => void; + sendCallsError: (err: string, callID?: string, errMsg?: string) => void; + getDesktopSources: (opts: DesktopSourcesOptions) => Promise; + onOpenScreenShareModal: (listener: () => void) => () => void; + shareScreen: (sourceID: string, withAudi: boolean) => void; + joinCall: (opts: { + callID: string; + title: string; + rootID: string; + channelURL: string; + }) => Promise<{ + callID: string; + sessionID: string; + }>; + sendJoinCallRequest: (callId: string) => void; + onCallsError: (listener: (err: string, callID?: string, errMsg?: string) => void) => () => void; + unregister: (channel: string) => void; +}; diff --git a/api-types/lib/index.js b/api-types/lib/index.js new file mode 100644 index 00000000..96ab4ebb --- /dev/null +++ b/api-types/lib/index.js @@ -0,0 +1,4 @@ +"use strict"; +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/api-types/package-lock.json b/api-types/package-lock.json new file mode 100644 index 00000000..eb4c10bc --- /dev/null +++ b/api-types/package-lock.json @@ -0,0 +1,21 @@ +{ + "name": "@mattermost/desktop-api", + "version": "5.7.0-2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@mattermost/desktop-api", + "version": "5.7.0-2", + "license": "MIT", + "peerDependencies": { + "typescript": "^4.3" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/api-types/package.json b/api-types/package.json new file mode 100644 index 00000000..3ef770fc --- /dev/null +++ b/api-types/package.json @@ -0,0 +1,33 @@ +{ + "name": "@mattermost/desktop-api", + "version": "5.7.0-2", + "description": "Shared types for the Desktop App API provided to the Web App", + "keywords": [ + "mattermost" + ], + "homepage": "https://github.com/mattermost/desktop", + "license": "MIT", + "files": [ + "lib" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "repository": { + "type": "git", + "url": "github:mattermost/desktop", + "directory": "api-types" + }, + "peerDependencies": { + "typescript": "^4.3" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + }, + "scripts": { + "build": "tsc --build --verbose", + "run": "tsc --watch --preserveWatchOutput", + "clean": "rm -rf tsconfig.tsbuildinfo ./lib" + } +} diff --git a/api-types/tsconfig.json b/api-types/tsconfig.json new file mode 100644 index 00000000..cbfd2e39 --- /dev/null +++ b/api-types/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "target": "es6", + "declaration": true, + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "jsx": "react", + "outDir": "./lib", + "rootDir": ".", + "composite": true + }, + "include": [ + "./index.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index c7c43d9b..2f57428f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@electron/fuses": "1.6.0", "@electron/universal": "1.3.1", "@mattermost/compass-icons": "0.1.32", + "@mattermost/desktop-api": "*", "@storybook/addon-actions": "6.4.20", "@storybook/react": "6.4.20", "@types/auto-launch": "5.0.2", @@ -117,6 +118,20 @@ "node": ">=16.16.0" } }, + "api-types": { + "name": "@mattermost/desktop-api", + "version": "5.7.0-0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "^4.3" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@ampproject/remapping": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", @@ -4791,6 +4806,10 @@ "integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==", "dev": true }, + "node_modules/@mattermost/desktop-api": { + "resolved": "api-types", + "link": true + }, "node_modules/@mdx-js/mdx": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz", @@ -40037,6 +40056,10 @@ "integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==", "dev": true }, + "@mattermost/desktop-api": { + "version": "file:api-types", + "requires": {} + }, "@mdx-js/mdx": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz", diff --git a/package.json b/package.json index 18635e54..08966d93 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ "@electron/fuses": "1.6.0", "@electron/universal": "1.3.1", "@mattermost/compass-icons": "0.1.32", + "@mattermost/desktop-api": "*", "@storybook/addon-actions": "6.4.20", "@storybook/react": "6.4.20", "@types/auto-launch": "5.0.2", diff --git a/src/app/serverViewState.ts b/src/app/serverViewState.ts index bbb84662..8d05fabe 100644 --- a/src/app/serverViewState.ts +++ b/src/app/serverViewState.ts @@ -68,7 +68,7 @@ export class ServerViewState { } getCurrentServer = () => { - log.debug('getCurrentServer'); + log.silly('getCurrentServer'); if (!this.currentServerId) { throw new Error('No server set as current'); @@ -132,7 +132,7 @@ export class ServerViewState { const modalPromise = ModalManager.addModal( 'newServer', getLocalURLString('newServer.html'), - getLocalPreload('desktopAPI.js'), + getLocalPreload('internalAPI.js'), null, mainWindow, !ServerManager.hasServers(), @@ -164,7 +164,7 @@ export class ServerViewState { const modalPromise = ModalManager.addModal( 'editServer', getLocalURLString('editServer.html'), - getLocalPreload('desktopAPI.js'), + getLocalPreload('internalAPI.js'), server.toUniqueServer(), mainWindow); @@ -191,7 +191,7 @@ export class ServerViewState { const modalPromise = ModalManager.addModal( 'removeServer', getLocalURLString('removeServer.html'), - getLocalPreload('desktopAPI.js'), + getLocalPreload('internalAPI.js'), server.name, mainWindow, ); diff --git a/src/common/communication.ts b/src/common/communication.ts index c5133734..6be01066 100644 --- a/src/common/communication.ts +++ b/src/common/communication.ts @@ -1,6 +1,8 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +export const GET_APP_INFO = 'get-app-info'; + export const SWITCH_SERVER = 'switch-server'; export const SWITCH_TAB = 'switch-tab'; export const CLOSE_VIEW = 'close-view'; @@ -58,9 +60,9 @@ export const GET_DOWNLOAD_LOCATION = 'get_download_location'; export const UPDATE_MENTIONS = 'update_mentions'; export const IS_UNREAD = 'is_unread'; export const UNREAD_RESULT = 'unread_result'; +export const UNREADS_AND_MENTIONS = 'unreads-and-mentions'; export const SESSION_EXPIRED = 'session_expired'; -export const SET_VIEW_OPTIONS = 'set-view-name'; export const REACT_APP_INITIALIZED = 'react-app-initialized'; export const TOGGLE_BACK_BUTTON = 'toggle-back-button'; @@ -93,7 +95,6 @@ export const START_UPGRADE = 'start-upgrade'; export const CHECK_FOR_UPDATES = 'check-for-updates'; export const NO_UPDATE_AVAILABLE = 'no-update-available'; -export const BROWSER_HISTORY_BUTTON = 'browser-history-button'; export const BROWSER_HISTORY_PUSH = 'browser-history-push'; export const APP_LOGGED_IN = 'app-logged-in'; export const APP_LOGGED_OUT = 'app-logged-out'; @@ -118,7 +119,7 @@ export const GET_AVAILABLE_LANGUAGES = 'get-available-languages'; export const VIEW_FINISHED_RESIZING = 'view-finished-resizing'; // Calls -export const DISPATCH_GET_DESKTOP_SOURCES = 'dispatch-get-desktop-sources'; +export const GET_DESKTOP_SOURCES = 'get-desktop-sources'; export const DESKTOP_SOURCES_RESULT = 'desktop-sources-result'; export const DESKTOP_SOURCES_MODAL_REQUEST = 'desktop-sources-modal-request'; export const CALLS_JOIN_CALL = 'calls-join-call'; @@ -177,3 +178,11 @@ export const VALIDATE_SERVER_URL = 'validate-server-url'; export const GET_IS_DEV_MODE = 'get-is-dev-mode'; export const TOGGLE_SECURE_INPUT = 'toggle-secure-input'; + +export const REQUEST_BROWSER_HISTORY_STATUS = 'request-browser-history-status'; +export const BROWSER_HISTORY_STATUS_UPDATED = 'browser-history-status-updated'; + +export const NOTIFICATION_CLICKED = 'notification-clicked'; + +// Legacy code remove signal +export const LEGACY_OFF = 'legacy-off'; diff --git a/src/common/utils/url.ts b/src/common/utils/url.ts index 8bed565c..856beb93 100644 --- a/src/common/utils/url.ts +++ b/src/common/utils/url.ts @@ -57,6 +57,7 @@ export const isUrlType = (urlType: string, serverURL: URL, inputURL: URL) => { getFormattedPathName(inputURL.pathname).startsWith(`/${urlType}/`)); }; +export const isLoginUrl = (serverURL: URL, inputURL: URL) => isUrlType('login', serverURL, inputURL); export const isHelpUrl = (serverURL: URL, inputURL: URL) => isUrlType('help', serverURL, inputURL); export const isImageProxyUrl = (serverURL: URL, inputURL: URL) => isUrlType('api/v4/image', serverURL, inputURL); export const isPublicFilesUrl = (serverURL: URL, inputURL: URL) => isUrlType('files', serverURL, inputURL); diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index 201e633a..712fc340 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -30,6 +30,7 @@ import { WINDOW_RESTORE, DOUBLE_CLICK_ON_WINDOW, TOGGLE_SECURE_INPUT, + GET_APP_INFO, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -249,7 +250,7 @@ function initializeBeforeAppReady() { function initializeInterCommunicationEventListeners() { ipcMain.on(NOTIFY_MENTION, handleMentionNotification); - ipcMain.handle('get-app-version', handleAppVersion); + ipcMain.handle(GET_APP_INFO, handleAppVersion); ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent); ipcMain.on(FOCUS_BROWSERVIEW, ViewManager.focusCurrentView); @@ -366,7 +367,7 @@ async function initializeAfterAppReady() { // listen for status updates and pass on to renderer UserActivityMonitor.on('status', (status) => { log.debug('UserActivityMonitor.on(status)', status); - ViewManager.sendToAllViews(USER_ACTIVITY_UPDATE, status); + ViewManager.sendToAllViews(USER_ACTIVITY_UPDATE, status.userIsActive, status.idleTime, status.isSystemEvent); }); // start monitoring user activity (needs to be started after the app is ready) diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index d2663cbb..5b8f11a8 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -4,7 +4,6 @@ import {app, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron'; import {UniqueServer} from 'types/config'; -import {MentionData} from 'types/notification'; import ServerViewState from 'app/serverViewState'; @@ -92,7 +91,7 @@ export function handleWelcomeScreenModal() { const html = getLocalURLString('welcomeScreen.html'); - const preload = getLocalPreload('desktopAPI.js'); + const preload = getLocalPreload('internalAPI.js'); const mainWindow = MainWindow.get(); if (!mainWindow) { @@ -114,9 +113,9 @@ 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}); - NotificationManager.displayMention(title, body, channel, teamId, url, silent, event.sender, data); +export function handleMentionNotification(event: IpcMainEvent, 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); } export function handleOpenAppMenu() { diff --git a/src/main/authManager.ts b/src/main/authManager.ts index ab5ee6b2..f999e463 100644 --- a/src/main/authManager.ts +++ b/src/main/authManager.ts @@ -16,7 +16,7 @@ import MainWindow from 'main/windows/mainWindow'; import ViewManager from 'main/views/viewManager'; const log = new Logger('AuthManager'); -const preload = getLocalPreload('desktopAPI.js'); +const preload = getLocalPreload('internalAPI.js'); const loginModalHtml = getLocalURLString('loginModal.html'); const permissionModalHtml = getLocalURLString('permissionModal.html'); diff --git a/src/main/certificateManager.ts b/src/main/certificateManager.ts index ddc834ac..f3d6398d 100644 --- a/src/main/certificateManager.ts +++ b/src/main/certificateManager.ts @@ -12,7 +12,7 @@ import {getLocalURLString, getLocalPreload} from './utils'; import MainWindow from './windows/mainWindow'; const log = new Logger('CertificateManager'); -const preload = getLocalPreload('desktopAPI.js'); +const preload = getLocalPreload('internalAPI.js'); const html = getLocalURLString('certificateModal.html'); type CertificateModalResult = { diff --git a/src/main/notifications/Mention.ts b/src/main/notifications/Mention.ts index 378946b9..d83ce148 100644 --- a/src/main/notifications/Mention.ts +++ b/src/main/notifications/Mention.ts @@ -27,25 +27,25 @@ const DEFAULT_WIN7 = 'Ding'; export class Mention extends Notification { customSound: string; - channel: {id: string}; // TODO: Channel from mattermost-redux + channelId: string; teamId: string; uId: string; - constructor(customOptions: MentionOptions, channel: {id: string}, teamId: string) { + constructor(customOptions: MentionOptions, channelId: string, teamId: string) { const options = {...defaultOptions, ...customOptions}; if (process.platform === 'darwin' || (process.platform === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '10.0'))) { // Notification Center shows app's icon, so there were two icons on the notification. Reflect.deleteProperty(options, 'icon'); } const isWin7 = (process.platform === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.3') && DEFAULT_WIN7); - const customSound = String(!options.silent && ((options.data && options.data.soundName !== 'None' && options.data.soundName) || isWin7)); + const customSound = String(!options.silent && ((options.soundName !== 'None' && options.soundName) || isWin7)); if (customSound) { options.silent = true; } super(options); this.customSound = customSound; - this.channel = channel; + this.channelId = channelId; this.teamId = teamId; this.uId = uuid(); } diff --git a/src/main/notifications/index.test.ts b/src/main/notifications/index.test.ts index 1741fc6c..d0f2f8ea 100644 --- a/src/main/notifications/index.test.ts +++ b/src/main/notifications/index.test.ts @@ -92,6 +92,7 @@ jest.mock('../views/viewManager', () => ({ name: 'server_name', url: new URL('http://someurl.com'), }, + shouldNotify: true, }, }), showById: jest.fn(), @@ -144,12 +145,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'test', 'test body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1} as WebContents, - {soundName: ''}, + '', ); expect(MainWindow.show).not.toBeCalled(); }); @@ -164,12 +165,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'test', 'test body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1} as WebContents, - {soundName: ''}, + '', ); expect(MainWindow.show).not.toBeCalled(); @@ -188,12 +189,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'test', 'test body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1} as WebContents, - {soundName: ''}, + '', ); expect(MainWindow.show).not.toBeCalled(); @@ -207,12 +208,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'test', 'test body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1} as WebContents, - {soundName: ''}, + '', ); expect(MainWindow.show).not.toBeCalled(); }); @@ -221,12 +222,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'test', 'test body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1} as WebContents, - {soundName: 'test_sound'}, + 'test_sound', ); expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound'); }); @@ -240,12 +241,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'test', 'test body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1} as WebContents, - {soundName: ''}, + '', ); // convert to any to access private field @@ -257,12 +258,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'test', 'test body 2', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1} as WebContents, - {soundName: ''}, + '', ); expect(mentionsPerChannel.delete).toHaveBeenCalled(); @@ -277,12 +278,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'click_test', 'mention_click_body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1, send: jest.fn()} as unknown as WebContents, - {soundName: ''}, + '', ); const mention = mentions.find((m) => m.body === 'mention_click_body'); mention?.value.click(); @@ -298,12 +299,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'click_test', 'mention_click_body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1, send: jest.fn()} as unknown as WebContents, - {soundName: ''}, + '', ); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -324,12 +325,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'click_test', 'mention_click_body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1, send: jest.fn()} as unknown as WebContents, - {soundName: ''}, + '', ); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -345,12 +346,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'click_test', 'mention_click_body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1, send: jest.fn()} as unknown as WebContents, - {soundName: ''}, + '', ); Object.defineProperty(process, 'platform', { value: originalPlatform, @@ -371,12 +372,12 @@ describe('main/notifications', () => { await NotificationManager.displayMention( 'click_test', 'mention_click_body', - {id: 'channel_id'}, + 'channel_id', 'team_id', 'http://server-1.com/team_id/channel_id', false, {id: 1, send: jest.fn()} as unknown as WebContents, - {soundName: ''}, + '', ); Object.defineProperty(process, 'platform', { value: originalPlatform, diff --git a/src/main/notifications/index.ts b/src/main/notifications/index.ts index 8c4c6666..9ffda75d 100644 --- a/src/main/notifications/index.ts +++ b/src/main/notifications/index.ts @@ -5,10 +5,8 @@ import {app, shell, Notification} from 'electron'; import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state'; -import {MentionData} from 'types/notification'; - import Config from 'common/config'; -import {PLAY_SOUND} from 'common/communication'; +import {PLAY_SOUND, NOTIFICATION_CLICKED} from 'common/communication'; import {Logger} from 'common/log'; import PermissionsManager from '../permissionsManager'; @@ -29,8 +27,8 @@ class NotificationManager { private upgradeNotification?: NewVersionNotification; private restartToUpgradeNotification?: UpgradeNotification; - 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}); + public async displayMention(title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, soundName: string) { + log.debug('displayMention', {title, body, channelId, teamId, url, silent, soundName}); if (!Notification.isSupported()) { log.error('notification not supported'); @@ -45,21 +43,24 @@ class NotificationManager { if (!view) { return; } + if (!view.view.shouldNotify) { + return; + } const serverName = view.view.server.name; const options = { title: `${serverName}: ${title}`, body, silent, - data, + soundName, }; if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) { return; } - const mention = new Mention(options, channel, teamId); - const mentionKey = `${mention.teamId}:${mention.channel.id}`; + const mention = new Mention(options, channelId, teamId); + const mentionKey = `${mention.teamId}:${mention.channelId}`; this.allActiveNotifications.set(mention.uId, mention); mention.on('show', () => { @@ -88,7 +89,7 @@ class NotificationManager { MainWindow.show(); if (serverName) { ViewManager.showById(view.id); - webcontents.send('notification-clicked', {channel, teamId, url}); + webcontents.send(NOTIFICATION_CLICKED, channelId, teamId, url); } }); diff --git a/src/main/preload/callsWidget.js b/src/main/preload/callsWidget.js deleted file mode 100644 index dd895484..00000000 --- a/src/main/preload/callsWidget.js +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -'use strict'; - -import {ipcRenderer} from 'electron'; - -import { - CALLS_LEAVE_CALL, - CALLS_JOINED_CALL, - CALLS_POPOUT_FOCUS, - CALLS_WIDGET_RESIZE, - CALLS_WIDGET_SHARE_SCREEN, - CALLS_WIDGET_CHANNEL_LINK_CLICK, - CALLS_ERROR, - DESKTOP_SOURCES_RESULT, - DESKTOP_SOURCES_MODAL_REQUEST, - CALLS_LINK_CLICK, - CALLS_JOIN_REQUEST, -} from 'common/communication'; - -// -// Handle messages FROM the widget. (i.e., widget's webapp -> widget's window) -// -window.addEventListener('message', ({origin, data = {}} = {}) => { - const {type, message = {}} = data; - - if (origin !== window.location.origin) { - return; - } - - switch (type) { - case 'get-app-version': { - ipcRenderer.invoke('get-app-version').then(({name, version}) => { - window.postMessage( - { - type: 'register-desktop', - message: { - name, - version, - }, - }, - window.location.origin, - ); - }); - break; - } - case DESKTOP_SOURCES_MODAL_REQUEST: - case CALLS_WIDGET_CHANNEL_LINK_CLICK: - case CALLS_LINK_CLICK: - case CALLS_WIDGET_RESIZE: - case CALLS_JOINED_CALL: - case CALLS_POPOUT_FOCUS: - case CALLS_ERROR: - case CALLS_LEAVE_CALL: - case CALLS_JOIN_REQUEST: { - ipcRenderer.send(type, 'widget', message); - break; - } - } -}); - -// -// Handle messages TO the widget. -// -ipcRenderer.on(DESKTOP_SOURCES_RESULT, (event, sources) => { - window.postMessage( - { - type: DESKTOP_SOURCES_RESULT, - message: sources, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(CALLS_WIDGET_SHARE_SCREEN, (event, message) => { - window.postMessage( - { - type: CALLS_WIDGET_SHARE_SCREEN, - message, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(CALLS_ERROR, (event, message) => { - window.postMessage( - { - type: CALLS_ERROR, - message, - }, - window.location.origin, - ); -}); diff --git a/src/main/preload/externalAPI.ts b/src/main/preload/externalAPI.ts new file mode 100644 index 00000000..9273ddca --- /dev/null +++ b/src/main/preload/externalAPI.ts @@ -0,0 +1,470 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {IpcRendererEvent, contextBridge, ipcRenderer, webFrame} from 'electron'; + +import {ExternalAPI} from 'types/externalAPI'; + +import {DesktopAPI} from '@mattermost/desktop-api'; + +import { + NOTIFY_MENTION, + IS_UNREAD, + UNREAD_RESULT, + SESSION_EXPIRED, + REACT_APP_INITIALIZED, + USER_ACTIVITY_UPDATE, + BROWSER_HISTORY_PUSH, + APP_LOGGED_IN, + APP_LOGGED_OUT, + GET_VIEW_INFO_FOR_TEST, + DESKTOP_SOURCES_RESULT, + VIEW_FINISHED_RESIZING, + CALLS_JOIN_CALL, + CALLS_JOINED_CALL, + CALLS_LEAVE_CALL, + DESKTOP_SOURCES_MODAL_REQUEST, + CALLS_WIDGET_SHARE_SCREEN, + CALLS_ERROR, + CALLS_JOIN_REQUEST, + GET_IS_DEV_MODE, + TOGGLE_SECURE_INPUT, + GET_APP_INFO, + REQUEST_BROWSER_HISTORY_STATUS, + BROWSER_HISTORY_STATUS_UPDATED, + NOTIFICATION_CLICKED, + CALLS_WIDGET_RESIZE, + CALLS_WIDGET_CHANNEL_LINK_CLICK, + CALLS_LINK_CLICK, + CALLS_POPOUT_FOCUS, + GET_DESKTOP_SOURCES, + UNREADS_AND_MENTIONS, + LEGACY_OFF, +} from 'common/communication'; + +const createListener: ExternalAPI['createListener'] = (channel: string, listener: (...args: never[]) => void) => { + const listenerWithEvent = (_: IpcRendererEvent, ...args: unknown[]) => + listener(...args as never[]); + ipcRenderer.on(channel, listenerWithEvent); + return () => { + ipcRenderer.off(channel, listenerWithEvent); + }; +}; + +const desktopAPI: DesktopAPI = { + + // Initialization + isDev: () => ipcRenderer.invoke(GET_IS_DEV_MODE), + getAppInfo: () => { + // Using this signal as the sign to disable the legacy code, since it is run before the app is rendered + if (legacyEnabled) { + legacyOff(); + } + + return ipcRenderer.invoke(GET_APP_INFO); + }, + reactAppInitialized: () => ipcRenderer.send(REACT_APP_INITIALIZED), + + // Session + setSessionExpired: (isExpired) => ipcRenderer.send(SESSION_EXPIRED, isExpired), + onUserActivityUpdate: (listener) => createListener(USER_ACTIVITY_UPDATE, listener), + + // Unreads/mentions/notifications + sendNotification: (title, body, channelId, teamId, url, silent, soundName) => + ipcRenderer.send(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), + + // Navigation + requestBrowserHistoryStatus: () => ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS), + onBrowserHistoryStatusUpdated: (listener) => createListener(BROWSER_HISTORY_STATUS_UPDATED, listener), + onBrowserHistoryPush: (listener) => createListener(BROWSER_HISTORY_PUSH, listener), + sendBrowserHistoryPush: (path) => ipcRenderer.send(BROWSER_HISTORY_PUSH, path), + + // Calls widget + openLinkFromCallsWidget: (url) => ipcRenderer.send(CALLS_LINK_CLICK, url), + openScreenShareModal: () => ipcRenderer.send(DESKTOP_SOURCES_MODAL_REQUEST), + onScreenShared: (listener) => createListener(CALLS_WIDGET_SHARE_SCREEN, listener), + callsWidgetConnected: (callID, sessionID) => ipcRenderer.send(CALLS_JOINED_CALL, callID, sessionID), + onJoinCallRequest: (listener) => createListener(CALLS_JOIN_REQUEST, listener), + resizeCallsWidget: (width, height) => ipcRenderer.send(CALLS_WIDGET_RESIZE, width, height), + focusPopout: () => ipcRenderer.send(CALLS_POPOUT_FOCUS), + leaveCall: () => ipcRenderer.send(CALLS_LEAVE_CALL), + sendCallsError: (error) => ipcRenderer.send(CALLS_ERROR, error), + + // Calls plugin + getDesktopSources: (opts) => ipcRenderer.invoke(GET_DESKTOP_SOURCES, opts), + onOpenScreenShareModal: (listener) => createListener(DESKTOP_SOURCES_MODAL_REQUEST, listener), + shareScreen: (sourceID, withAudio) => ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio), + joinCall: (opts) => ipcRenderer.invoke(CALLS_JOIN_CALL, opts), + sendJoinCallRequest: (callId) => ipcRenderer.send(CALLS_JOIN_REQUEST, callId), + onCallsError: (listener) => createListener(CALLS_ERROR, listener), + + // Utility + unregister: (channel) => ipcRenderer.removeAllListeners(channel), +}; +contextBridge.exposeInMainWorld('desktopAPI', desktopAPI); + +// Specific info for the testing environment +if (process.env.NODE_ENV === 'test') { + contextBridge.exposeInMainWorld('testHelper', { + getViewInfoForTest: () => ipcRenderer.invoke(GET_VIEW_INFO_FOR_TEST), + }); +} + +/**************************************************************************** + * window/document listeners + * These are here to perform specific tasks when global window or document events happen + * Avoid using these unless absolutely necessary + **************************************************************************** + */ + +// Let the main process know when the window has finished resizing +// This is to reduce the amount of white box that happens when expand the BrowserView +window.addEventListener('resize', () => { + ipcRenderer.send(VIEW_FINISHED_RESIZING); +}); + +// Enable secure input on macOS clients when the user is on a password input +let isPasswordBox = false; +const shouldSecureInput = (element: {tagName?: string; type?: string} | null, force = false) => { + const targetIsPasswordBox = (element && element.tagName === 'INPUT' && element.type === 'password'); + if (targetIsPasswordBox && (!isPasswordBox || force)) { + ipcRenderer.send(TOGGLE_SECURE_INPUT, true); + } else if (!targetIsPasswordBox && (isPasswordBox || force)) { + ipcRenderer.send(TOGGLE_SECURE_INPUT, false); + } + + isPasswordBox = Boolean(targetIsPasswordBox); +}; +window.addEventListener('focusin', (event) => { + shouldSecureInput(event.target as Element); +}); +window.addEventListener('focus', () => { + shouldSecureInput(document.activeElement, true); +}); + +// exit fullscreen embedded elements like youtube - https://mattermost.atlassian.net/browse/MM-19226 +ipcRenderer.on('exit-fullscreen', () => { + if (document.fullscreenElement && document.fullscreenElement.nodeName.toLowerCase() === 'iframe') { + document.exitFullscreen(); + } +}); + +// mattermost-webapp is SPA. So cache is not cleared due to no navigation. +// We needed to manually clear cache to free memory in long-term-use. +// http://seenaburns.com/debugging-electron-memory-usage/ +const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours +setInterval(() => { + webFrame.clearCache(); +}, CLEAR_CACHE_INTERVAL); + +/**************************************************************************** + * LEGACY CODE BELOW + * All of this code is deprecated and should be removed eventually + * Current it is there to support older versions of the web app + **************************************************************************** + */ + +/** + * Legacy helper functions + */ + +const onLoad = () => { + if (document.getElementById('root') === null) { + console.log('The guest is not assumed as mattermost-webapp'); + return; + } + watchReactAppUntilInitialized(() => { + console.log('Legacy preload initialized'); + ipcRenderer.send(REACT_APP_INITIALIZED); + ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS).then(sendHistoryButtonReturn); + }); +}; + +const onStorageChanged = (e: StorageEvent) => { + if (e.key === '__login__' && e.storageArea === localStorage && e.newValue) { + ipcRenderer.send(APP_LOGGED_IN); + } + if (e.key === '__logout__' && e.storageArea === localStorage && e.newValue) { + ipcRenderer.send(APP_LOGGED_OUT); + } +}; + +const isReactAppInitialized = () => { + const initializedRoot = + document.querySelector('#root.channel-view') || // React 16 webapp + document.querySelector('#root .signup-team__container') || // React 16 login + document.querySelector('div[data-reactroot]'); // Older React apps + if (initializedRoot === null) { + return false; + } + return initializedRoot.children.length !== 0; +}; + +const watchReactAppUntilInitialized = (callback: () => void) => { + let count = 0; + const interval = 500; + const timeout = 30000; + const timer = setInterval(() => { + count += interval; + if (isReactAppInitialized() || count >= timeout) { // assumed as webapp has been initialized. + clearTimeout(timer); + callback(); + } + }, interval); +}; + +const checkUnread = () => { + if (isReactAppInitialized()) { + findUnread(); + } else { + watchReactAppUntilInitialized(() => { + findUnread(); + }); + } +}; + +const findUnread = () => { + const classes = ['team-container unread', 'SidebarChannel unread', 'sidebar-item unread-title']; + const isUnread = classes.some((classPair) => { + const result = document.getElementsByClassName(classPair); + return result && result.length > 0; + }); + ipcRenderer.send(UNREAD_RESULT, isUnread); +}; + +let sessionExpired: boolean; +const getUnreadCount = () => { + // LHS not found => Log out => Count should be 0, but session may be expired. + let isExpired; + if (document.getElementById('sidebar-left') === null) { + const extraParam = (new URLSearchParams(window.location.search)).get('extra'); + isExpired = extraParam === 'expired'; + } else { + isExpired = false; + } + if (isExpired !== sessionExpired) { + sessionExpired = isExpired; + ipcRenderer.send(SESSION_EXPIRED, sessionExpired); + } +}; + +/** + * Legacy message passing code - can be running alongside the new API stuff + */ + +// Disabling no-explicit-any for this legacy code +// eslint-disable-next-line @typescript-eslint/no-explicit-any +window.addEventListener('message', ({origin, data = {}}: {origin?: string; data?: {type?: string; message?: any}} = {}) => { + const {type, message = {}} = data; + if (origin !== window.location.origin) { + return; + } + switch (type) { + case 'webapp-ready': + case 'get-app-version': { + // register with the webapp to enable custom integration functionality + ipcRenderer.invoke(GET_APP_INFO).then((info) => { + console.log(`registering ${info.name} v${info.version} with the server`); + window.postMessage( + { + type: 'register-desktop', + message: info, + }, + window.location.origin || '*', + ); + }); + break; + } + 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); + break; + } + case BROWSER_HISTORY_PUSH: { + const {path} = message as {path: string}; + ipcRenderer.send(BROWSER_HISTORY_PUSH, path); + break; + } + case 'history-button': { + ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS).then(sendHistoryButtonReturn); + break; + } + case CALLS_LINK_CLICK: { + ipcRenderer.send(CALLS_LINK_CLICK, message.link); + break; + } + case GET_DESKTOP_SOURCES: { + ipcRenderer.invoke(GET_DESKTOP_SOURCES, message).then(sendDesktopSourcesResult); + break; + } + case CALLS_WIDGET_SHARE_SCREEN: { + ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, message.sourceID, message.withAudio); + break; + } + case CALLS_JOIN_CALL: { + ipcRenderer.invoke(CALLS_JOIN_CALL, message).then(sendCallsJoinedCall); + break; + } + case CALLS_JOINED_CALL: { + ipcRenderer.send(CALLS_JOINED_CALL, message.callID, message.sessionID); + break; + } + case CALLS_JOIN_REQUEST: { + ipcRenderer.send(CALLS_JOIN_REQUEST, message.callID); + break; + } + case CALLS_WIDGET_RESIZE: { + ipcRenderer.send(CALLS_WIDGET_RESIZE, message.width, message.height); + break; + } + case CALLS_ERROR: { + ipcRenderer.send(CALLS_ERROR, message.err, message.callID, message.errMsg); + break; + } + case CALLS_WIDGET_CHANNEL_LINK_CLICK: + case CALLS_LEAVE_CALL: + case DESKTOP_SOURCES_MODAL_REQUEST: + case CALLS_POPOUT_FOCUS: { + ipcRenderer.send(type); + } + } +}); + +// Legacy support to hold the full channel object so that it can be used for the click event +const channels: Map = new Map(); +ipcRenderer.on(NOTIFICATION_CLICKED, (event, channelId, teamId, url) => { + const channel = channels.get(channelId) ?? {id: channelId}; + channels.delete(channelId); + window.postMessage( + { + type: NOTIFICATION_CLICKED, + message: { + channel, + teamId, + url, + }, + }, + window.location.origin, + ); +}); + +ipcRenderer.on(BROWSER_HISTORY_PUSH, (event, pathName) => { + window.postMessage( + { + type: 'browser-history-push-return', + message: { + pathName, + }, + }, + window.location.origin, + ); +}); + +const sendHistoryButtonReturn = (status: {canGoBack: boolean; canGoForward: boolean}) => { + window.postMessage( + { + type: 'history-button-return', + message: { + enableBack: status.canGoBack, + enableForward: status.canGoForward, + }, + }, + window.location.origin, + ); +}; + +ipcRenderer.on(BROWSER_HISTORY_STATUS_UPDATED, (event, canGoBack, canGoForward) => sendHistoryButtonReturn({canGoBack, canGoForward})); + +const sendDesktopSourcesResult = (sources: Array<{ + id: string; + name: string; + thumbnailURL: string; +}>) => { + window.postMessage( + { + type: DESKTOP_SOURCES_RESULT, + message: sources, + }, + window.location.origin, + ); +}; + +const sendCallsJoinedCall = (message: {callID: string; sessionID: string}) => { + window.postMessage( + { + type: CALLS_JOINED_CALL, + message, + }, + window.location.origin, + ); +}; + +ipcRenderer.on(CALLS_JOIN_REQUEST, (_, callID) => { + window.postMessage( + { + type: CALLS_JOIN_REQUEST, + message: {callID}, + }, + window.location.origin, + ); +}); + +ipcRenderer.on(DESKTOP_SOURCES_MODAL_REQUEST, () => { + window.postMessage( + { + type: DESKTOP_SOURCES_MODAL_REQUEST, + }, + window.location.origin, + ); +}); + +ipcRenderer.on(CALLS_WIDGET_SHARE_SCREEN, (_, sourceID, withAudio) => { + window.postMessage( + { + type: CALLS_WIDGET_SHARE_SCREEN, + message: {sourceID, withAudio}, + }, + window.location.origin, + ); +}); + +ipcRenderer.on(CALLS_ERROR, (_, err, callID, errMsg) => { + window.postMessage( + { + type: CALLS_ERROR, + message: {err, callID, errMsg}, + }, + window.location.origin, + ); +}); + +// push user activity updates to the webapp +ipcRenderer.on(USER_ACTIVITY_UPDATE, (event, userIsActive, isSystemEvent) => { + if (window.location.origin !== 'null') { + window.postMessage({type: USER_ACTIVITY_UPDATE, message: {userIsActive, manual: isSystemEvent}}, window.location.origin); + } +}); + +/** + * Legacy functionality that needs to be disabled with the new API + */ + +let legacyEnabled = true; +ipcRenderer.on(IS_UNREAD, checkUnread); +const unreadInterval = setInterval(getUnreadCount, 1000); +window.addEventListener('storage', onStorageChanged); +window.addEventListener('load', onLoad); + +function legacyOff() { + ipcRenderer.send(LEGACY_OFF); + ipcRenderer.off(IS_UNREAD, checkUnread); + clearInterval(unreadInterval); + window.removeEventListener('storage', onStorageChanged); + window.removeEventListener('load', onLoad); + + legacyEnabled = false; + console.log('New API preload initialized'); +} diff --git a/src/main/preload/desktopAPI.js b/src/main/preload/internalAPI.js similarity index 99% rename from src/main/preload/desktopAPI.js rename to src/main/preload/internalAPI.js index 0692d9a8..6b759b24 100644 --- a/src/main/preload/desktopAPI.js +++ b/src/main/preload/internalAPI.js @@ -89,6 +89,7 @@ import { GET_ORDERED_TABS_FOR_SERVER, SERVERS_UPDATE, VALIDATE_SERVER_URL, + GET_APP_INFO, } from 'common/communication'; console.log('Preload initialized'); @@ -139,7 +140,7 @@ contextBridge.exposeInMainWorld('desktop', { validateServerURL: (url, currentId) => ipcRenderer.invoke(VALIDATE_SERVER_URL, url, currentId), getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION), - getVersion: () => ipcRenderer.invoke('get-app-version'), + getVersion: () => ipcRenderer.invoke(GET_APP_INFO), getDarkMode: () => ipcRenderer.invoke(GET_DARK_MODE), requestHasDownloads: () => ipcRenderer.invoke(REQUEST_HAS_DOWNLOADS), getFullScreenStatus: () => ipcRenderer.invoke(GET_FULL_SCREEN_STATUS), diff --git a/src/main/preload/mattermost.js b/src/main/preload/mattermost.js deleted file mode 100644 index d8b54da6..00000000 --- a/src/main/preload/mattermost.js +++ /dev/null @@ -1,396 +0,0 @@ -// Copyright (c) 2015-2016 Yuya Ochiai -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -'use strict'; - -/* eslint-disable no-magic-numbers */ - -import {contextBridge, ipcRenderer, webFrame} from 'electron'; - -// I've filed an issue in electron-log https://github.com/megahertz/electron-log/issues/267 -// we'll be able to use it again if there is a workaround for the 'os' import -//import log from 'electron-log'; - -import { - NOTIFY_MENTION, - IS_UNREAD, - UNREAD_RESULT, - SESSION_EXPIRED, - SET_VIEW_OPTIONS, - REACT_APP_INITIALIZED, - USER_ACTIVITY_UPDATE, - CLOSE_SERVERS_DROPDOWN, - BROWSER_HISTORY_BUTTON, - BROWSER_HISTORY_PUSH, - APP_LOGGED_IN, - APP_LOGGED_OUT, - GET_VIEW_INFO_FOR_TEST, - DISPATCH_GET_DESKTOP_SOURCES, - DESKTOP_SOURCES_RESULT, - VIEW_FINISHED_RESIZING, - CALLS_JOIN_CALL, - CALLS_JOINED_CALL, - CALLS_LEAVE_CALL, - DESKTOP_SOURCES_MODAL_REQUEST, - CALLS_WIDGET_SHARE_SCREEN, - CLOSE_DOWNLOADS_DROPDOWN, - CALLS_ERROR, - CALLS_JOIN_REQUEST, - GET_IS_DEV_MODE, - TOGGLE_SECURE_INPUT, -} from 'common/communication'; - -const UNREAD_COUNT_INTERVAL = 1000; -const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours - -let appVersion; -let appName; -let sessionExpired; -let viewId; -let shouldSendNotifications; - -console.log('Preload initialized'); - -if (process.env.NODE_ENV === 'test') { - contextBridge.exposeInMainWorld('testHelper', { - getViewInfoForTest: () => ipcRenderer.invoke(GET_VIEW_INFO_FOR_TEST), - }); -} - -contextBridge.exposeInMainWorld('desktopAPI', { - isDev: () => ipcRenderer.invoke(GET_IS_DEV_MODE), -}); - -ipcRenderer.invoke('get-app-version').then(({name, version}) => { - appVersion = version; - appName = name; -}); - -function isReactAppInitialized() { - const initializedRoot = - document.querySelector('#root.channel-view') || // React 16 webapp - document.querySelector('#root .signup-team__container') || // React 16 login - document.querySelector('div[data-reactroot]'); // Older React apps - if (initializedRoot === null) { - return false; - } - return initializedRoot.children.length !== 0; -} - -function watchReactAppUntilInitialized(callback) { - let count = 0; - const interval = 500; - const timeout = 30000; - const timer = setInterval(() => { - count += interval; - if (isReactAppInitialized() || count >= timeout) { // assumed as webapp has been initialized. - clearTimeout(timer); - callback(); - } - }, interval); -} - -window.addEventListener('load', () => { - if (document.getElementById('root') === null) { - console.log('The guest is not assumed as mattermost-webapp'); - return; - } - watchReactAppUntilInitialized(() => { - ipcRenderer.send(REACT_APP_INITIALIZED, viewId); - ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewId); - }); -}); - -const parentTag = (target) => { - if (target.parentNode && target.parentNode.tagName) { - return target.parentNode.tagName.toUpperCase(); - } - return null; -}; - -document.addEventListener('mouseover', (event) => { - if (event.target && (event.target.tagName === 'A')) { - ipcRenderer.send('update-target-url', event.target.href); - } else if (event.target && (parentTag(event.target) === 'A')) { - ipcRenderer.send('update-target-url', event.target.parentNode.href); - } -}); - -document.addEventListener('mouseout', (event) => { - if (event.target && event.target.tagName === 'A') { - ipcRenderer.send('delete-target-url', event.target.href); - } -}); - -// listen for messages from the webapp -window.addEventListener('message', ({origin, data = {}} = {}) => { - const {type, message = {}} = data; - if (origin !== window.location.origin) { - return; - } - switch (type) { - case 'webapp-ready': { - // register with the webapp to enable custom integration functionality - console.log(`registering ${appName} v${appVersion} with the server`); - window.postMessage( - { - type: 'register-desktop', - message: { - version: appVersion, - name: appName, - }, - }, - window.location.origin || '*', - ); - break; - } - case 'register-desktop': - // it will be captured by itself too - break; - case 'dispatch-notification': { - if (shouldSendNotifications) { - const {title, body, channel, teamId, url, silent, data: messageData} = message; - ipcRenderer.send(NOTIFY_MENTION, title, body, channel, teamId, url, silent, messageData); - } - break; - } - case 'browser-history-push': { - const {path} = message; - ipcRenderer.send(BROWSER_HISTORY_PUSH, viewId, path); - break; - } - case 'history-button': { - ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewId); - break; - } - case 'get-desktop-sources': { - ipcRenderer.send(DISPATCH_GET_DESKTOP_SOURCES, viewId, message); - break; - } - case CALLS_JOIN_CALL: { - ipcRenderer.send(CALLS_JOIN_CALL, viewId, message); - break; - } - case CALLS_WIDGET_SHARE_SCREEN: { - ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, viewId, message); - break; - } - case CALLS_LEAVE_CALL: { - ipcRenderer.send(CALLS_LEAVE_CALL, viewId, message); - break; - } - } -}); - -const handleNotificationClick = ({channel, teamId, url}) => { - window.postMessage( - { - type: 'notification-clicked', - message: { - channel, - teamId, - url, - }, - }, - window.location.origin, - ); -}; - -ipcRenderer.on('notification-clicked', (event, data) => { - handleNotificationClick(data); -}); - -const findUnread = (favicon) => { - const classes = ['team-container unread', 'SidebarChannel unread', 'sidebar-item unread-title']; - const isUnread = classes.some((classPair) => { - const result = document.getElementsByClassName(classPair); - return result && result.length > 0; - }); - ipcRenderer.send(UNREAD_RESULT, favicon, viewId, isUnread); -}; - -ipcRenderer.on(IS_UNREAD, (event, favicon, server) => { - if (typeof viewId === 'undefined') { - viewId = server; - } - if (isReactAppInitialized()) { - findUnread(favicon); - } else { - watchReactAppUntilInitialized(() => { - findUnread(favicon); - }); - } -}); - -ipcRenderer.on(SET_VIEW_OPTIONS, (_, name, shouldNotify) => { - viewId = name; - shouldSendNotifications = shouldNotify; -}); - -function getUnreadCount() { - // LHS not found => Log out => Count should be 0, but session may be expired. - if (typeof viewId !== 'undefined') { - let isExpired; - if (document.getElementById('sidebar-left') === null) { - const extraParam = (new URLSearchParams(window.location.search)).get('extra'); - isExpired = extraParam === 'expired'; - } else { - isExpired = false; - } - if (isExpired !== sessionExpired) { - sessionExpired = isExpired; - ipcRenderer.send(SESSION_EXPIRED, sessionExpired, viewId); - } - } -} -setInterval(getUnreadCount, UNREAD_COUNT_INTERVAL); - -// push user activity updates to the webapp -ipcRenderer.on(USER_ACTIVITY_UPDATE, (event, {userIsActive, isSystemEvent}) => { - if (window.location.origin !== 'null') { - window.postMessage({type: USER_ACTIVITY_UPDATE, message: {userIsActive, manual: isSystemEvent}}, window.location.origin); - } -}); - -// exit fullscreen embedded elements like youtube - https://mattermost.atlassian.net/browse/MM-19226 -ipcRenderer.on('exit-fullscreen', () => { - if (document.fullscreenElement && document.fullscreenElement.nodeName.toLowerCase() === 'iframe') { - document.exitFullscreen(); - } -}); - -// mattermost-webapp is SPA. So cache is not cleared due to no navigation. -// We needed to manually clear cache to free memory in long-term-use. -// http://seenaburns.com/debugging-electron-memory-usage/ -setInterval(() => { - webFrame.clearCache(); -}, CLEAR_CACHE_INTERVAL); - -function isDownloadLink(el) { - if (typeof el !== 'object') { - return false; - } - const parentEl = el.parentElement; - if (typeof parentEl !== 'object') { - return el.className?.includes?.('download') || el.tagName?.toLowerCase?.() === 'svg'; - } - return el.closest('a[download]') !== null; -} - -window.addEventListener('click', (e) => { - ipcRenderer.send(CLOSE_SERVERS_DROPDOWN); - const el = e.target; - if (!isDownloadLink(el)) { - ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN); - } -}); - -ipcRenderer.on(BROWSER_HISTORY_PUSH, (event, pathName) => { - window.postMessage( - { - type: 'browser-history-push-return', - message: { - pathName, - }, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(BROWSER_HISTORY_BUTTON, (event, enableBack, enableForward) => { - window.postMessage( - { - type: 'history-button-return', - message: { - enableBack, - enableForward, - }, - }, - window.location.origin, - ); -}); - -window.addEventListener('storage', (e) => { - if (e.key === '__login__' && e.storageArea === localStorage && e.newValue) { - ipcRenderer.send(APP_LOGGED_IN, viewId); - } - if (e.key === '__logout__' && e.storageArea === localStorage && e.newValue) { - ipcRenderer.send(APP_LOGGED_OUT, viewId); - } -}); - -ipcRenderer.on(DESKTOP_SOURCES_RESULT, (event, sources) => { - window.postMessage( - { - type: 'desktop-sources-result', - message: sources, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(DESKTOP_SOURCES_MODAL_REQUEST, () => { - window.postMessage( - { - type: DESKTOP_SOURCES_MODAL_REQUEST, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(CALLS_JOINED_CALL, (event, message) => { - window.postMessage( - { - type: CALLS_JOINED_CALL, - message, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(CALLS_ERROR, (event, message) => { - window.postMessage( - { - type: CALLS_ERROR, - message, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(CALLS_JOIN_REQUEST, (event, message) => { - window.postMessage( - { - type: CALLS_JOIN_REQUEST, - message, - }, - window.location.origin, - ); -}); - -/* eslint-enable no-magic-numbers */ - -window.addEventListener('resize', () => { - ipcRenderer.send(VIEW_FINISHED_RESIZING); -}); - -let isPasswordBox = false; -const shouldSecureInput = (element, force = false) => { - const targetIsPasswordBox = (element && element.tagName === 'INPUT' && element.type === 'password'); - if (targetIsPasswordBox && (!isPasswordBox || force)) { - ipcRenderer.send(TOGGLE_SECURE_INPUT, true); - } else if (!targetIsPasswordBox && (isPasswordBox || force)) { - ipcRenderer.send(TOGGLE_SECURE_INPUT, false); - } - - isPasswordBox = targetIsPasswordBox; -}; - -window.addEventListener('focusin', (event) => { - shouldSecureInput(event.target); -}); - -window.addEventListener('focus', () => { - shouldSecureInput(document.activeElement, true); -}); diff --git a/src/main/views/MattermostBrowserView.test.js b/src/main/views/MattermostBrowserView.test.js index e44d77c7..f7096170 100644 --- a/src/main/views/MattermostBrowserView.test.js +++ b/src/main/views/MattermostBrowserView.test.js @@ -45,6 +45,7 @@ jest.mock('../windows/mainWindow', () => ({ jest.mock('common/appState', () => ({ clear: jest.fn(), updateMentions: jest.fn(), + updateExpired: jest.fn(), })); jest.mock('./webContentEvents', () => ({ addWebContentsEventListeners: jest.fn(), diff --git a/src/main/views/MattermostBrowserView.ts b/src/main/views/MattermostBrowserView.ts index 3cd1bca8..4149450b 100644 --- a/src/main/views/MattermostBrowserView.ts +++ b/src/main/views/MattermostBrowserView.ts @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {BrowserView, app} from 'electron'; +import {BrowserView, app, ipcMain} from 'electron'; import {BrowserViewConstructorOptions, Event, Input} from 'electron/main'; import {EventEmitter} from 'events'; @@ -15,10 +15,11 @@ import { UPDATE_TARGET_URL, IS_UNREAD, TOGGLE_BACK_BUTTON, - SET_VIEW_OPTIONS, LOADSCREEN_END, - BROWSER_HISTORY_BUTTON, SERVERS_URL_MODIFIED, + BROWSER_HISTORY_STATUS_UPDATED, + CLOSE_SERVERS_DROPDOWN, + CLOSE_DOWNLOADS_DROPDOWN, } from 'common/communication'; import ServerManager from 'common/servers/serverManager'; import {Logger} from 'common/log'; @@ -62,7 +63,7 @@ export class MattermostBrowserView extends EventEmitter { super(); this.view = view; - const preload = getLocalPreload('preload.js'); + const preload = getLocalPreload('externalAPI.js'); this.options = Object.assign({}, options); this.options.webPreferences = { preload, @@ -81,14 +82,21 @@ export class MattermostBrowserView extends EventEmitter { this.log = ServerManager.getViewLog(this.id, 'MattermostBrowserView'); this.log.verbose('View created'); - this.browserView.webContents.on('did-finish-load', this.handleDidFinishLoad); - this.browserView.webContents.on('page-title-updated', this.handleTitleUpdate); - this.browserView.webContents.on('page-favicon-updated', this.handleFaviconUpdate); this.browserView.webContents.on('update-target-url', this.handleUpdateTarget); this.browserView.webContents.on('did-navigate', this.handleDidNavigate); if (process.platform !== 'darwin') { this.browserView.webContents.on('before-input-event', this.handleInputEvents); } + this.browserView.webContents.on('input-event', (_, inputEvent) => { + if (inputEvent.type === 'mouseDown') { + ipcMain.emit(CLOSE_SERVERS_DROPDOWN); + ipcMain.emit(CLOSE_DOWNLOADS_DROPDOWN); + } + }); + + // Legacy handlers using the title/favicon + this.browserView.webContents.on('page-title-updated', this.handleTitleUpdate); + this.browserView.webContents.on('page-favicon-updated', this.handleFaviconUpdate); WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents); @@ -148,14 +156,23 @@ export class MattermostBrowserView extends EventEmitter { } } - updateHistoryButton = () => { + getBrowserHistoryStatus = () => { if (this.currentURL?.toString() === this.view.url.toString()) { this.browserView.webContents.clearHistory(); this.atRoot = true; } else { this.atRoot = false; } - this.browserView.webContents.send(BROWSER_HISTORY_BUTTON, this.browserView.webContents.canGoBack(), this.browserView.webContents.canGoForward()); + + return { + canGoBack: this.browserView.webContents.canGoBack(), + canGoForward: this.browserView.webContents.canGoForward(), + }; + } + + updateHistoryButton = () => { + const {canGoBack, canGoForward} = this.getBrowserHistoryStatus(); + this.browserView.webContents.send(BROWSER_HISTORY_STATUS_UPDATED, canGoBack, canGoForward); } load = (someURL?: URL | string) => { @@ -175,6 +192,7 @@ export class MattermostBrowserView extends EventEmitter { } else { loadURL = this.view.url.toString(); } + AppState.updateExpired(this.id, false); this.log.verbose(`Loading ${loadURL}`); const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()}); loading.then(this.loadSuccess(loadURL)).catch((err) => { @@ -257,6 +275,15 @@ export class MattermostBrowserView extends EventEmitter { } } + /** + * Code to turn off the old method of getting unreads + * Newer web apps will send the mentions/unreads directly + */ + offLegacyUnreads = () => { + this.browserView.webContents.off('page-title-updated', this.handleTitleUpdate); + this.browserView.webContents.off('page-favicon-updated', this.handleFaviconUpdate); + } + /** * Status hooks */ @@ -463,26 +490,6 @@ export class MattermostBrowserView extends EventEmitter { * WebContents event handlers */ - private handleDidFinishLoad = () => { - this.log.debug('did-finish-load'); - - // wait for screen to truly finish loading before sending the message down - const timeout = setInterval(() => { - if (!this.browserView.webContents) { - return; - } - - if (!this.browserView.webContents.isLoading()) { - try { - this.browserView.webContents.send(SET_VIEW_OPTIONS, this.id, this.view.shouldNotify); - clearTimeout(timeout); - } catch (e) { - this.log.error('failed to send view options to view'); - } - } - }, 100); - } - private handleDidNavigate = (event: Event, url: string) => { this.log.debug('handleDidNavigate', url); @@ -507,7 +514,7 @@ export class MattermostBrowserView extends EventEmitter { } private handleUpdateTarget = (e: Event, url: string) => { - this.log.silly('handleUpdateTarget', url); + this.log.silly('handleUpdateTarget', e, url); const parsedURL = parseURL(url); if (parsedURL && isInternalURL(parsedURL, this.view.server.url)) { this.emit(UPDATE_TARGET_URL); diff --git a/src/main/views/downloadsDropdownMenuView.ts b/src/main/views/downloadsDropdownMenuView.ts index f8a44edf..dff6c9fa 100644 --- a/src/main/views/downloadsDropdownMenuView.ts +++ b/src/main/views/downloadsDropdownMenuView.ts @@ -65,7 +65,7 @@ export class DownloadsDropdownMenuView { } this.bounds = this.getBounds(this.windowBounds.width, DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT); - const preload = getLocalPreload('desktopAPI.js'); + const preload = getLocalPreload('internalAPI.js'); this.view = new BrowserView({webPreferences: { preload, @@ -83,7 +83,7 @@ export class DownloadsDropdownMenuView { * the downloads dropdown at the correct position */ private updateWindowBounds = (newBounds: Electron.Rectangle) => { - log.debug('updateWindowBounds'); + log.silly('updateWindowBounds'); this.windowBounds = newBounds; this.updateDownloadsDropdownMenu(); @@ -98,7 +98,7 @@ export class DownloadsDropdownMenuView { } private updateDownloadsDropdownMenu = () => { - log.debug('updateDownloadsDropdownMenu'); + log.silly('updateDownloadsDropdownMenu'); this.view?.webContents.send( UPDATE_DOWNLOADS_DROPDOWN_MENU, @@ -131,7 +131,7 @@ export class DownloadsDropdownMenuView { } private handleClose = () => { - log.debug('handleClose'); + log.silly('handleClose'); this.open = false; this.item = undefined; diff --git a/src/main/views/downloadsDropdownView.ts b/src/main/views/downloadsDropdownView.ts index d4fe6d4f..f7a2ae72 100644 --- a/src/main/views/downloadsDropdownView.ts +++ b/src/main/views/downloadsDropdownView.ts @@ -57,7 +57,7 @@ export class DownloadsDropdownView { } this.bounds = this.getBounds(this.windowBounds.width, DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT); - const preload = getLocalPreload('desktopAPI.js'); + const preload = getLocalPreload('internalAPI.js'); this.view = new BrowserView({webPreferences: { preload, @@ -77,7 +77,7 @@ export class DownloadsDropdownView { * the downloads dropdown at the correct position */ private updateWindowBounds = (newBounds: Electron.Rectangle) => { - log.debug('updateWindowBounds'); + log.silly('updateWindowBounds'); this.windowBounds = newBounds; this.updateDownloadsDropdown(); @@ -85,13 +85,13 @@ export class DownloadsDropdownView { } private updateDownloadsDropdownMenuItem = (event: IpcMainEvent, item?: DownloadedItem) => { - log.debug('updateDownloadsDropdownMenuItem', {item}); + log.silly('updateDownloadsDropdownMenuItem', {item}); this.item = item; this.updateDownloadsDropdown(); } private updateDownloadsDropdown = () => { - log.debug('updateDownloadsDropdown'); + log.silly('updateDownloadsDropdown'); this.view?.webContents.send( UPDATE_DOWNLOADS_DROPDOWN, @@ -117,7 +117,7 @@ export class DownloadsDropdownView { } private handleClose = () => { - log.debug('handleClose'); + log.silly('handleClose'); this.view?.setBounds(this.getBounds(this.windowBounds?.width ?? 0, 0, 0)); downloadsManager.onClose(); diff --git a/src/main/views/loadingScreen.ts b/src/main/views/loadingScreen.ts index 32b95c66..57896ea5 100644 --- a/src/main/views/loadingScreen.ts +++ b/src/main/views/loadingScreen.ts @@ -77,7 +77,7 @@ export class LoadingScreen { } private create = () => { - const preload = getLocalPreload('desktopAPI.js'); + const preload = getLocalPreload('internalAPI.js'); this.view = new BrowserView({webPreferences: { preload, diff --git a/src/main/views/serverDropdownView.ts b/src/main/views/serverDropdownView.ts index f3d9751f..2e116ba3 100644 --- a/src/main/views/serverDropdownView.ts +++ b/src/main/views/serverDropdownView.ts @@ -75,7 +75,7 @@ export class ServerDropdownView { private init = () => { log.info('init'); - const preload = getLocalPreload('desktopAPI.js'); + const preload = getLocalPreload('internalAPI.js'); this.view = new BrowserView({webPreferences: { preload, @@ -144,7 +144,7 @@ export class ServerDropdownView { } private handleClose = () => { - log.debug('handleClose'); + log.silly('handleClose'); this.view?.setBounds(this.getBounds(0, 0)); MainWindow.sendToRenderer(CLOSE_SERVERS_DROPDOWN); diff --git a/src/main/views/viewManager.test.js b/src/main/views/viewManager.test.js index f2f01f6e..39fda0cf 100644 --- a/src/main/views/viewManager.test.js +++ b/src/main/views/viewManager.test.js @@ -407,6 +407,7 @@ describe('main/views/viewManager', () => { ]; const view1 = { id: 'server-1_view-messaging', + webContentsId: 1, isLoggedIn: true, view: { type: TAB_MESSAGING, @@ -415,10 +416,12 @@ describe('main/views/viewManager', () => { }, }, sendToRenderer: jest.fn(), + updateHistoryButton: jest.fn(), }; const view2 = { ...view1, id: 'server-1_other_type_1', + webContentsId: 2, view: { ...view1.view, type: 'other_type_1', @@ -427,6 +430,7 @@ describe('main/views/viewManager', () => { const view3 = { ...view1, id: 'server-1_other_type_2', + webContentsId: 3, view: { ...view1.view, type: 'other_type_2', @@ -442,6 +446,7 @@ describe('main/views/viewManager', () => { viewManager.getView = (viewId) => views.get(viewId); viewManager.isViewClosed = (viewId) => closedViews.has(viewId); viewManager.openClosedView = jest.fn(); + viewManager.getViewByWebContentsId = (webContentsId) => [...views.values()].find((view) => view.webContentsId === webContentsId); beforeEach(() => { ServerManager.getAllServers.mockReturnValue(servers); @@ -460,19 +465,19 @@ describe('main/views/viewManager', () => { views.set(name, view); }); ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_other_type_2'}); - viewManager.handleBrowserHistoryPush(null, 'server-1_view-messaging', '/other_type_2/subpath'); + viewManager.handleBrowserHistoryPush({sender: {id: 1}}, '/other_type_2/subpath'); expect(viewManager.openClosedView).toBeCalledWith('server-1_other_type_2', 'http://server-1.com/other_type_2/subpath'); }); it('should open redirect view if different from current view', () => { ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_other_type_1'}); - viewManager.handleBrowserHistoryPush(null, 'server-1_view-messaging', '/other_type_1/subpath'); + viewManager.handleBrowserHistoryPush({sender: {id: 1}}, '/other_type_1/subpath'); expect(viewManager.showById).toBeCalledWith('server-1_other_type_1'); }); it('should ignore redirects to "/" to Messages from other views', () => { ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_view-messaging'}); - viewManager.handleBrowserHistoryPush(null, 'server-1_other_type_1', '/'); + viewManager.handleBrowserHistoryPush({sender: {id: 2}}, '/'); expect(view1.sendToRenderer).not.toBeCalled(); }); }); diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index c2f27be8..8b54cb2d 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent, Event} from 'electron'; +import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron'; import isDev from 'electron-is-dev'; import ServerViewState from 'app/serverViewState'; @@ -19,7 +19,6 @@ import { UPDATE_URL_VIEW_WIDTH, SERVERS_UPDATE, REACT_APP_INITIALIZED, - BROWSER_HISTORY_BUTTON, APP_LOGGED_OUT, APP_LOGGED_IN, RELOAD_CURRENT_VIEW, @@ -32,6 +31,9 @@ import { MAIN_WINDOW_FOCUSED, SWITCH_TAB, GET_IS_DEV_MODE, + REQUEST_BROWSER_HISTORY_STATUS, + LEGACY_OFF, + UNREADS_AND_MENTIONS, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -70,15 +72,17 @@ export class ViewManager { MainWindow.on(MAIN_WINDOW_FOCUSED, this.focusCurrentView); ipcMain.handle(GET_VIEW_INFO_FOR_TEST, this.handleGetViewInfoForTest); ipcMain.handle(GET_IS_DEV_MODE, () => isDev); + ipcMain.handle(REQUEST_BROWSER_HISTORY_STATUS, this.handleRequestBrowserHistoryStatus); ipcMain.on(HISTORY, this.handleHistory); ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized); ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush); - ipcMain.on(BROWSER_HISTORY_BUTTON, this.handleBrowserHistoryButton); ipcMain.on(APP_LOGGED_IN, this.handleAppLoggedIn); ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut); ipcMain.on(RELOAD_CURRENT_VIEW, this.handleReloadCurrentView); - ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread); + ipcMain.on(UNREAD_RESULT, this.handleUnreadChanged); + ipcMain.on(UNREADS_AND_MENTIONS, this.handleUnreadsAndMentionsChanged); ipcMain.on(SESSION_EXPIRED, this.handleSessionExpired); + ipcMain.on(LEGACY_OFF, this.handleLegacyOff); ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId)); @@ -326,7 +330,7 @@ export class ViewManager { } if (url && url !== '') { const urlString = typeof url === 'string' ? url : url.toString(); - const preload = getLocalPreload('desktopAPI.js'); + const preload = getLocalPreload('internalAPI.js'); const urlView = new BrowserView({ webPreferences: { preload, @@ -470,18 +474,18 @@ export class ViewManager { this.getCurrentView()?.goToOffset(offset); } - private handleAppLoggedIn = (event: IpcMainEvent, viewId: string) => { - this.getView(viewId)?.onLogin(true); + private handleAppLoggedIn = (event: IpcMainEvent) => { + this.getViewByWebContentsId(event.sender.id)?.onLogin(true); } - private handleAppLoggedOut = (event: IpcMainEvent, viewId: string) => { - this.getView(viewId)?.onLogin(false); + private handleAppLoggedOut = (event: IpcMainEvent) => { + this.getViewByWebContentsId(event.sender.id)?.onLogin(false); } - private handleBrowserHistoryPush = (e: IpcMainEvent, viewId: string, pathName: string) => { - log.debug('handleBrowserHistoryPush', {viewId, pathName}); + private handleBrowserHistoryPush = (e: IpcMainEvent, pathName: string) => { + log.debug('handleBrowserHistoryPush', e.sender.id, pathName); - const currentView = this.getView(viewId); + const currentView = this.getViewByWebContentsId(e.sender.id); if (!currentView) { return; } @@ -489,7 +493,7 @@ export class ViewManager { if (currentView.view.server.url.pathname !== '/' && pathName.startsWith(currentView.view.server.url.pathname)) { cleanedPathName = pathName.replace(currentView.view.server.url.pathname, ''); } - const redirectedviewId = ServerManager.lookupViewByURL(`${currentView.view.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.id || viewId; + const redirectedviewId = ServerManager.lookupViewByURL(`${currentView.view.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.id || currentView.id; if (this.isViewClosed(redirectedviewId)) { // If it's a closed view, just open it and stop this.openClosedView(redirectedviewId, `${currentView.view.server.url}${cleanedPathName}`); @@ -497,8 +501,8 @@ export class ViewManager { } let redirectedView = this.getView(redirectedviewId) || currentView; if (redirectedView !== currentView && redirectedView?.view.server.id === ServerViewState.getCurrentServer().id && redirectedView?.isLoggedIn) { - log.info('redirecting to a new view', redirectedView?.id || viewId); - this.showById(redirectedView?.id || viewId); + log.info('redirecting to a new view', redirectedView?.id || currentView.id); + this.showById(redirectedView?.id || currentView.id); } else { redirectedView = currentView; } @@ -506,20 +510,20 @@ export class ViewManager { // Special case check for Channels to not force a redirect to "/", causing a refresh if (!(redirectedView !== currentView && redirectedView?.view.type === TAB_MESSAGING && cleanedPathName === '/')) { redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName); - if (redirectedView) { - this.handleBrowserHistoryButton(e, redirectedView.id); - } + redirectedView?.updateHistoryButton(); } } - private handleBrowserHistoryButton = (e: IpcMainEvent, viewId: string) => { - this.getView(viewId)?.updateHistoryButton(); + private handleRequestBrowserHistoryStatus = (e: IpcMainInvokeEvent) => { + log.silly('handleRequestBrowserHistoryStatus', e.sender.id); + + return this.getViewByWebContentsId(e.sender.id)?.getBrowserHistoryStatus(); } - private handleReactAppInitialized = (e: IpcMainEvent, viewId: string) => { - log.debug('handleReactAppInitialized', viewId); + private handleReactAppInitialized = (e: IpcMainEvent) => { + log.debug('handleReactAppInitialized', e.sender.id); - const view = this.views.get(viewId); + const view = this.getViewByWebContentsId(e.sender.id); if (view) { view.setInitialized(); if (this.getCurrentView() === view) { @@ -539,18 +543,47 @@ export class ViewManager { this.showById(view?.id); } - // if favicon is null, it means it is the initial load, - // so don't memoize as we don't have the favicons and there is no rush to find out. - private handleFaviconIsUnread = (e: Event, favicon: string, viewId: string, result: boolean) => { - log.silly('handleFaviconIsUnread', {favicon, viewId, result}); + private handleLegacyOff = (e: IpcMainEvent) => { + log.silly('handleLegacyOff', {webContentsId: e.sender.id}); - AppState.updateUnreads(viewId, result); + const view = this.getViewByWebContentsId(e.sender.id); + if (!view) { + return; + } + view.offLegacyUnreads(); } - private handleSessionExpired = (event: IpcMainEvent, isExpired: boolean, viewId: string) => { - ServerManager.getViewLog(viewId, 'ViewManager').debug('handleSessionExpired', isExpired); + // if favicon is null, it means it is the initial load, + // so don't memoize as we don't have the favicons and there is no rush to find out. + private handleUnreadChanged = (e: IpcMainEvent, result: boolean) => { + log.silly('handleUnreadChanged', {webContentsId: e.sender.id, result}); - AppState.updateExpired(viewId, isExpired); + const view = this.getViewByWebContentsId(e.sender.id); + if (!view) { + return; + } + AppState.updateUnreads(view.id, result); + } + + private handleUnreadsAndMentionsChanged = (e: IpcMainEvent, isUnread: boolean, mentionCount: number) => { + log.silly('handleUnreadsAndMentionsChanged', {webContentsId: e.sender.id, isUnread, mentionCount}); + + const view = this.getViewByWebContentsId(e.sender.id); + if (!view) { + return; + } + AppState.updateUnreads(view.id, isUnread); + AppState.updateMentions(view.id, mentionCount); + } + + private handleSessionExpired = (event: IpcMainEvent, isExpired: boolean) => { + const view = this.getViewByWebContentsId(event.sender.id); + if (!view) { + return; + } + ServerManager.getViewLog(view.id, 'ViewManager').debug('handleSessionExpired', isExpired); + + AppState.updateExpired(view.id, isExpired); } private handleSetCurrentViewBounds = (newBounds: Electron.Rectangle) => { diff --git a/src/main/views/webContentEvents.ts b/src/main/views/webContentEvents.ts index 26654d36..e6db3a65 100644 --- a/src/main/views/webContentEvents.ts +++ b/src/main/views/webContentEvents.ts @@ -14,6 +14,7 @@ import { isHelpUrl, isImageProxyUrl, isInternalURL, + isLoginUrl, isManagedResource, isPluginUrl, isPublicFilesUrl, @@ -91,7 +92,9 @@ export class WebContentsEventManager { const parsedURL = parseURL(url)!; const serverURL = this.getServerURLFromWebContentsId(webContentsId); - if (serverURL && (isTeamUrl(serverURL, parsedURL) || isAdminUrl(serverURL, parsedURL) || this.isTrustedPopupWindow(webContentsId))) { + this.log(webContentsId).info(serverURL?.toString()); + + if (serverURL && (isTeamUrl(serverURL, parsedURL) || isAdminUrl(serverURL, parsedURL) || isLoginUrl(serverURL, parsedURL) || this.isTrustedPopupWindow(webContentsId))) { return; } diff --git a/src/main/windows/callsWidgetWindow.test.js b/src/main/windows/callsWidgetWindow.test.js index eb400929..dcb9f54a 100644 --- a/src/main/windows/callsWidgetWindow.test.js +++ b/src/main/windows/callsWidgetWindow.test.js @@ -3,11 +3,11 @@ /* eslint-disable max-lines */ -import {BrowserWindow, desktopCapturer, systemPreferences} from 'electron'; +import {BrowserWindow, desktopCapturer, systemPreferences, ipcMain} from 'electron'; import ServerViewState from 'app/serverViewState'; -import {CALLS_WIDGET_SHARE_SCREEN, CALLS_JOINED_CALL, CALLS_JOIN_REQUEST} from 'common/communication'; +import {CALLS_WIDGET_SHARE_SCREEN} from 'common/communication'; import { MINIMUM_CALLS_WIDGET_WIDTH, MINIMUM_CALLS_WIDGET_HEIGHT, @@ -62,6 +62,7 @@ jest.mock('app/serverViewState', () => ({ })); jest.mock('main/views/viewManager', () => ({ getView: jest.fn(), + getViewByWebContentsId: jest.fn(), })); jest.mock('../utils', () => ({ openScreensharePermissionsSettingsMacOS: jest.fn(), @@ -187,11 +188,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should resize correctly', () => { callsWidgetWindow.handleResize({ sender: {id: 'windowID'}, - }, 'widget', { - element: 'calls-widget', - width: 300, - height: 100, - }); + }, 300, 100); expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({ x: 12, y: 720 - (100 - MINIMUM_CALLS_WIDGET_HEIGHT), @@ -204,11 +201,7 @@ describe('main/windows/callsWidgetWindow', () => { callsWidgetWindow.win.webContents.getZoomFactor.mockReturnValue(2.0); callsWidgetWindow.handleResize({ sender: {id: 'windowID'}, - }, 'widget', { - element: 'calls-widget', - width: 300, - height: 100, - }); + }, 300, 100); expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({ x: 12, y: 720 - (200 - MINIMUM_CALLS_WIDGET_HEIGHT), @@ -221,11 +214,7 @@ describe('main/windows/callsWidgetWindow', () => { callsWidgetWindow.win.webContents.getZoomFactor.mockReturnValue(0.5); callsWidgetWindow.handleResize({ sender: {id: 'windowID'}, - }, 'widget', { - element: 'calls-widget', - width: 300, - height: 100, - }); + }, 300, 100); expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({ x: 12, y: 720 - (50 - MINIMUM_CALLS_WIDGET_HEIGHT), @@ -288,51 +277,26 @@ describe('main/windows/callsWidgetWindow', () => { it('handleShareScreen', () => { const callsWidgetWindow = new CallsWidgetWindow(); callsWidgetWindow.isAllowedEvent = jest.fn(); + callsWidgetWindow.mainView = { + webContentsId: 'goodID', + }; callsWidgetWindow.win = { webContents: { - id: 'goodID', send: jest.fn(), }, }; - const message = { - callID: 'test-call-id', - }; callsWidgetWindow.isAllowedEvent.mockReturnValue(false); callsWidgetWindow.handleShareScreen({ sender: {id: 'badID'}, - }, message); + }, 'sourceId', true); expect(callsWidgetWindow.win.webContents.send).not.toHaveBeenCalled(); callsWidgetWindow.isAllowedEvent.mockReturnValue(true); callsWidgetWindow.handleShareScreen({ sender: {id: 'goodID'}, - }, 'widget', message); - expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith(CALLS_WIDGET_SHARE_SCREEN, message); - }); - - it('handleJoinedCall', () => { - const callsWidgetWindow = new CallsWidgetWindow(); - callsWidgetWindow.isAllowedEvent = jest.fn(); - callsWidgetWindow.mainView = { - webContentsId: 'goodID', - sendToRenderer: jest.fn(), - }; - const message = { - callID: 'test-call-id', - }; - - callsWidgetWindow.isAllowedEvent.mockReturnValue(false); - callsWidgetWindow.handleJoinedCall({ - sender: {id: 'badID'}, - }, 'widget', message); - expect(callsWidgetWindow.mainView.sendToRenderer).not.toHaveBeenCalled(); - - callsWidgetWindow.isAllowedEvent.mockReturnValue(true); - callsWidgetWindow.handleJoinedCall({ - sender: {id: 'goodID'}, - }, 'widget', message); - expect(callsWidgetWindow.mainView.sendToRenderer).toHaveBeenCalledWith(CALLS_JOINED_CALL, message); + }, 'sourceId', true); + expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith(CALLS_WIDGET_SHARE_SCREEN, 'sourceId', true); }); describe('onPopOutOpen', () => { @@ -451,42 +415,6 @@ describe('main/windows/callsWidgetWindow', () => { expect(callsWidgetWindow.getViewURL().toString()).toBe('http://localhost:8065/'); }); - describe('isAllowedEvent', () => { - const callsWidgetWindow = new CallsWidgetWindow(); - callsWidgetWindow.mainView = { - webContentsId: 'mainViewID', - }; - callsWidgetWindow.win = { - webContents: { - id: 'windowID', - }, - }; - - it('should not allow on unknown sender id', () => { - expect(callsWidgetWindow.isAllowedEvent({ - sender: { - id: 'senderID', - }, - })).toEqual(false); - }); - - it('should allow on attached browser view', () => { - expect(callsWidgetWindow.isAllowedEvent({ - sender: { - id: 'mainViewID', - }, - })).toEqual(true); - }); - - it('should allow on widget window', () => { - expect(callsWidgetWindow.isAllowedEvent({ - sender: { - id: 'windowID', - }, - })).toEqual(true); - }); - }); - it('onNavigate', () => { const callsWidgetWindow = new CallsWidgetWindow(); callsWidgetWindow.getWidgetURL = () => 'http://localhost:8065'; @@ -510,11 +438,12 @@ describe('main/windows/callsWidgetWindow', () => { url: new URL('http://server-1.com'), }, }, + webContentsId: 2, }; const browserWindow = { on: jest.fn(), once: jest.fn(), - loadURL: jest.fn().mockReturnValue(Promise.resolve()), + loadURL: jest.fn(), webContents: { setWindowOpenHandler: jest.fn(), on: jest.fn(), @@ -524,9 +453,18 @@ describe('main/windows/callsWidgetWindow', () => { }; beforeEach(() => { + let func; + ipcMain.on.mockImplementation((_, callback) => { + func = callback; + }); + browserWindow.loadURL.mockImplementation(() => { + func({sender: {id: 1}}, 'test'); + return Promise.resolve(); + }); BrowserWindow.mockReturnValue(browserWindow); callsWidgetWindow.close.mockReturnValue(Promise.resolve()); - ViewManager.getView.mockReturnValue(view); + callsWidgetWindow.getWidgetURL.mockReturnValue('http://server-1.com/widget'); + ViewManager.getViewByWebContentsId.mockReturnValue(view); }); afterEach(() => { @@ -539,12 +477,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should create calls widget window', async () => { expect(callsWidgetWindow.win).toBeUndefined(); - await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test'}); - expect(callsWidgetWindow.win).toBeDefined(); - }); - - it('should create with correct initial configuration', async () => { - await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test'}); + await callsWidgetWindow.handleCreateCallsWidgetWindow({sender: {id: 2}}, {callID: 'test'}); expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({ width: MINIMUM_CALLS_WIDGET_WIDTH, height: MINIMUM_CALLS_WIDGET_HEIGHT, @@ -556,6 +489,7 @@ describe('main/windows/callsWidgetWindow', () => { alwaysOnTop: true, backgroundColor: '#00ffffff', })); + expect(callsWidgetWindow.win).toBeDefined(); }); it('should catch error when failing to load the URL', async () => { @@ -570,18 +504,28 @@ describe('main/windows/callsWidgetWindow', () => { }); it('should not create a new window if call is the same', async () => { - const window = {webContents: {id: 2}}; + const window = {webContents: {id: 3}}; callsWidgetWindow.win = window; callsWidgetWindow.options = {callID: 'test'}; - await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test'}); + await callsWidgetWindow.handleCreateCallsWidgetWindow({sender: {id: 2}}, {callID: 'test'}); expect(callsWidgetWindow.win).toEqual(window); }); it('should create a new window if switching calls', async () => { - const window = {webContents: {id: 2}}; + let func; + ipcMain.on.mockImplementation((_, callback) => { + func = callback; + }); + browserWindow.loadURL.mockImplementation(() => { + func({sender: {id: 1}}, 'test2'); + return Promise.resolve(); + }); + BrowserWindow.mockReturnValue(browserWindow); + + const window = {webContents: {id: 3}}; callsWidgetWindow.win = window; - callsWidgetWindow.getCallID = jest.fn(() => 'test'); - await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test2'}); + callsWidgetWindow.options = {callID: 'test'}; + await callsWidgetWindow.handleCreateCallsWidgetWindow({sender: {id: 2}}, {callID: 'test2'}); expect(callsWidgetWindow.win).not.toEqual(window); }); }); @@ -627,10 +571,13 @@ describe('main/windows/callsWidgetWindow', () => { lastActiveView: 2, }, ]; + let index = 0; const map = servers.reduce((arr, item) => { item.views.forEach((view) => { + index++; arr.push([`${item.name}_${view.name}`, { sendToRenderer: jest.fn(), + webContentsId: index, }]); }); return arr; @@ -638,7 +585,8 @@ describe('main/windows/callsWidgetWindow', () => { const views = new Map(map); beforeEach(() => { - ViewManager.getView.mockImplementation((viewId) => views.get(viewId)); + ViewManager.getViewByWebContentsId.mockImplementation((id) => [...views.values()].find((view) => view.webContentsId === id)); + callsWidgetWindow.mainView = views.get('server-1_view-1'); }); afterEach(() => { @@ -662,9 +610,8 @@ describe('main/windows/callsWidgetWindow', () => { }, ]); - await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null); - - expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('desktop-sources-result', [ + const sources = await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null); + expect(sources).toEqual([ { id: 'screen0', }, @@ -676,11 +623,11 @@ describe('main/windows/callsWidgetWindow', () => { it('should send error with no sources', async () => { jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([]); - await callsWidgetWindow.handleGetDesktopSources('server-2_view-1', null); + await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null); expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { err: 'screen-permissions', }); - expect(views.get('server-2_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', { + expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', { err: 'screen-permissions', }); expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1); @@ -697,7 +644,7 @@ describe('main/windows/callsWidgetWindow', () => { ]); jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied'); - await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null); + await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null); expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen'); expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { @@ -726,7 +673,7 @@ describe('main/windows/callsWidgetWindow', () => { ]); jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied'); - await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null); + await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null); expect(callsWidgetWindow.missingScreensharePermissions).toBe(true); expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(1); @@ -738,7 +685,7 @@ describe('main/windows/callsWidgetWindow', () => { err: 'screen-permissions', }); - await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null); + await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null); expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(2); expect(openScreensharePermissionsSettingsMacOS).toHaveBeenCalledTimes(1); @@ -749,74 +696,9 @@ describe('main/windows/callsWidgetWindow', () => { }); }); - describe('handleDesktopSourcesModalRequest', () => { - const callsWidgetWindow = new CallsWidgetWindow(); - callsWidgetWindow.mainView = { - view: { - server: { - id: 'server-1', - }, - }, - sendToRenderer: jest.fn(), - }; - const servers = [ - { - name: 'server-1', - order: 1, - views: [ - { - name: 'view-1', - order: 0, - isOpen: false, - }, - { - name: 'view-2', - order: 2, - isOpen: true, - }, - ], - }, { - name: 'server-2', - order: 0, - views: [ - { - name: 'view-1', - order: 0, - isOpen: false, - }, - { - name: 'view-2', - order: 2, - isOpen: true, - }, - ], - lastActiveView: 2, - }, - ]; - const map = servers.reduce((arr, item) => { - item.views.forEach((view) => { - arr.push([`${item.name}_${view.name}`, {}]); - }); - return arr; - }, []); - const views = new Map(map); - - beforeEach(() => { - ViewManager.getView.mockImplementation((viewId) => views.get(viewId)); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should switch server', () => { - callsWidgetWindow.handleDesktopSourcesModalRequest(); - expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-1'); - }); - }); - describe('handleCallsWidgetChannelLinkClick', () => { const callsWidgetWindow = new CallsWidgetWindow(); + callsWidgetWindow.win = {webContents: {id: 1}}; callsWidgetWindow.mainView = { view: { server: { @@ -877,89 +759,12 @@ describe('main/windows/callsWidgetWindow', () => { }); it('should switch server', () => { - callsWidgetWindow.handleCallsWidgetChannelLinkClick(); + callsWidgetWindow.handleCallsWidgetChannelLinkClick({sender: {id: 1}}); expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-2'); }); }); - describe('handleCallsError', () => { - const callsWidgetWindow = new CallsWidgetWindow(); - callsWidgetWindow.mainView = { - view: { - server: { - id: 'server-2', - }, - }, - sendToRenderer: jest.fn(), - }; - const focus = jest.fn(); - - beforeEach(() => { - MainWindow.get.mockReturnValue({focus}); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should focus view and propagate error to main view', () => { - callsWidgetWindow.handleCallsError('', {err: 'client-error'}); - expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-2'); - expect(focus).toHaveBeenCalled(); - expect(callsWidgetWindow.mainView.sendToRenderer).toHaveBeenCalledWith('calls-error', {err: 'client-error'}); - }); - }); - - describe('handleCallsLinkClick', () => { - const view = { - view: { - server: { - id: 'server-1', - }, - }, - sendToRenderer: jest.fn(), - }; - const callsWidgetWindow = new CallsWidgetWindow(); - callsWidgetWindow.mainView = view; - - beforeEach(() => { - ViewManager.getView.mockReturnValue(view); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should pass through the click link to browser history push', () => { - callsWidgetWindow.handleCallsLinkClick('', {link: '/other/subpath'}); - expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-1'); - expect(view.sendToRenderer).toBeCalledWith('browser-history-push', '/other/subpath'); - }); - }); - - describe('genCallsEventHandler', () => { - const handler = jest.fn(); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should not call handler if source is not allowed', () => { - const callsWidgetWindow = new CallsWidgetWindow(); - callsWidgetWindow.isAllowedEvent = () => false; - callsWidgetWindow.genCallsEventHandler(handler)(); - expect(handler).not.toHaveBeenCalled(); - }); - - it('should call handler if source is allowed', () => { - const callsWidgetWindow = new CallsWidgetWindow(); - callsWidgetWindow.isAllowedEvent = () => true; - callsWidgetWindow.genCallsEventHandler(handler)(); - expect(handler).toHaveBeenCalledTimes(1); - }); - }); - - describe('handleCallsJoinRequest', () => { + describe('forwardToMainApp', () => { const view = { view: { server: { @@ -970,6 +775,7 @@ describe('main/windows/callsWidgetWindow', () => { }; const callsWidgetWindow = new CallsWidgetWindow(); callsWidgetWindow.mainView = view; + callsWidgetWindow.win = {webContents: {id: 1}}; const focus = jest.fn(); @@ -982,11 +788,12 @@ describe('main/windows/callsWidgetWindow', () => { jest.resetAllMocks(); }); - it('should pass through the join call callID to the webapp', () => { - callsWidgetWindow.handleCallsJoinRequest('', {callID: 'thecallchannelid'}); + it('should pass through the arguments to the webapp', () => { + const func = callsWidgetWindow.forwardToMainApp('some-channel'); + func({sender: {id: 1}}, 'thecallchannelid'); expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-1'); expect(focus).toHaveBeenCalled(); - expect(view.sendToRenderer).toBeCalledWith(CALLS_JOIN_REQUEST, {callID: 'thecallchannelid'}); + expect(view.sendToRenderer).toBeCalledWith('some-channel', 'thecallchannelid'); }); }); }); diff --git a/src/main/windows/callsWidgetWindow.ts b/src/main/windows/callsWidgetWindow.ts index 279a5b4d..95efd2f6 100644 --- a/src/main/windows/callsWidgetWindow.ts +++ b/src/main/windows/callsWidgetWindow.ts @@ -1,17 +1,10 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {BrowserWindow, desktopCapturer, ipcMain, IpcMainEvent, Rectangle, systemPreferences, Event} from 'electron'; +import {BrowserWindow, desktopCapturer, ipcMain, IpcMainEvent, Rectangle, systemPreferences, Event, IpcMainInvokeEvent} from 'electron'; import { - CallsErrorMessage, - CallsEventHandler, CallsJoinCallMessage, - CallsJoinedCallMessage, - CallsJoinRequestMessage, - CallsLinkClickMessage, - CallsWidgetResizeMessage, - CallsWidgetShareScreenMessage, CallsWidgetWindowConfig, } from 'types/calls'; @@ -34,8 +27,7 @@ import { CALLS_WIDGET_RESIZE, CALLS_WIDGET_SHARE_SCREEN, DESKTOP_SOURCES_MODAL_REQUEST, - DESKTOP_SOURCES_RESULT, - DISPATCH_GET_DESKTOP_SOURCES, + GET_DESKTOP_SOURCES, } from 'common/communication'; import {MattermostBrowserView} from 'main/views/MattermostBrowserView'; @@ -68,16 +60,19 @@ export class CallsWidgetWindow { constructor() { ipcMain.on(CALLS_WIDGET_RESIZE, this.handleResize); ipcMain.on(CALLS_WIDGET_SHARE_SCREEN, this.handleShareScreen); - ipcMain.on(CALLS_JOINED_CALL, this.handleJoinedCall); ipcMain.on(CALLS_POPOUT_FOCUS, this.handlePopOutFocus); - ipcMain.on(DISPATCH_GET_DESKTOP_SOURCES, this.genCallsEventHandler(this.handleGetDesktopSources)); - ipcMain.on(DESKTOP_SOURCES_MODAL_REQUEST, this.genCallsEventHandler(this.handleDesktopSourcesModalRequest)); - ipcMain.on(CALLS_JOIN_CALL, this.genCallsEventHandler(this.handleCreateCallsWidgetWindow)); - ipcMain.on(CALLS_LEAVE_CALL, this.genCallsEventHandler(this.handleCallsLeave)); - ipcMain.on(CALLS_WIDGET_CHANNEL_LINK_CLICK, this.genCallsEventHandler(this.handleCallsWidgetChannelLinkClick)); - ipcMain.on(CALLS_ERROR, this.genCallsEventHandler(this.handleCallsError)); - ipcMain.on(CALLS_LINK_CLICK, this.genCallsEventHandler(this.handleCallsLinkClick)); - ipcMain.on(CALLS_JOIN_REQUEST, this.genCallsEventHandler(this.handleCallsJoinRequest)); + ipcMain.handle(GET_DESKTOP_SOURCES, this.handleGetDesktopSources); + ipcMain.handle(CALLS_JOIN_CALL, this.handleCreateCallsWidgetWindow); + ipcMain.on(CALLS_LEAVE_CALL, this.handleCallsLeave); + + // forwards to the main app + ipcMain.on(DESKTOP_SOURCES_MODAL_REQUEST, this.forwardToMainApp(DESKTOP_SOURCES_MODAL_REQUEST)); + ipcMain.on(CALLS_ERROR, this.forwardToMainApp(CALLS_ERROR)); + ipcMain.on(CALLS_LINK_CLICK, this.forwardToMainApp(CALLS_LINK_CLICK)); + ipcMain.on(CALLS_JOIN_REQUEST, this.forwardToMainApp(CALLS_JOIN_REQUEST)); + + // deprecated in favour of CALLS_LINK_CLICK + ipcMain.on(CALLS_WIDGET_CHANNEL_LINK_CLICK, this.handleCallsWidgetChannelLinkClick); } /** @@ -140,7 +135,7 @@ export class CallsWidgetWindow { hasShadow: false, backgroundColor: '#00ffffff', webPreferences: { - preload: getLocalPreload('callsWidget.js'), + preload: getLocalPreload('externalAPI.js'), }, }); this.mainView = view; @@ -203,28 +198,6 @@ export class CallsWidgetWindow { this.boundsErr = Utils.boundsDiff(bounds, this.win.getBounds()); } - private isAllowedEvent = (event: IpcMainEvent) => { - // Allow events when a call isn't in progress - if (!(this.win && this.mainView)) { - return true; - } - - // Only allow events coming from either the widget window or the - // original Mattermost view that initiated it. - return event.sender.id === this.win?.webContents.id || - event.sender.id === this.mainView?.webContentsId; - } - - private genCallsEventHandler = (handler: CallsEventHandler) => { - return (event: IpcMainEvent, viewId: string, msg?: any) => { - if (!this.isAllowedEvent(event)) { - log.warn('genCallsEventHandler', 'Disallowed calls event'); - return; - } - handler(viewId, msg); - }; - } - /** * BrowserWindow/WebContents handlers */ @@ -333,15 +306,15 @@ export class CallsWidgetWindow { * IPC HANDLERS ************************/ - private handleResize = (ev: IpcMainEvent, _: string, msg: CallsWidgetResizeMessage) => { - log.debug('onResize', msg); + private handleResize = (ev: IpcMainEvent, width: number, height: number) => { + log.debug('handleResize', width, height); if (!this.win) { return; } - if (!this.isAllowedEvent(ev)) { - log.warn('onResize', 'Disallowed calls event'); + if (!this.isCallsWidget(ev.sender.id)) { + log.debug('handleResize', 'Disallowed calls event'); return; } @@ -349,34 +322,23 @@ export class CallsWidgetWindow { const currBounds = this.win.getBounds(); const newBounds = { x: currBounds.x, - y: currBounds.y - (Math.ceil(msg.height * zoomFactor) - currBounds.height), - width: Math.ceil(msg.width * zoomFactor), - height: Math.ceil(msg.height * zoomFactor), + y: currBounds.y - (Math.ceil(height * zoomFactor) - currBounds.height), + width: Math.ceil(width * zoomFactor), + height: Math.ceil(height * zoomFactor), }; this.setBounds(newBounds); } - private handleShareScreen = (ev: IpcMainEvent, _: string, message: CallsWidgetShareScreenMessage) => { - log.debug('handleShareScreen'); + private handleShareScreen = (ev: IpcMainEvent, sourceID: string, withAudio: boolean) => { + log.debug('handleShareScreen', {sourceID, withAudio}); - if (!this.isAllowedEvent(ev)) { - log.warn('Disallowed calls event'); + if (this.mainView?.webContentsId !== ev.sender.id) { + log.debug('handleShareScreen', 'blocked on wrong webContentsId'); return; } - this.win?.webContents.send(CALLS_WIDGET_SHARE_SCREEN, message); - } - - private handleJoinedCall = (ev: IpcMainEvent, _: string, message: CallsJoinedCallMessage) => { - log.debug('handleJoinedCall'); - - if (!this.isAllowedEvent(ev)) { - log.warn('handleJoinedCall', 'Disallowed calls event'); - return; - } - - this.mainView?.sendToRenderer(CALLS_JOINED_CALL, message); + this.win?.webContents.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio); } private handlePopOutFocus = () => { @@ -389,13 +351,18 @@ export class CallsWidgetWindow { this.popOut.focus(); } - private handleGetDesktopSources = async (viewId: string, opts: Electron.SourcesOptions) => { + private handleGetDesktopSources = async (event: IpcMainInvokeEvent, opts: Electron.SourcesOptions) => { log.debug('handleGetDesktopSources', opts); - const view = ViewManager.getView(viewId); + if (event.sender.id !== this.mainView?.webContentsId) { + log.warn('handleGetDesktopSources', 'Blocked on wrong webContentsId'); + return []; + } + + const view = ViewManager.getViewByWebContentsId(event.sender.id); if (!view) { log.error('handleGetDesktopSources: view not found'); - return Promise.resolve(); + return []; } if (process.platform === 'darwin' && systemPreferences.getMediaAccessStatus('screen') === 'denied') { @@ -432,7 +399,7 @@ export class CallsWidgetWindow { log.info('missing screen permissions'); view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg); this.win?.webContents.send(CALLS_ERROR, screenPermissionsErrMsg); - return; + return []; } const message = sources.map((source) => { @@ -443,53 +410,63 @@ export class CallsWidgetWindow { }; }); - if (message.length > 0) { - view.sendToRenderer(DESKTOP_SOURCES_RESULT, message); - } + return message; }).catch((err) => { log.error('desktopCapturer.getSources failed', err); view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg); this.win?.webContents.send(CALLS_ERROR, screenPermissionsErrMsg); + + return []; }); } - private handleCreateCallsWidgetWindow = async (viewId: string, msg: CallsJoinCallMessage) => { + private handleCreateCallsWidgetWindow = async (event: IpcMainInvokeEvent, msg: CallsJoinCallMessage) => { log.debug('createCallsWidgetWindow'); // trying to join again the call we are already in should not be allowed. if (this.options?.callID === msg.callID) { - return; + return Promise.resolve(); } // to switch from one call to another we need to wait for the existing // window to be fully closed. await this.close(); - const currentView = ViewManager.getView(viewId); + const currentView = ViewManager.getViewByWebContentsId(event.sender.id); if (!currentView) { log.error('unable to create calls widget window: currentView is missing'); - return; + return Promise.resolve(); } + const promise = new Promise((resolve) => { + const connected = (ev: IpcMainEvent, incomingCallId: string, incomingSessionId: string) => { + log.debug('onJoinedCall', incomingCallId); + + if (!this.isCallsWidget(ev.sender.id)) { + log.debug('onJoinedCall', 'blocked on wrong webContentsId'); + return; + } + + if (msg.callID !== incomingCallId) { + log.debug('onJoinedCall', 'blocked on wrong callId'); + return; + } + + ipcMain.off(CALLS_JOINED_CALL, connected); + resolve({callID: msg.callID, sessionID: incomingSessionId}); + }; + ipcMain.on(CALLS_JOINED_CALL, connected); + }); + this.init(currentView, { callID: msg.callID, title: msg.title, rootID: msg.rootID, channelURL: msg.channelURL, }); - } - private handleDesktopSourcesModalRequest = () => { - log.debug('handleDesktopSourcesModalRequest'); - - if (!this.serverID) { - return; - } - - ServerViewState.switchServer(this.serverID); - MainWindow.get()?.focus(); - this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST); + return promise; } private handleCallsLeave = () => { @@ -498,9 +475,34 @@ export class CallsWidgetWindow { this.close(); } - private handleCallsWidgetChannelLinkClick = () => { + private forwardToMainApp = (channel: string) => { + return (event: IpcMainEvent, ...args: any) => { + log.debug('forwardToMainApp', channel, ...args); + + if (!this.isCallsWidget(event.sender.id)) { + return; + } + + if (!this.serverID) { + return; + } + + ServerViewState.switchServer(this.serverID); + MainWindow.get()?.focus(); + this.mainView?.sendToRenderer(channel, ...args); + }; + } + + /** + * @deprecated + */ + private handleCallsWidgetChannelLinkClick = (event: IpcMainEvent) => { log.debug('handleCallsWidgetChannelLinkClick'); + if (!this.isCallsWidget(event.sender.id)) { + return; + } + if (!this.serverID) { return; } @@ -509,41 +511,6 @@ export class CallsWidgetWindow { MainWindow.get()?.focus(); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL); } - - private handleCallsError = (_: string, msg: CallsErrorMessage) => { - log.debug('handleCallsError', msg); - - if (!this.serverID) { - return; - } - - ServerViewState.switchServer(this.serverID); - MainWindow.get()?.focus(); - this.mainView?.sendToRenderer(CALLS_ERROR, msg); - } - - private handleCallsLinkClick = (_: string, msg: CallsLinkClickMessage) => { - log.debug('handleCallsLinkClick with linkURL', msg.link); - - if (!this.serverID) { - return; - } - - ServerViewState.switchServer(this.serverID); - MainWindow.get()?.focus(); - this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link); - } - - private handleCallsJoinRequest = (_: string, msg: CallsJoinRequestMessage) => { - log.debug('handleCallsJoinRequest with callID', msg.callID); - if (!this.serverID) { - return; - } - - ServerViewState.switchServer(this.serverID); - MainWindow.get()?.focus(); - this.mainView?.sendToRenderer(CALLS_JOIN_REQUEST, msg); - } } const callsWidgetWindow = new CallsWidgetWindow(); diff --git a/src/main/windows/mainWindow.ts b/src/main/windows/mainWindow.ts index 1052af18..9d8f3fe7 100644 --- a/src/main/windows/mainWindow.ts +++ b/src/main/windows/mainWindow.ts @@ -86,7 +86,7 @@ export class MainWindow extends EventEmitter { backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do webPreferences: { disableBlinkFeatures: 'Auxclick', - preload: getLocalPreload('desktopAPI.js'), + preload: getLocalPreload('internalAPI.js'), spellcheck: typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker, }, }); diff --git a/src/main/windows/settingsWindow.ts b/src/main/windows/settingsWindow.ts index 14086cd2..dde77c9e 100644 --- a/src/main/windows/settingsWindow.ts +++ b/src/main/windows/settingsWindow.ts @@ -43,7 +43,7 @@ export class SettingsWindow { return; } - const preload = getLocalPreload('desktopAPI.js'); + const preload = getLocalPreload('internalAPI.js'); const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker); this.win = new BrowserWindow({ parent: mainWindow, diff --git a/src/types/calls.ts b/src/types/calls.ts index b956021e..452f079d 100644 --- a/src/types/calls.ts +++ b/src/types/calls.ts @@ -8,34 +8,3 @@ export type CallsWidgetWindowConfig = { } export type CallsJoinCallMessage = CallsWidgetWindowConfig; - -export type CallsWidgetResizeMessage = { - element: string; - width: number; - height: number; -} - -export type CallsWidgetShareScreenMessage = { - sourceID: string; - withAudio: boolean; -} - -export type CallsJoinedCallMessage = { - callID: string; -} - -export type CallsErrorMessage = { - err: string; - callID?: string; - errMsg?: string; -} - -export type CallsLinkClickMessage = { - link: string | URL; -} - -export type CallsJoinRequestMessage = { - callID: string; -} - -export type CallsEventHandler = ((viewName: string, msg: any) => void) | ((viewName: string, opts: Electron.SourcesOptions) => Promise); diff --git a/src/types/externalAPI.ts b/src/types/externalAPI.ts new file mode 100644 index 00000000..949c59df --- /dev/null +++ b/src/types/externalAPI.ts @@ -0,0 +1,24 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +export interface ExternalAPI { + createListener(event: 'user-activity-update', listener: ( + userIsActive: boolean, + idleTime: number, + isSystemEvent: boolean, + ) => void): () => void; + createListener(event: 'notification-clicked', listener: ( + channelId: string, + teamId: string, + url: string, + ) => void): () => void; + createListener(event: 'browser-history-status-updated', listener: ( + canGoBack: boolean, + canGoForward: boolean, + ) => void): () => void; + createListener(event: 'browser-history-push', listener: (path: string) => void): () => void; + createListener(event: 'calls-widget-share-screen', listener: (sourceID: string, withAudio: boolean) => void): () => void; + createListener(event: 'calls-join-request', listener: (callID: string) => void): () => void; + createListener(event: 'calls-error', listener: (err: string, callID?: string, errMsg?: string) => void): () => void; + createListener(event: 'desktop-sources-modal-request', listener: () => void): () => void; +} diff --git a/src/types/notification.ts b/src/types/notification.ts index 5cff3df0..7bd4773d 100644 --- a/src/types/notification.ts +++ b/src/types/notification.ts @@ -3,10 +3,6 @@ import {NotificationConstructorOptions} from 'electron/common'; -export type MentionData = { +export type MentionOptions = NotificationConstructorOptions & { soundName: string; } - -export type MentionOptions = NotificationConstructorOptions & { - data: MentionData; -} diff --git a/webpack.config.main.js b/webpack.config.main.js index 7d8d2178..380bd7c4 100644 --- a/webpack.config.main.js +++ b/webpack.config.main.js @@ -15,9 +15,8 @@ const base = require('./webpack.config.base'); module.exports = merge(base, { entry: { index: './src/main/app/index.ts', - desktopAPI: './src/main/preload/desktopAPI.js', - preload: './src/main/preload/mattermost.js', - callsWidget: './src/main/preload/callsWidget.js', + internalAPI: './src/main/preload/internalAPI.js', + externalAPI: './src/main/preload/externalAPI.ts', }, externals: { 'macos-notification-state': 'require("macos-notification-state")',