[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
This commit is contained in:
parent
675ec6d661
commit
0cab09b7f5
|
@ -103,7 +103,7 @@
|
||||||
"src/main/downloadURL.ts",
|
"src/main/downloadURL.ts",
|
||||||
"src/main/SpellChecker.ts",
|
"src/main/SpellChecker.ts",
|
||||||
"src/main/menus/app.ts",
|
"src/main/menus/app.ts",
|
||||||
"src/main/preload/mattermost.js",
|
"src/main/preload/externalAPI.js",
|
||||||
"src/renderer/components/RemoveServerModal.tsx",
|
"src/renderer/components/RemoveServerModal.tsx",
|
||||||
"src/renderer/components/MainPage.tsx",
|
"src/renderer/components/MainPage.tsx",
|
||||||
"src/renderer/components/HoveringURL.tsx",
|
"src/renderer/components/HoveringURL.tsx",
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -24,3 +24,4 @@ fastlane/README.md
|
||||||
fastlane/report.xml
|
fastlane/report.xml
|
||||||
|
|
||||||
*.provisionprofile
|
*.provisionprofile
|
||||||
|
*.tsbuildinfo
|
67
api-types/index.ts
Normal file
67
api-types/index.ts
Normal file
|
@ -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<boolean>;
|
||||||
|
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<DesktopCaptureSource[]>;
|
||||||
|
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;
|
||||||
|
}
|
57
api-types/lib/index.d.ts
vendored
Normal file
57
api-types/lib/index.d.ts
vendored
Normal file
|
@ -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<boolean>;
|
||||||
|
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<DesktopCaptureSource[]>;
|
||||||
|
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;
|
||||||
|
};
|
4
api-types/lib/index.js
Normal file
4
api-types/lib/index.js
Normal file
|
@ -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 });
|
21
api-types/package-lock.json
generated
Normal file
21
api-types/package-lock.json
generated
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
api-types/package.json
Normal file
33
api-types/package.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
20
api-types/tsconfig.json
Normal file
20
api-types/tsconfig.json
Normal file
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
23
package-lock.json
generated
23
package-lock.json
generated
|
@ -26,6 +26,7 @@
|
||||||
"@electron/fuses": "1.6.0",
|
"@electron/fuses": "1.6.0",
|
||||||
"@electron/universal": "1.3.1",
|
"@electron/universal": "1.3.1",
|
||||||
"@mattermost/compass-icons": "0.1.32",
|
"@mattermost/compass-icons": "0.1.32",
|
||||||
|
"@mattermost/desktop-api": "*",
|
||||||
"@storybook/addon-actions": "6.4.20",
|
"@storybook/addon-actions": "6.4.20",
|
||||||
"@storybook/react": "6.4.20",
|
"@storybook/react": "6.4.20",
|
||||||
"@types/auto-launch": "5.0.2",
|
"@types/auto-launch": "5.0.2",
|
||||||
|
@ -117,6 +118,20 @@
|
||||||
"node": ">=16.16.0"
|
"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": {
|
"node_modules/@ampproject/remapping": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz",
|
||||||
|
@ -4791,6 +4806,10 @@
|
||||||
"integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==",
|
"integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@mattermost/desktop-api": {
|
||||||
|
"resolved": "api-types",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@mdx-js/mdx": {
|
"node_modules/@mdx-js/mdx": {
|
||||||
"version": "1.6.22",
|
"version": "1.6.22",
|
||||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz",
|
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz",
|
||||||
|
@ -40037,6 +40056,10 @@
|
||||||
"integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==",
|
"integrity": "sha512-SruyY3dJUGoOCuc5M7KkpFZgotfmeV5Osi+nrMObRdTmaLfJ8h9Q6ZueLx4k4LkLt7hW0CAl33pWc6jO7p3egQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@mattermost/desktop-api": {
|
||||||
|
"version": "file:api-types",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"@mdx-js/mdx": {
|
"@mdx-js/mdx": {
|
||||||
"version": "1.6.22",
|
"version": "1.6.22",
|
||||||
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz",
|
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz",
|
||||||
|
|
|
@ -145,6 +145,7 @@
|
||||||
"@electron/fuses": "1.6.0",
|
"@electron/fuses": "1.6.0",
|
||||||
"@electron/universal": "1.3.1",
|
"@electron/universal": "1.3.1",
|
||||||
"@mattermost/compass-icons": "0.1.32",
|
"@mattermost/compass-icons": "0.1.32",
|
||||||
|
"@mattermost/desktop-api": "*",
|
||||||
"@storybook/addon-actions": "6.4.20",
|
"@storybook/addon-actions": "6.4.20",
|
||||||
"@storybook/react": "6.4.20",
|
"@storybook/react": "6.4.20",
|
||||||
"@types/auto-launch": "5.0.2",
|
"@types/auto-launch": "5.0.2",
|
||||||
|
|
|
@ -68,7 +68,7 @@ export class ServerViewState {
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentServer = () => {
|
getCurrentServer = () => {
|
||||||
log.debug('getCurrentServer');
|
log.silly('getCurrentServer');
|
||||||
|
|
||||||
if (!this.currentServerId) {
|
if (!this.currentServerId) {
|
||||||
throw new Error('No server set as current');
|
throw new Error('No server set as current');
|
||||||
|
@ -132,7 +132,7 @@ export class ServerViewState {
|
||||||
const modalPromise = ModalManager.addModal<null, Server>(
|
const modalPromise = ModalManager.addModal<null, Server>(
|
||||||
'newServer',
|
'newServer',
|
||||||
getLocalURLString('newServer.html'),
|
getLocalURLString('newServer.html'),
|
||||||
getLocalPreload('desktopAPI.js'),
|
getLocalPreload('internalAPI.js'),
|
||||||
null,
|
null,
|
||||||
mainWindow,
|
mainWindow,
|
||||||
!ServerManager.hasServers(),
|
!ServerManager.hasServers(),
|
||||||
|
@ -164,7 +164,7 @@ export class ServerViewState {
|
||||||
const modalPromise = ModalManager.addModal<UniqueServer, Server>(
|
const modalPromise = ModalManager.addModal<UniqueServer, Server>(
|
||||||
'editServer',
|
'editServer',
|
||||||
getLocalURLString('editServer.html'),
|
getLocalURLString('editServer.html'),
|
||||||
getLocalPreload('desktopAPI.js'),
|
getLocalPreload('internalAPI.js'),
|
||||||
server.toUniqueServer(),
|
server.toUniqueServer(),
|
||||||
mainWindow);
|
mainWindow);
|
||||||
|
|
||||||
|
@ -191,7 +191,7 @@ export class ServerViewState {
|
||||||
const modalPromise = ModalManager.addModal<string, boolean>(
|
const modalPromise = ModalManager.addModal<string, boolean>(
|
||||||
'removeServer',
|
'removeServer',
|
||||||
getLocalURLString('removeServer.html'),
|
getLocalURLString('removeServer.html'),
|
||||||
getLocalPreload('desktopAPI.js'),
|
getLocalPreload('internalAPI.js'),
|
||||||
server.name,
|
server.name,
|
||||||
mainWindow,
|
mainWindow,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export const GET_APP_INFO = 'get-app-info';
|
||||||
|
|
||||||
export const SWITCH_SERVER = 'switch-server';
|
export const SWITCH_SERVER = 'switch-server';
|
||||||
export const SWITCH_TAB = 'switch-tab';
|
export const SWITCH_TAB = 'switch-tab';
|
||||||
export const CLOSE_VIEW = 'close-view';
|
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 UPDATE_MENTIONS = 'update_mentions';
|
||||||
export const IS_UNREAD = 'is_unread';
|
export const IS_UNREAD = 'is_unread';
|
||||||
export const UNREAD_RESULT = 'unread_result';
|
export const UNREAD_RESULT = 'unread_result';
|
||||||
|
export const UNREADS_AND_MENTIONS = 'unreads-and-mentions';
|
||||||
export const SESSION_EXPIRED = 'session_expired';
|
export const SESSION_EXPIRED = 'session_expired';
|
||||||
|
|
||||||
export const SET_VIEW_OPTIONS = 'set-view-name';
|
|
||||||
export const REACT_APP_INITIALIZED = 'react-app-initialized';
|
export const REACT_APP_INITIALIZED = 'react-app-initialized';
|
||||||
|
|
||||||
export const TOGGLE_BACK_BUTTON = 'toggle-back-button';
|
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 CHECK_FOR_UPDATES = 'check-for-updates';
|
||||||
export const NO_UPDATE_AVAILABLE = 'no-update-available';
|
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 BROWSER_HISTORY_PUSH = 'browser-history-push';
|
||||||
export const APP_LOGGED_IN = 'app-logged-in';
|
export const APP_LOGGED_IN = 'app-logged-in';
|
||||||
export const APP_LOGGED_OUT = 'app-logged-out';
|
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';
|
export const VIEW_FINISHED_RESIZING = 'view-finished-resizing';
|
||||||
|
|
||||||
// Calls
|
// 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_RESULT = 'desktop-sources-result';
|
||||||
export const DESKTOP_SOURCES_MODAL_REQUEST = 'desktop-sources-modal-request';
|
export const DESKTOP_SOURCES_MODAL_REQUEST = 'desktop-sources-modal-request';
|
||||||
export const CALLS_JOIN_CALL = 'calls-join-call';
|
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 GET_IS_DEV_MODE = 'get-is-dev-mode';
|
||||||
|
|
||||||
export const TOGGLE_SECURE_INPUT = 'toggle-secure-input';
|
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';
|
||||||
|
|
|
@ -57,6 +57,7 @@ export const isUrlType = (urlType: string, serverURL: URL, inputURL: URL) => {
|
||||||
getFormattedPathName(inputURL.pathname).startsWith(`/${urlType}/`));
|
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 isHelpUrl = (serverURL: URL, inputURL: URL) => isUrlType('help', serverURL, inputURL);
|
||||||
export const isImageProxyUrl = (serverURL: URL, inputURL: URL) => isUrlType('api/v4/image', 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);
|
export const isPublicFilesUrl = (serverURL: URL, inputURL: URL) => isUrlType('files', serverURL, inputURL);
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {
|
||||||
WINDOW_RESTORE,
|
WINDOW_RESTORE,
|
||||||
DOUBLE_CLICK_ON_WINDOW,
|
DOUBLE_CLICK_ON_WINDOW,
|
||||||
TOGGLE_SECURE_INPUT,
|
TOGGLE_SECURE_INPUT,
|
||||||
|
GET_APP_INFO,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
@ -249,7 +250,7 @@ function initializeBeforeAppReady() {
|
||||||
|
|
||||||
function initializeInterCommunicationEventListeners() {
|
function initializeInterCommunicationEventListeners() {
|
||||||
ipcMain.on(NOTIFY_MENTION, handleMentionNotification);
|
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(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent);
|
||||||
ipcMain.on(FOCUS_BROWSERVIEW, ViewManager.focusCurrentView);
|
ipcMain.on(FOCUS_BROWSERVIEW, ViewManager.focusCurrentView);
|
||||||
|
|
||||||
|
@ -366,7 +367,7 @@ async function initializeAfterAppReady() {
|
||||||
// listen for status updates and pass on to renderer
|
// listen for status updates and pass on to renderer
|
||||||
UserActivityMonitor.on('status', (status) => {
|
UserActivityMonitor.on('status', (status) => {
|
||||||
log.debug('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)
|
// start monitoring user activity (needs to be started after the app is ready)
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
import {app, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron';
|
import {app, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron';
|
||||||
|
|
||||||
import {UniqueServer} from 'types/config';
|
import {UniqueServer} from 'types/config';
|
||||||
import {MentionData} from 'types/notification';
|
|
||||||
|
|
||||||
import ServerViewState from 'app/serverViewState';
|
import ServerViewState from 'app/serverViewState';
|
||||||
|
|
||||||
|
@ -92,7 +91,7 @@ export function handleWelcomeScreenModal() {
|
||||||
|
|
||||||
const html = getLocalURLString('welcomeScreen.html');
|
const html = getLocalURLString('welcomeScreen.html');
|
||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('internalAPI.js');
|
||||||
|
|
||||||
const mainWindow = MainWindow.get();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
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) {
|
export function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, soundName: string) {
|
||||||
log.debug('handleMentionNotification', {title, body, channel, teamId, url, silent, data});
|
log.debug('handleMentionNotification', {title, body, channelId, teamId, url, silent, soundName});
|
||||||
NotificationManager.displayMention(title, body, channel, teamId, url, silent, event.sender, data);
|
NotificationManager.displayMention(title, body, channelId, teamId, url, silent, event.sender, soundName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleOpenAppMenu() {
|
export function handleOpenAppMenu() {
|
||||||
|
|
|
@ -16,7 +16,7 @@ import MainWindow from 'main/windows/mainWindow';
|
||||||
import ViewManager from 'main/views/viewManager';
|
import ViewManager from 'main/views/viewManager';
|
||||||
|
|
||||||
const log = new Logger('AuthManager');
|
const log = new Logger('AuthManager');
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('internalAPI.js');
|
||||||
const loginModalHtml = getLocalURLString('loginModal.html');
|
const loginModalHtml = getLocalURLString('loginModal.html');
|
||||||
const permissionModalHtml = getLocalURLString('permissionModal.html');
|
const permissionModalHtml = getLocalURLString('permissionModal.html');
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {getLocalURLString, getLocalPreload} from './utils';
|
||||||
import MainWindow from './windows/mainWindow';
|
import MainWindow from './windows/mainWindow';
|
||||||
|
|
||||||
const log = new Logger('CertificateManager');
|
const log = new Logger('CertificateManager');
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('internalAPI.js');
|
||||||
const html = getLocalURLString('certificateModal.html');
|
const html = getLocalURLString('certificateModal.html');
|
||||||
|
|
||||||
type CertificateModalResult = {
|
type CertificateModalResult = {
|
||||||
|
|
|
@ -27,25 +27,25 @@ const DEFAULT_WIN7 = 'Ding';
|
||||||
|
|
||||||
export class Mention extends Notification {
|
export class Mention extends Notification {
|
||||||
customSound: string;
|
customSound: string;
|
||||||
channel: {id: string}; // TODO: Channel from mattermost-redux
|
channelId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
uId: string;
|
uId: string;
|
||||||
|
|
||||||
constructor(customOptions: MentionOptions, channel: {id: string}, teamId: string) {
|
constructor(customOptions: MentionOptions, channelId: string, teamId: string) {
|
||||||
const options = {...defaultOptions, ...customOptions};
|
const options = {...defaultOptions, ...customOptions};
|
||||||
if (process.platform === 'darwin' || (process.platform === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '10.0'))) {
|
if (process.platform === 'darwin' || (process.platform === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '10.0'))) {
|
||||||
// Notification Center shows app's icon, so there were two icons on the notification.
|
// Notification Center shows app's icon, so there were two icons on the notification.
|
||||||
Reflect.deleteProperty(options, 'icon');
|
Reflect.deleteProperty(options, 'icon');
|
||||||
}
|
}
|
||||||
const isWin7 = (process.platform === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.3') && DEFAULT_WIN7);
|
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) {
|
if (customSound) {
|
||||||
options.silent = true;
|
options.silent = true;
|
||||||
}
|
}
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.customSound = customSound;
|
this.customSound = customSound;
|
||||||
this.channel = channel;
|
this.channelId = channelId;
|
||||||
this.teamId = teamId;
|
this.teamId = teamId;
|
||||||
this.uId = uuid();
|
this.uId = uuid();
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,6 +92,7 @@ jest.mock('../views/viewManager', () => ({
|
||||||
name: 'server_name',
|
name: 'server_name',
|
||||||
url: new URL('http://someurl.com'),
|
url: new URL('http://someurl.com'),
|
||||||
},
|
},
|
||||||
|
shouldNotify: true,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
showById: jest.fn(),
|
showById: jest.fn(),
|
||||||
|
@ -144,12 +145,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'test',
|
'test',
|
||||||
'test body',
|
'test body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1} as WebContents,
|
{id: 1} as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
expect(MainWindow.show).not.toBeCalled();
|
expect(MainWindow.show).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
@ -164,12 +165,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'test',
|
'test',
|
||||||
'test body',
|
'test body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1} as WebContents,
|
{id: 1} as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
expect(MainWindow.show).not.toBeCalled();
|
expect(MainWindow.show).not.toBeCalled();
|
||||||
|
|
||||||
|
@ -188,12 +189,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'test',
|
'test',
|
||||||
'test body',
|
'test body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1} as WebContents,
|
{id: 1} as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
expect(MainWindow.show).not.toBeCalled();
|
expect(MainWindow.show).not.toBeCalled();
|
||||||
|
|
||||||
|
@ -207,12 +208,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'test',
|
'test',
|
||||||
'test body',
|
'test body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1} as WebContents,
|
{id: 1} as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
expect(MainWindow.show).not.toBeCalled();
|
expect(MainWindow.show).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
@ -221,12 +222,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'test',
|
'test',
|
||||||
'test body',
|
'test body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1} as WebContents,
|
{id: 1} as WebContents,
|
||||||
{soundName: 'test_sound'},
|
'test_sound',
|
||||||
);
|
);
|
||||||
expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound');
|
expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound');
|
||||||
});
|
});
|
||||||
|
@ -240,12 +241,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'test',
|
'test',
|
||||||
'test body',
|
'test body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1} as WebContents,
|
{id: 1} as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
// convert to any to access private field
|
// convert to any to access private field
|
||||||
|
@ -257,12 +258,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'test',
|
'test',
|
||||||
'test body 2',
|
'test body 2',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1} as WebContents,
|
{id: 1} as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mentionsPerChannel.delete).toHaveBeenCalled();
|
expect(mentionsPerChannel.delete).toHaveBeenCalled();
|
||||||
|
@ -277,12 +278,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'click_test',
|
'click_test',
|
||||||
'mention_click_body',
|
'mention_click_body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1, send: jest.fn()} as unknown as WebContents,
|
{id: 1, send: jest.fn()} as unknown as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
const mention = mentions.find((m) => m.body === 'mention_click_body');
|
const mention = mentions.find((m) => m.body === 'mention_click_body');
|
||||||
mention?.value.click();
|
mention?.value.click();
|
||||||
|
@ -298,12 +299,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'click_test',
|
'click_test',
|
||||||
'mention_click_body',
|
'mention_click_body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1, send: jest.fn()} as unknown as WebContents,
|
{id: 1, send: jest.fn()} as unknown as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
|
@ -324,12 +325,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'click_test',
|
'click_test',
|
||||||
'mention_click_body',
|
'mention_click_body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1, send: jest.fn()} as unknown as WebContents,
|
{id: 1, send: jest.fn()} as unknown as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
|
@ -345,12 +346,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'click_test',
|
'click_test',
|
||||||
'mention_click_body',
|
'mention_click_body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1, send: jest.fn()} as unknown as WebContents,
|
{id: 1, send: jest.fn()} as unknown as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
|
@ -371,12 +372,12 @@ describe('main/notifications', () => {
|
||||||
await NotificationManager.displayMention(
|
await NotificationManager.displayMention(
|
||||||
'click_test',
|
'click_test',
|
||||||
'mention_click_body',
|
'mention_click_body',
|
||||||
{id: 'channel_id'},
|
'channel_id',
|
||||||
'team_id',
|
'team_id',
|
||||||
'http://server-1.com/team_id/channel_id',
|
'http://server-1.com/team_id/channel_id',
|
||||||
false,
|
false,
|
||||||
{id: 1, send: jest.fn()} as unknown as WebContents,
|
{id: 1, send: jest.fn()} as unknown as WebContents,
|
||||||
{soundName: ''},
|
'',
|
||||||
);
|
);
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
|
|
|
@ -5,10 +5,8 @@ import {app, shell, Notification} from 'electron';
|
||||||
|
|
||||||
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
|
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
|
||||||
|
|
||||||
import {MentionData} from 'types/notification';
|
|
||||||
|
|
||||||
import Config from 'common/config';
|
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 {Logger} from 'common/log';
|
||||||
|
|
||||||
import PermissionsManager from '../permissionsManager';
|
import PermissionsManager from '../permissionsManager';
|
||||||
|
@ -29,8 +27,8 @@ class NotificationManager {
|
||||||
private upgradeNotification?: NewVersionNotification;
|
private upgradeNotification?: NewVersionNotification;
|
||||||
private restartToUpgradeNotification?: UpgradeNotification;
|
private restartToUpgradeNotification?: UpgradeNotification;
|
||||||
|
|
||||||
public async displayMention(title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, data: MentionData) {
|
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, channel, teamId, url, silent, data});
|
log.debug('displayMention', {title, body, channelId, teamId, url, silent, soundName});
|
||||||
|
|
||||||
if (!Notification.isSupported()) {
|
if (!Notification.isSupported()) {
|
||||||
log.error('notification not supported');
|
log.error('notification not supported');
|
||||||
|
@ -45,21 +43,24 @@ class NotificationManager {
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!view.view.shouldNotify) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const serverName = view.view.server.name;
|
const serverName = view.view.server.name;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
title: `${serverName}: ${title}`,
|
title: `${serverName}: ${title}`,
|
||||||
body,
|
body,
|
||||||
silent,
|
silent,
|
||||||
data,
|
soundName,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) {
|
if (!await PermissionsManager.doPermissionRequest(webcontents.id, 'notifications', view.view.server.url.toString())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mention = new Mention(options, channel, teamId);
|
const mention = new Mention(options, channelId, teamId);
|
||||||
const mentionKey = `${mention.teamId}:${mention.channel.id}`;
|
const mentionKey = `${mention.teamId}:${mention.channelId}`;
|
||||||
this.allActiveNotifications.set(mention.uId, mention);
|
this.allActiveNotifications.set(mention.uId, mention);
|
||||||
|
|
||||||
mention.on('show', () => {
|
mention.on('show', () => {
|
||||||
|
@ -88,7 +89,7 @@ class NotificationManager {
|
||||||
MainWindow.show();
|
MainWindow.show();
|
||||||
if (serverName) {
|
if (serverName) {
|
||||||
ViewManager.showById(view.id);
|
ViewManager.showById(view.id);
|
||||||
webcontents.send('notification-clicked', {channel, teamId, url});
|
webcontents.send(NOTIFICATION_CLICKED, channelId, teamId, url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
});
|
|
470
src/main/preload/externalAPI.ts
Normal file
470
src/main/preload/externalAPI.ts
Normal file
|
@ -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<string, {id: string}> = 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');
|
||||||
|
}
|
|
@ -89,6 +89,7 @@ import {
|
||||||
GET_ORDERED_TABS_FOR_SERVER,
|
GET_ORDERED_TABS_FOR_SERVER,
|
||||||
SERVERS_UPDATE,
|
SERVERS_UPDATE,
|
||||||
VALIDATE_SERVER_URL,
|
VALIDATE_SERVER_URL,
|
||||||
|
GET_APP_INFO,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
console.log('Preload initialized');
|
console.log('Preload initialized');
|
||||||
|
@ -139,7 +140,7 @@ contextBridge.exposeInMainWorld('desktop', {
|
||||||
validateServerURL: (url, currentId) => ipcRenderer.invoke(VALIDATE_SERVER_URL, url, currentId),
|
validateServerURL: (url, currentId) => ipcRenderer.invoke(VALIDATE_SERVER_URL, url, currentId),
|
||||||
|
|
||||||
getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION),
|
getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION),
|
||||||
getVersion: () => ipcRenderer.invoke('get-app-version'),
|
getVersion: () => ipcRenderer.invoke(GET_APP_INFO),
|
||||||
getDarkMode: () => ipcRenderer.invoke(GET_DARK_MODE),
|
getDarkMode: () => ipcRenderer.invoke(GET_DARK_MODE),
|
||||||
requestHasDownloads: () => ipcRenderer.invoke(REQUEST_HAS_DOWNLOADS),
|
requestHasDownloads: () => ipcRenderer.invoke(REQUEST_HAS_DOWNLOADS),
|
||||||
getFullScreenStatus: () => ipcRenderer.invoke(GET_FULL_SCREEN_STATUS),
|
getFullScreenStatus: () => ipcRenderer.invoke(GET_FULL_SCREEN_STATUS),
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -45,6 +45,7 @@ jest.mock('../windows/mainWindow', () => ({
|
||||||
jest.mock('common/appState', () => ({
|
jest.mock('common/appState', () => ({
|
||||||
clear: jest.fn(),
|
clear: jest.fn(),
|
||||||
updateMentions: jest.fn(),
|
updateMentions: jest.fn(),
|
||||||
|
updateExpired: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('./webContentEvents', () => ({
|
jest.mock('./webContentEvents', () => ({
|
||||||
addWebContentsEventListeners: jest.fn(),
|
addWebContentsEventListeners: jest.fn(),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {BrowserView, app} from 'electron';
|
import {BrowserView, app, ipcMain} from 'electron';
|
||||||
import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
|
import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
|
||||||
|
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
|
@ -15,10 +15,11 @@ import {
|
||||||
UPDATE_TARGET_URL,
|
UPDATE_TARGET_URL,
|
||||||
IS_UNREAD,
|
IS_UNREAD,
|
||||||
TOGGLE_BACK_BUTTON,
|
TOGGLE_BACK_BUTTON,
|
||||||
SET_VIEW_OPTIONS,
|
|
||||||
LOADSCREEN_END,
|
LOADSCREEN_END,
|
||||||
BROWSER_HISTORY_BUTTON,
|
|
||||||
SERVERS_URL_MODIFIED,
|
SERVERS_URL_MODIFIED,
|
||||||
|
BROWSER_HISTORY_STATUS_UPDATED,
|
||||||
|
CLOSE_SERVERS_DROPDOWN,
|
||||||
|
CLOSE_DOWNLOADS_DROPDOWN,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import ServerManager from 'common/servers/serverManager';
|
import ServerManager from 'common/servers/serverManager';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
@ -62,7 +63,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
super();
|
super();
|
||||||
this.view = view;
|
this.view = view;
|
||||||
|
|
||||||
const preload = getLocalPreload('preload.js');
|
const preload = getLocalPreload('externalAPI.js');
|
||||||
this.options = Object.assign({}, options);
|
this.options = Object.assign({}, options);
|
||||||
this.options.webPreferences = {
|
this.options.webPreferences = {
|
||||||
preload,
|
preload,
|
||||||
|
@ -81,14 +82,21 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
this.log = ServerManager.getViewLog(this.id, 'MattermostBrowserView');
|
this.log = ServerManager.getViewLog(this.id, 'MattermostBrowserView');
|
||||||
this.log.verbose('View created');
|
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('update-target-url', this.handleUpdateTarget);
|
||||||
this.browserView.webContents.on('did-navigate', this.handleDidNavigate);
|
this.browserView.webContents.on('did-navigate', this.handleDidNavigate);
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
this.browserView.webContents.on('before-input-event', this.handleInputEvents);
|
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);
|
WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents);
|
||||||
|
|
||||||
|
@ -148,14 +156,23 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHistoryButton = () => {
|
getBrowserHistoryStatus = () => {
|
||||||
if (this.currentURL?.toString() === this.view.url.toString()) {
|
if (this.currentURL?.toString() === this.view.url.toString()) {
|
||||||
this.browserView.webContents.clearHistory();
|
this.browserView.webContents.clearHistory();
|
||||||
this.atRoot = true;
|
this.atRoot = true;
|
||||||
} else {
|
} else {
|
||||||
this.atRoot = false;
|
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) => {
|
load = (someURL?: URL | string) => {
|
||||||
|
@ -175,6 +192,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
} else {
|
} else {
|
||||||
loadURL = this.view.url.toString();
|
loadURL = this.view.url.toString();
|
||||||
}
|
}
|
||||||
|
AppState.updateExpired(this.id, false);
|
||||||
this.log.verbose(`Loading ${loadURL}`);
|
this.log.verbose(`Loading ${loadURL}`);
|
||||||
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
|
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
|
||||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
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
|
* Status hooks
|
||||||
*/
|
*/
|
||||||
|
@ -463,26 +490,6 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
* WebContents event handlers
|
* 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) => {
|
private handleDidNavigate = (event: Event, url: string) => {
|
||||||
this.log.debug('handleDidNavigate', url);
|
this.log.debug('handleDidNavigate', url);
|
||||||
|
|
||||||
|
@ -507,7 +514,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleUpdateTarget = (e: Event, url: string) => {
|
private handleUpdateTarget = (e: Event, url: string) => {
|
||||||
this.log.silly('handleUpdateTarget', url);
|
this.log.silly('handleUpdateTarget', e, url);
|
||||||
const parsedURL = parseURL(url);
|
const parsedURL = parseURL(url);
|
||||||
if (parsedURL && isInternalURL(parsedURL, this.view.server.url)) {
|
if (parsedURL && isInternalURL(parsedURL, this.view.server.url)) {
|
||||||
this.emit(UPDATE_TARGET_URL);
|
this.emit(UPDATE_TARGET_URL);
|
||||||
|
|
|
@ -65,7 +65,7 @@ export class DownloadsDropdownMenuView {
|
||||||
}
|
}
|
||||||
this.bounds = this.getBounds(this.windowBounds.width, DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
|
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: {
|
this.view = new BrowserView({webPreferences: {
|
||||||
preload,
|
preload,
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ export class DownloadsDropdownMenuView {
|
||||||
* the downloads dropdown at the correct position
|
* the downloads dropdown at the correct position
|
||||||
*/
|
*/
|
||||||
private updateWindowBounds = (newBounds: Electron.Rectangle) => {
|
private updateWindowBounds = (newBounds: Electron.Rectangle) => {
|
||||||
log.debug('updateWindowBounds');
|
log.silly('updateWindowBounds');
|
||||||
|
|
||||||
this.windowBounds = newBounds;
|
this.windowBounds = newBounds;
|
||||||
this.updateDownloadsDropdownMenu();
|
this.updateDownloadsDropdownMenu();
|
||||||
|
@ -98,7 +98,7 @@ export class DownloadsDropdownMenuView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateDownloadsDropdownMenu = () => {
|
private updateDownloadsDropdownMenu = () => {
|
||||||
log.debug('updateDownloadsDropdownMenu');
|
log.silly('updateDownloadsDropdownMenu');
|
||||||
|
|
||||||
this.view?.webContents.send(
|
this.view?.webContents.send(
|
||||||
UPDATE_DOWNLOADS_DROPDOWN_MENU,
|
UPDATE_DOWNLOADS_DROPDOWN_MENU,
|
||||||
|
@ -131,7 +131,7 @@ export class DownloadsDropdownMenuView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClose = () => {
|
private handleClose = () => {
|
||||||
log.debug('handleClose');
|
log.silly('handleClose');
|
||||||
|
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.item = undefined;
|
this.item = undefined;
|
||||||
|
|
|
@ -57,7 +57,7 @@ export class DownloadsDropdownView {
|
||||||
}
|
}
|
||||||
this.bounds = this.getBounds(this.windowBounds.width, DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT);
|
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: {
|
this.view = new BrowserView({webPreferences: {
|
||||||
preload,
|
preload,
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ export class DownloadsDropdownView {
|
||||||
* the downloads dropdown at the correct position
|
* the downloads dropdown at the correct position
|
||||||
*/
|
*/
|
||||||
private updateWindowBounds = (newBounds: Electron.Rectangle) => {
|
private updateWindowBounds = (newBounds: Electron.Rectangle) => {
|
||||||
log.debug('updateWindowBounds');
|
log.silly('updateWindowBounds');
|
||||||
|
|
||||||
this.windowBounds = newBounds;
|
this.windowBounds = newBounds;
|
||||||
this.updateDownloadsDropdown();
|
this.updateDownloadsDropdown();
|
||||||
|
@ -85,13 +85,13 @@ export class DownloadsDropdownView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateDownloadsDropdownMenuItem = (event: IpcMainEvent, item?: DownloadedItem) => {
|
private updateDownloadsDropdownMenuItem = (event: IpcMainEvent, item?: DownloadedItem) => {
|
||||||
log.debug('updateDownloadsDropdownMenuItem', {item});
|
log.silly('updateDownloadsDropdownMenuItem', {item});
|
||||||
this.item = item;
|
this.item = item;
|
||||||
this.updateDownloadsDropdown();
|
this.updateDownloadsDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateDownloadsDropdown = () => {
|
private updateDownloadsDropdown = () => {
|
||||||
log.debug('updateDownloadsDropdown');
|
log.silly('updateDownloadsDropdown');
|
||||||
|
|
||||||
this.view?.webContents.send(
|
this.view?.webContents.send(
|
||||||
UPDATE_DOWNLOADS_DROPDOWN,
|
UPDATE_DOWNLOADS_DROPDOWN,
|
||||||
|
@ -117,7 +117,7 @@ export class DownloadsDropdownView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClose = () => {
|
private handleClose = () => {
|
||||||
log.debug('handleClose');
|
log.silly('handleClose');
|
||||||
|
|
||||||
this.view?.setBounds(this.getBounds(this.windowBounds?.width ?? 0, 0, 0));
|
this.view?.setBounds(this.getBounds(this.windowBounds?.width ?? 0, 0, 0));
|
||||||
downloadsManager.onClose();
|
downloadsManager.onClose();
|
||||||
|
|
|
@ -77,7 +77,7 @@ export class LoadingScreen {
|
||||||
}
|
}
|
||||||
|
|
||||||
private create = () => {
|
private create = () => {
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('internalAPI.js');
|
||||||
this.view = new BrowserView({webPreferences: {
|
this.view = new BrowserView({webPreferences: {
|
||||||
preload,
|
preload,
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class ServerDropdownView {
|
||||||
|
|
||||||
private init = () => {
|
private init = () => {
|
||||||
log.info('init');
|
log.info('init');
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('internalAPI.js');
|
||||||
this.view = new BrowserView({webPreferences: {
|
this.view = new BrowserView({webPreferences: {
|
||||||
preload,
|
preload,
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ export class ServerDropdownView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleClose = () => {
|
private handleClose = () => {
|
||||||
log.debug('handleClose');
|
log.silly('handleClose');
|
||||||
|
|
||||||
this.view?.setBounds(this.getBounds(0, 0));
|
this.view?.setBounds(this.getBounds(0, 0));
|
||||||
MainWindow.sendToRenderer(CLOSE_SERVERS_DROPDOWN);
|
MainWindow.sendToRenderer(CLOSE_SERVERS_DROPDOWN);
|
||||||
|
|
|
@ -407,6 +407,7 @@ describe('main/views/viewManager', () => {
|
||||||
];
|
];
|
||||||
const view1 = {
|
const view1 = {
|
||||||
id: 'server-1_view-messaging',
|
id: 'server-1_view-messaging',
|
||||||
|
webContentsId: 1,
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
view: {
|
view: {
|
||||||
type: TAB_MESSAGING,
|
type: TAB_MESSAGING,
|
||||||
|
@ -415,10 +416,12 @@ describe('main/views/viewManager', () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
|
updateHistoryButton: jest.fn(),
|
||||||
};
|
};
|
||||||
const view2 = {
|
const view2 = {
|
||||||
...view1,
|
...view1,
|
||||||
id: 'server-1_other_type_1',
|
id: 'server-1_other_type_1',
|
||||||
|
webContentsId: 2,
|
||||||
view: {
|
view: {
|
||||||
...view1.view,
|
...view1.view,
|
||||||
type: 'other_type_1',
|
type: 'other_type_1',
|
||||||
|
@ -427,6 +430,7 @@ describe('main/views/viewManager', () => {
|
||||||
const view3 = {
|
const view3 = {
|
||||||
...view1,
|
...view1,
|
||||||
id: 'server-1_other_type_2',
|
id: 'server-1_other_type_2',
|
||||||
|
webContentsId: 3,
|
||||||
view: {
|
view: {
|
||||||
...view1.view,
|
...view1.view,
|
||||||
type: 'other_type_2',
|
type: 'other_type_2',
|
||||||
|
@ -442,6 +446,7 @@ describe('main/views/viewManager', () => {
|
||||||
viewManager.getView = (viewId) => views.get(viewId);
|
viewManager.getView = (viewId) => views.get(viewId);
|
||||||
viewManager.isViewClosed = (viewId) => closedViews.has(viewId);
|
viewManager.isViewClosed = (viewId) => closedViews.has(viewId);
|
||||||
viewManager.openClosedView = jest.fn();
|
viewManager.openClosedView = jest.fn();
|
||||||
|
viewManager.getViewByWebContentsId = (webContentsId) => [...views.values()].find((view) => view.webContentsId === webContentsId);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ServerManager.getAllServers.mockReturnValue(servers);
|
ServerManager.getAllServers.mockReturnValue(servers);
|
||||||
|
@ -460,19 +465,19 @@ describe('main/views/viewManager', () => {
|
||||||
views.set(name, view);
|
views.set(name, view);
|
||||||
});
|
});
|
||||||
ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_other_type_2'});
|
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');
|
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', () => {
|
it('should open redirect view if different from current view', () => {
|
||||||
ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_other_type_1'});
|
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');
|
expect(viewManager.showById).toBeCalledWith('server-1_other_type_1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore redirects to "/" to Messages from other views', () => {
|
it('should ignore redirects to "/" to Messages from other views', () => {
|
||||||
ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_view-messaging'});
|
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();
|
expect(view1.sendToRenderer).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent, Event} from 'electron';
|
import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
|
||||||
import isDev from 'electron-is-dev';
|
import isDev from 'electron-is-dev';
|
||||||
|
|
||||||
import ServerViewState from 'app/serverViewState';
|
import ServerViewState from 'app/serverViewState';
|
||||||
|
@ -19,7 +19,6 @@ import {
|
||||||
UPDATE_URL_VIEW_WIDTH,
|
UPDATE_URL_VIEW_WIDTH,
|
||||||
SERVERS_UPDATE,
|
SERVERS_UPDATE,
|
||||||
REACT_APP_INITIALIZED,
|
REACT_APP_INITIALIZED,
|
||||||
BROWSER_HISTORY_BUTTON,
|
|
||||||
APP_LOGGED_OUT,
|
APP_LOGGED_OUT,
|
||||||
APP_LOGGED_IN,
|
APP_LOGGED_IN,
|
||||||
RELOAD_CURRENT_VIEW,
|
RELOAD_CURRENT_VIEW,
|
||||||
|
@ -32,6 +31,9 @@ import {
|
||||||
MAIN_WINDOW_FOCUSED,
|
MAIN_WINDOW_FOCUSED,
|
||||||
SWITCH_TAB,
|
SWITCH_TAB,
|
||||||
GET_IS_DEV_MODE,
|
GET_IS_DEV_MODE,
|
||||||
|
REQUEST_BROWSER_HISTORY_STATUS,
|
||||||
|
LEGACY_OFF,
|
||||||
|
UNREADS_AND_MENTIONS,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
@ -70,15 +72,17 @@ export class ViewManager {
|
||||||
MainWindow.on(MAIN_WINDOW_FOCUSED, this.focusCurrentView);
|
MainWindow.on(MAIN_WINDOW_FOCUSED, this.focusCurrentView);
|
||||||
ipcMain.handle(GET_VIEW_INFO_FOR_TEST, this.handleGetViewInfoForTest);
|
ipcMain.handle(GET_VIEW_INFO_FOR_TEST, this.handleGetViewInfoForTest);
|
||||||
ipcMain.handle(GET_IS_DEV_MODE, () => isDev);
|
ipcMain.handle(GET_IS_DEV_MODE, () => isDev);
|
||||||
|
ipcMain.handle(REQUEST_BROWSER_HISTORY_STATUS, this.handleRequestBrowserHistoryStatus);
|
||||||
ipcMain.on(HISTORY, this.handleHistory);
|
ipcMain.on(HISTORY, this.handleHistory);
|
||||||
ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized);
|
ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized);
|
||||||
ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush);
|
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_IN, this.handleAppLoggedIn);
|
||||||
ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut);
|
ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut);
|
||||||
ipcMain.on(RELOAD_CURRENT_VIEW, this.handleReloadCurrentView);
|
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(SESSION_EXPIRED, this.handleSessionExpired);
|
||||||
|
ipcMain.on(LEGACY_OFF, this.handleLegacyOff);
|
||||||
|
|
||||||
ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId));
|
ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId));
|
||||||
|
|
||||||
|
@ -326,7 +330,7 @@ export class ViewManager {
|
||||||
}
|
}
|
||||||
if (url && url !== '') {
|
if (url && url !== '') {
|
||||||
const urlString = typeof url === 'string' ? url : url.toString();
|
const urlString = typeof url === 'string' ? url : url.toString();
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('internalAPI.js');
|
||||||
const urlView = new BrowserView({
|
const urlView = new BrowserView({
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload,
|
preload,
|
||||||
|
@ -470,18 +474,18 @@ export class ViewManager {
|
||||||
this.getCurrentView()?.goToOffset(offset);
|
this.getCurrentView()?.goToOffset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAppLoggedIn = (event: IpcMainEvent, viewId: string) => {
|
private handleAppLoggedIn = (event: IpcMainEvent) => {
|
||||||
this.getView(viewId)?.onLogin(true);
|
this.getViewByWebContentsId(event.sender.id)?.onLogin(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleAppLoggedOut = (event: IpcMainEvent, viewId: string) => {
|
private handleAppLoggedOut = (event: IpcMainEvent) => {
|
||||||
this.getView(viewId)?.onLogin(false);
|
this.getViewByWebContentsId(event.sender.id)?.onLogin(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleBrowserHistoryPush = (e: IpcMainEvent, viewId: string, pathName: string) => {
|
private handleBrowserHistoryPush = (e: IpcMainEvent, pathName: string) => {
|
||||||
log.debug('handleBrowserHistoryPush', {viewId, pathName});
|
log.debug('handleBrowserHistoryPush', e.sender.id, pathName);
|
||||||
|
|
||||||
const currentView = this.getView(viewId);
|
const currentView = this.getViewByWebContentsId(e.sender.id);
|
||||||
if (!currentView) {
|
if (!currentView) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -489,7 +493,7 @@ export class ViewManager {
|
||||||
if (currentView.view.server.url.pathname !== '/' && pathName.startsWith(currentView.view.server.url.pathname)) {
|
if (currentView.view.server.url.pathname !== '/' && pathName.startsWith(currentView.view.server.url.pathname)) {
|
||||||
cleanedPathName = pathName.replace(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 (this.isViewClosed(redirectedviewId)) {
|
||||||
// If it's a closed view, just open it and stop
|
// If it's a closed view, just open it and stop
|
||||||
this.openClosedView(redirectedviewId, `${currentView.view.server.url}${cleanedPathName}`);
|
this.openClosedView(redirectedviewId, `${currentView.view.server.url}${cleanedPathName}`);
|
||||||
|
@ -497,8 +501,8 @@ export class ViewManager {
|
||||||
}
|
}
|
||||||
let redirectedView = this.getView(redirectedviewId) || currentView;
|
let redirectedView = this.getView(redirectedviewId) || currentView;
|
||||||
if (redirectedView !== currentView && redirectedView?.view.server.id === ServerViewState.getCurrentServer().id && redirectedView?.isLoggedIn) {
|
if (redirectedView !== currentView && redirectedView?.view.server.id === ServerViewState.getCurrentServer().id && redirectedView?.isLoggedIn) {
|
||||||
log.info('redirecting to a new view', redirectedView?.id || viewId);
|
log.info('redirecting to a new view', redirectedView?.id || currentView.id);
|
||||||
this.showById(redirectedView?.id || viewId);
|
this.showById(redirectedView?.id || currentView.id);
|
||||||
} else {
|
} else {
|
||||||
redirectedView = currentView;
|
redirectedView = currentView;
|
||||||
}
|
}
|
||||||
|
@ -506,20 +510,20 @@ export class ViewManager {
|
||||||
// Special case check for Channels to not force a redirect to "/", causing a refresh
|
// Special case check for Channels to not force a redirect to "/", causing a refresh
|
||||||
if (!(redirectedView !== currentView && redirectedView?.view.type === TAB_MESSAGING && cleanedPathName === '/')) {
|
if (!(redirectedView !== currentView && redirectedView?.view.type === TAB_MESSAGING && cleanedPathName === '/')) {
|
||||||
redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName);
|
redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName);
|
||||||
if (redirectedView) {
|
redirectedView?.updateHistoryButton();
|
||||||
this.handleBrowserHistoryButton(e, redirectedView.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleBrowserHistoryButton = (e: IpcMainEvent, viewId: string) => {
|
private handleRequestBrowserHistoryStatus = (e: IpcMainInvokeEvent) => {
|
||||||
this.getView(viewId)?.updateHistoryButton();
|
log.silly('handleRequestBrowserHistoryStatus', e.sender.id);
|
||||||
|
|
||||||
|
return this.getViewByWebContentsId(e.sender.id)?.getBrowserHistoryStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleReactAppInitialized = (e: IpcMainEvent, viewId: string) => {
|
private handleReactAppInitialized = (e: IpcMainEvent) => {
|
||||||
log.debug('handleReactAppInitialized', viewId);
|
log.debug('handleReactAppInitialized', e.sender.id);
|
||||||
|
|
||||||
const view = this.views.get(viewId);
|
const view = this.getViewByWebContentsId(e.sender.id);
|
||||||
if (view) {
|
if (view) {
|
||||||
view.setInitialized();
|
view.setInitialized();
|
||||||
if (this.getCurrentView() === view) {
|
if (this.getCurrentView() === view) {
|
||||||
|
@ -539,18 +543,47 @@ export class ViewManager {
|
||||||
this.showById(view?.id);
|
this.showById(view?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if favicon is null, it means it is the initial load,
|
private handleLegacyOff = (e: IpcMainEvent) => {
|
||||||
// so don't memoize as we don't have the favicons and there is no rush to find out.
|
log.silly('handleLegacyOff', {webContentsId: e.sender.id});
|
||||||
private handleFaviconIsUnread = (e: Event, favicon: string, viewId: string, result: boolean) => {
|
|
||||||
log.silly('handleFaviconIsUnread', {favicon, viewId, result});
|
|
||||||
|
|
||||||
AppState.updateUnreads(viewId, result);
|
const view = this.getViewByWebContentsId(e.sender.id);
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
view.offLegacyUnreads();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSessionExpired = (event: IpcMainEvent, isExpired: boolean, viewId: string) => {
|
// if favicon is null, it means it is the initial load,
|
||||||
ServerManager.getViewLog(viewId, 'ViewManager').debug('handleSessionExpired', isExpired);
|
// 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) => {
|
private handleSetCurrentViewBounds = (newBounds: Electron.Rectangle) => {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
isHelpUrl,
|
isHelpUrl,
|
||||||
isImageProxyUrl,
|
isImageProxyUrl,
|
||||||
isInternalURL,
|
isInternalURL,
|
||||||
|
isLoginUrl,
|
||||||
isManagedResource,
|
isManagedResource,
|
||||||
isPluginUrl,
|
isPluginUrl,
|
||||||
isPublicFilesUrl,
|
isPublicFilesUrl,
|
||||||
|
@ -91,7 +92,9 @@ export class WebContentsEventManager {
|
||||||
const parsedURL = parseURL(url)!;
|
const parsedURL = parseURL(url)!;
|
||||||
const serverURL = this.getServerURLFromWebContentsId(webContentsId);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,11 @@
|
||||||
|
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
import {BrowserWindow, desktopCapturer, systemPreferences} from 'electron';
|
import {BrowserWindow, desktopCapturer, systemPreferences, ipcMain} from 'electron';
|
||||||
|
|
||||||
import ServerViewState from 'app/serverViewState';
|
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 {
|
import {
|
||||||
MINIMUM_CALLS_WIDGET_WIDTH,
|
MINIMUM_CALLS_WIDGET_WIDTH,
|
||||||
MINIMUM_CALLS_WIDGET_HEIGHT,
|
MINIMUM_CALLS_WIDGET_HEIGHT,
|
||||||
|
@ -62,6 +62,7 @@ jest.mock('app/serverViewState', () => ({
|
||||||
}));
|
}));
|
||||||
jest.mock('main/views/viewManager', () => ({
|
jest.mock('main/views/viewManager', () => ({
|
||||||
getView: jest.fn(),
|
getView: jest.fn(),
|
||||||
|
getViewByWebContentsId: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('../utils', () => ({
|
jest.mock('../utils', () => ({
|
||||||
openScreensharePermissionsSettingsMacOS: jest.fn(),
|
openScreensharePermissionsSettingsMacOS: jest.fn(),
|
||||||
|
@ -187,11 +188,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
it('should resize correctly', () => {
|
it('should resize correctly', () => {
|
||||||
callsWidgetWindow.handleResize({
|
callsWidgetWindow.handleResize({
|
||||||
sender: {id: 'windowID'},
|
sender: {id: 'windowID'},
|
||||||
}, 'widget', {
|
}, 300, 100);
|
||||||
element: 'calls-widget',
|
|
||||||
width: 300,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({
|
expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({
|
||||||
x: 12,
|
x: 12,
|
||||||
y: 720 - (100 - MINIMUM_CALLS_WIDGET_HEIGHT),
|
y: 720 - (100 - MINIMUM_CALLS_WIDGET_HEIGHT),
|
||||||
|
@ -204,11 +201,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
callsWidgetWindow.win.webContents.getZoomFactor.mockReturnValue(2.0);
|
callsWidgetWindow.win.webContents.getZoomFactor.mockReturnValue(2.0);
|
||||||
callsWidgetWindow.handleResize({
|
callsWidgetWindow.handleResize({
|
||||||
sender: {id: 'windowID'},
|
sender: {id: 'windowID'},
|
||||||
}, 'widget', {
|
}, 300, 100);
|
||||||
element: 'calls-widget',
|
|
||||||
width: 300,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({
|
expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({
|
||||||
x: 12,
|
x: 12,
|
||||||
y: 720 - (200 - MINIMUM_CALLS_WIDGET_HEIGHT),
|
y: 720 - (200 - MINIMUM_CALLS_WIDGET_HEIGHT),
|
||||||
|
@ -221,11 +214,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
callsWidgetWindow.win.webContents.getZoomFactor.mockReturnValue(0.5);
|
callsWidgetWindow.win.webContents.getZoomFactor.mockReturnValue(0.5);
|
||||||
callsWidgetWindow.handleResize({
|
callsWidgetWindow.handleResize({
|
||||||
sender: {id: 'windowID'},
|
sender: {id: 'windowID'},
|
||||||
}, 'widget', {
|
}, 300, 100);
|
||||||
element: 'calls-widget',
|
|
||||||
width: 300,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({
|
expect(callsWidgetWindow.setBounds).toHaveBeenCalledWith({
|
||||||
x: 12,
|
x: 12,
|
||||||
y: 720 - (50 - MINIMUM_CALLS_WIDGET_HEIGHT),
|
y: 720 - (50 - MINIMUM_CALLS_WIDGET_HEIGHT),
|
||||||
|
@ -288,51 +277,26 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
it('handleShareScreen', () => {
|
it('handleShareScreen', () => {
|
||||||
const callsWidgetWindow = new CallsWidgetWindow();
|
const callsWidgetWindow = new CallsWidgetWindow();
|
||||||
callsWidgetWindow.isAllowedEvent = jest.fn();
|
callsWidgetWindow.isAllowedEvent = jest.fn();
|
||||||
|
callsWidgetWindow.mainView = {
|
||||||
|
webContentsId: 'goodID',
|
||||||
|
};
|
||||||
callsWidgetWindow.win = {
|
callsWidgetWindow.win = {
|
||||||
webContents: {
|
webContents: {
|
||||||
id: 'goodID',
|
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const message = {
|
|
||||||
callID: 'test-call-id',
|
|
||||||
};
|
|
||||||
|
|
||||||
callsWidgetWindow.isAllowedEvent.mockReturnValue(false);
|
callsWidgetWindow.isAllowedEvent.mockReturnValue(false);
|
||||||
callsWidgetWindow.handleShareScreen({
|
callsWidgetWindow.handleShareScreen({
|
||||||
sender: {id: 'badID'},
|
sender: {id: 'badID'},
|
||||||
}, message);
|
}, 'sourceId', true);
|
||||||
expect(callsWidgetWindow.win.webContents.send).not.toHaveBeenCalled();
|
expect(callsWidgetWindow.win.webContents.send).not.toHaveBeenCalled();
|
||||||
|
|
||||||
callsWidgetWindow.isAllowedEvent.mockReturnValue(true);
|
callsWidgetWindow.isAllowedEvent.mockReturnValue(true);
|
||||||
callsWidgetWindow.handleShareScreen({
|
callsWidgetWindow.handleShareScreen({
|
||||||
sender: {id: 'goodID'},
|
sender: {id: 'goodID'},
|
||||||
}, 'widget', message);
|
}, 'sourceId', true);
|
||||||
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith(CALLS_WIDGET_SHARE_SCREEN, message);
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith(CALLS_WIDGET_SHARE_SCREEN, 'sourceId', true);
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onPopOutOpen', () => {
|
describe('onPopOutOpen', () => {
|
||||||
|
@ -451,42 +415,6 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
expect(callsWidgetWindow.getViewURL().toString()).toBe('http://localhost:8065/');
|
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', () => {
|
it('onNavigate', () => {
|
||||||
const callsWidgetWindow = new CallsWidgetWindow();
|
const callsWidgetWindow = new CallsWidgetWindow();
|
||||||
callsWidgetWindow.getWidgetURL = () => 'http://localhost:8065';
|
callsWidgetWindow.getWidgetURL = () => 'http://localhost:8065';
|
||||||
|
@ -510,11 +438,12 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
url: new URL('http://server-1.com'),
|
url: new URL('http://server-1.com'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
webContentsId: 2,
|
||||||
};
|
};
|
||||||
const browserWindow = {
|
const browserWindow = {
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
once: jest.fn(),
|
once: jest.fn(),
|
||||||
loadURL: jest.fn().mockReturnValue(Promise.resolve()),
|
loadURL: jest.fn(),
|
||||||
webContents: {
|
webContents: {
|
||||||
setWindowOpenHandler: jest.fn(),
|
setWindowOpenHandler: jest.fn(),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
|
@ -524,9 +453,18 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
let func;
|
||||||
|
ipcMain.on.mockImplementation((_, callback) => {
|
||||||
|
func = callback;
|
||||||
|
});
|
||||||
|
browserWindow.loadURL.mockImplementation(() => {
|
||||||
|
func({sender: {id: 1}}, 'test');
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
BrowserWindow.mockReturnValue(browserWindow);
|
BrowserWindow.mockReturnValue(browserWindow);
|
||||||
callsWidgetWindow.close.mockReturnValue(Promise.resolve());
|
callsWidgetWindow.close.mockReturnValue(Promise.resolve());
|
||||||
ViewManager.getView.mockReturnValue(view);
|
callsWidgetWindow.getWidgetURL.mockReturnValue('http://server-1.com/widget');
|
||||||
|
ViewManager.getViewByWebContentsId.mockReturnValue(view);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -539,12 +477,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
|
|
||||||
it('should create calls widget window', async () => {
|
it('should create calls widget window', async () => {
|
||||||
expect(callsWidgetWindow.win).toBeUndefined();
|
expect(callsWidgetWindow.win).toBeUndefined();
|
||||||
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test'});
|
await callsWidgetWindow.handleCreateCallsWidgetWindow({sender: {id: 2}}, {callID: 'test'});
|
||||||
expect(callsWidgetWindow.win).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create with correct initial configuration', async () => {
|
|
||||||
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test'});
|
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
width: MINIMUM_CALLS_WIDGET_WIDTH,
|
width: MINIMUM_CALLS_WIDGET_WIDTH,
|
||||||
height: MINIMUM_CALLS_WIDGET_HEIGHT,
|
height: MINIMUM_CALLS_WIDGET_HEIGHT,
|
||||||
|
@ -556,6 +489,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
backgroundColor: '#00ffffff',
|
backgroundColor: '#00ffffff',
|
||||||
}));
|
}));
|
||||||
|
expect(callsWidgetWindow.win).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should catch error when failing to load the URL', async () => {
|
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 () => {
|
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.win = window;
|
||||||
callsWidgetWindow.options = {callID: 'test'};
|
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);
|
expect(callsWidgetWindow.win).toEqual(window);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new window if switching calls', async () => {
|
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.win = window;
|
||||||
callsWidgetWindow.getCallID = jest.fn(() => 'test');
|
callsWidgetWindow.options = {callID: 'test'};
|
||||||
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test2'});
|
await callsWidgetWindow.handleCreateCallsWidgetWindow({sender: {id: 2}}, {callID: 'test2'});
|
||||||
expect(callsWidgetWindow.win).not.toEqual(window);
|
expect(callsWidgetWindow.win).not.toEqual(window);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -627,10 +571,13 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
lastActiveView: 2,
|
lastActiveView: 2,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
let index = 0;
|
||||||
const map = servers.reduce((arr, item) => {
|
const map = servers.reduce((arr, item) => {
|
||||||
item.views.forEach((view) => {
|
item.views.forEach((view) => {
|
||||||
|
index++;
|
||||||
arr.push([`${item.name}_${view.name}`, {
|
arr.push([`${item.name}_${view.name}`, {
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
|
webContentsId: index,
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
return arr;
|
return arr;
|
||||||
|
@ -638,7 +585,8 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
const views = new Map(map);
|
const views = new Map(map);
|
||||||
|
|
||||||
beforeEach(() => {
|
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(() => {
|
afterEach(() => {
|
||||||
|
@ -662,9 +610,8 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null);
|
const sources = await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null);
|
||||||
|
expect(sources).toEqual([
|
||||||
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('desktop-sources-result', [
|
|
||||||
{
|
{
|
||||||
id: 'screen0',
|
id: 'screen0',
|
||||||
},
|
},
|
||||||
|
@ -676,11 +623,11 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
|
|
||||||
it('should send error with no sources', async () => {
|
it('should send error with no sources', async () => {
|
||||||
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([]);
|
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', {
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
|
||||||
err: 'screen-permissions',
|
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',
|
err: 'screen-permissions',
|
||||||
});
|
});
|
||||||
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
|
||||||
|
@ -697,7 +644,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
]);
|
]);
|
||||||
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
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(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen');
|
||||||
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
|
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
|
||||||
|
@ -726,7 +673,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
]);
|
]);
|
||||||
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
|
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(callsWidgetWindow.missingScreensharePermissions).toBe(true);
|
||||||
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(1);
|
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(1);
|
||||||
|
@ -738,7 +685,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
err: 'screen-permissions',
|
err: 'screen-permissions',
|
||||||
});
|
});
|
||||||
|
|
||||||
await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null);
|
await callsWidgetWindow.handleGetDesktopSources({sender: {id: 1}}, null);
|
||||||
|
|
||||||
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(2);
|
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(2);
|
||||||
expect(openScreensharePermissionsSettingsMacOS).toHaveBeenCalledTimes(1);
|
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', () => {
|
describe('handleCallsWidgetChannelLinkClick', () => {
|
||||||
const callsWidgetWindow = new CallsWidgetWindow();
|
const callsWidgetWindow = new CallsWidgetWindow();
|
||||||
|
callsWidgetWindow.win = {webContents: {id: 1}};
|
||||||
callsWidgetWindow.mainView = {
|
callsWidgetWindow.mainView = {
|
||||||
view: {
|
view: {
|
||||||
server: {
|
server: {
|
||||||
|
@ -877,89 +759,12 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should switch server', () => {
|
it('should switch server', () => {
|
||||||
callsWidgetWindow.handleCallsWidgetChannelLinkClick();
|
callsWidgetWindow.handleCallsWidgetChannelLinkClick({sender: {id: 1}});
|
||||||
expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-2');
|
expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleCallsError', () => {
|
describe('forwardToMainApp', () => {
|
||||||
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', () => {
|
|
||||||
const view = {
|
const view = {
|
||||||
view: {
|
view: {
|
||||||
server: {
|
server: {
|
||||||
|
@ -970,6 +775,7 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
};
|
};
|
||||||
const callsWidgetWindow = new CallsWidgetWindow();
|
const callsWidgetWindow = new CallsWidgetWindow();
|
||||||
callsWidgetWindow.mainView = view;
|
callsWidgetWindow.mainView = view;
|
||||||
|
callsWidgetWindow.win = {webContents: {id: 1}};
|
||||||
|
|
||||||
const focus = jest.fn();
|
const focus = jest.fn();
|
||||||
|
|
||||||
|
@ -982,11 +788,12 @@ describe('main/windows/callsWidgetWindow', () => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass through the join call callID to the webapp', () => {
|
it('should pass through the arguments to the webapp', () => {
|
||||||
callsWidgetWindow.handleCallsJoinRequest('', {callID: 'thecallchannelid'});
|
const func = callsWidgetWindow.forwardToMainApp('some-channel');
|
||||||
|
func({sender: {id: 1}}, 'thecallchannelid');
|
||||||
expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-1');
|
expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-1');
|
||||||
expect(focus).toHaveBeenCalled();
|
expect(focus).toHaveBeenCalled();
|
||||||
expect(view.sendToRenderer).toBeCalledWith(CALLS_JOIN_REQUEST, {callID: 'thecallchannelid'});
|
expect(view.sendToRenderer).toBeCalledWith('some-channel', 'thecallchannelid');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {BrowserWindow, desktopCapturer, ipcMain, IpcMainEvent, Rectangle, systemPreferences, Event} from 'electron';
|
import {BrowserWindow, desktopCapturer, ipcMain, IpcMainEvent, Rectangle, systemPreferences, Event, IpcMainInvokeEvent} from 'electron';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CallsErrorMessage,
|
|
||||||
CallsEventHandler,
|
|
||||||
CallsJoinCallMessage,
|
CallsJoinCallMessage,
|
||||||
CallsJoinedCallMessage,
|
|
||||||
CallsJoinRequestMessage,
|
|
||||||
CallsLinkClickMessage,
|
|
||||||
CallsWidgetResizeMessage,
|
|
||||||
CallsWidgetShareScreenMessage,
|
|
||||||
CallsWidgetWindowConfig,
|
CallsWidgetWindowConfig,
|
||||||
} from 'types/calls';
|
} from 'types/calls';
|
||||||
|
|
||||||
|
@ -34,8 +27,7 @@ import {
|
||||||
CALLS_WIDGET_RESIZE,
|
CALLS_WIDGET_RESIZE,
|
||||||
CALLS_WIDGET_SHARE_SCREEN,
|
CALLS_WIDGET_SHARE_SCREEN,
|
||||||
DESKTOP_SOURCES_MODAL_REQUEST,
|
DESKTOP_SOURCES_MODAL_REQUEST,
|
||||||
DESKTOP_SOURCES_RESULT,
|
GET_DESKTOP_SOURCES,
|
||||||
DISPATCH_GET_DESKTOP_SOURCES,
|
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
import {MattermostBrowserView} from 'main/views/MattermostBrowserView';
|
import {MattermostBrowserView} from 'main/views/MattermostBrowserView';
|
||||||
|
@ -68,16 +60,19 @@ export class CallsWidgetWindow {
|
||||||
constructor() {
|
constructor() {
|
||||||
ipcMain.on(CALLS_WIDGET_RESIZE, this.handleResize);
|
ipcMain.on(CALLS_WIDGET_RESIZE, this.handleResize);
|
||||||
ipcMain.on(CALLS_WIDGET_SHARE_SCREEN, this.handleShareScreen);
|
ipcMain.on(CALLS_WIDGET_SHARE_SCREEN, this.handleShareScreen);
|
||||||
ipcMain.on(CALLS_JOINED_CALL, this.handleJoinedCall);
|
|
||||||
ipcMain.on(CALLS_POPOUT_FOCUS, this.handlePopOutFocus);
|
ipcMain.on(CALLS_POPOUT_FOCUS, this.handlePopOutFocus);
|
||||||
ipcMain.on(DISPATCH_GET_DESKTOP_SOURCES, this.genCallsEventHandler(this.handleGetDesktopSources));
|
ipcMain.handle(GET_DESKTOP_SOURCES, this.handleGetDesktopSources);
|
||||||
ipcMain.on(DESKTOP_SOURCES_MODAL_REQUEST, this.genCallsEventHandler(this.handleDesktopSourcesModalRequest));
|
ipcMain.handle(CALLS_JOIN_CALL, this.handleCreateCallsWidgetWindow);
|
||||||
ipcMain.on(CALLS_JOIN_CALL, this.genCallsEventHandler(this.handleCreateCallsWidgetWindow));
|
ipcMain.on(CALLS_LEAVE_CALL, this.handleCallsLeave);
|
||||||
ipcMain.on(CALLS_LEAVE_CALL, this.genCallsEventHandler(this.handleCallsLeave));
|
|
||||||
ipcMain.on(CALLS_WIDGET_CHANNEL_LINK_CLICK, this.genCallsEventHandler(this.handleCallsWidgetChannelLinkClick));
|
// forwards to the main app
|
||||||
ipcMain.on(CALLS_ERROR, this.genCallsEventHandler(this.handleCallsError));
|
ipcMain.on(DESKTOP_SOURCES_MODAL_REQUEST, this.forwardToMainApp(DESKTOP_SOURCES_MODAL_REQUEST));
|
||||||
ipcMain.on(CALLS_LINK_CLICK, this.genCallsEventHandler(this.handleCallsLinkClick));
|
ipcMain.on(CALLS_ERROR, this.forwardToMainApp(CALLS_ERROR));
|
||||||
ipcMain.on(CALLS_JOIN_REQUEST, this.genCallsEventHandler(this.handleCallsJoinRequest));
|
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,
|
hasShadow: false,
|
||||||
backgroundColor: '#00ffffff',
|
backgroundColor: '#00ffffff',
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: getLocalPreload('callsWidget.js'),
|
preload: getLocalPreload('externalAPI.js'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.mainView = view;
|
this.mainView = view;
|
||||||
|
@ -203,28 +198,6 @@ export class CallsWidgetWindow {
|
||||||
this.boundsErr = Utils.boundsDiff(bounds, this.win.getBounds());
|
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
|
* BrowserWindow/WebContents handlers
|
||||||
*/
|
*/
|
||||||
|
@ -333,15 +306,15 @@ export class CallsWidgetWindow {
|
||||||
* IPC HANDLERS
|
* IPC HANDLERS
|
||||||
************************/
|
************************/
|
||||||
|
|
||||||
private handleResize = (ev: IpcMainEvent, _: string, msg: CallsWidgetResizeMessage) => {
|
private handleResize = (ev: IpcMainEvent, width: number, height: number) => {
|
||||||
log.debug('onResize', msg);
|
log.debug('handleResize', width, height);
|
||||||
|
|
||||||
if (!this.win) {
|
if (!this.win) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.isAllowedEvent(ev)) {
|
if (!this.isCallsWidget(ev.sender.id)) {
|
||||||
log.warn('onResize', 'Disallowed calls event');
|
log.debug('handleResize', 'Disallowed calls event');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -349,34 +322,23 @@ export class CallsWidgetWindow {
|
||||||
const currBounds = this.win.getBounds();
|
const currBounds = this.win.getBounds();
|
||||||
const newBounds = {
|
const newBounds = {
|
||||||
x: currBounds.x,
|
x: currBounds.x,
|
||||||
y: currBounds.y - (Math.ceil(msg.height * zoomFactor) - currBounds.height),
|
y: currBounds.y - (Math.ceil(height * zoomFactor) - currBounds.height),
|
||||||
width: Math.ceil(msg.width * zoomFactor),
|
width: Math.ceil(width * zoomFactor),
|
||||||
height: Math.ceil(msg.height * zoomFactor),
|
height: Math.ceil(height * zoomFactor),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setBounds(newBounds);
|
this.setBounds(newBounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleShareScreen = (ev: IpcMainEvent, _: string, message: CallsWidgetShareScreenMessage) => {
|
private handleShareScreen = (ev: IpcMainEvent, sourceID: string, withAudio: boolean) => {
|
||||||
log.debug('handleShareScreen');
|
log.debug('handleShareScreen', {sourceID, withAudio});
|
||||||
|
|
||||||
if (!this.isAllowedEvent(ev)) {
|
if (this.mainView?.webContentsId !== ev.sender.id) {
|
||||||
log.warn('Disallowed calls event');
|
log.debug('handleShareScreen', 'blocked on wrong webContentsId');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.win?.webContents.send(CALLS_WIDGET_SHARE_SCREEN, message);
|
this.win?.webContents.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio);
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handlePopOutFocus = () => {
|
private handlePopOutFocus = () => {
|
||||||
|
@ -389,13 +351,18 @@ export class CallsWidgetWindow {
|
||||||
this.popOut.focus();
|
this.popOut.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleGetDesktopSources = async (viewId: string, opts: Electron.SourcesOptions) => {
|
private handleGetDesktopSources = async (event: IpcMainInvokeEvent, opts: Electron.SourcesOptions) => {
|
||||||
log.debug('handleGetDesktopSources', opts);
|
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) {
|
if (!view) {
|
||||||
log.error('handleGetDesktopSources: view not found');
|
log.error('handleGetDesktopSources: view not found');
|
||||||
return Promise.resolve();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.platform === 'darwin' && systemPreferences.getMediaAccessStatus('screen') === 'denied') {
|
if (process.platform === 'darwin' && systemPreferences.getMediaAccessStatus('screen') === 'denied') {
|
||||||
|
@ -432,7 +399,7 @@ export class CallsWidgetWindow {
|
||||||
log.info('missing screen permissions');
|
log.info('missing screen permissions');
|
||||||
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
|
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
|
||||||
this.win?.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
|
this.win?.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = sources.map((source) => {
|
const message = sources.map((source) => {
|
||||||
|
@ -443,53 +410,63 @@ export class CallsWidgetWindow {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (message.length > 0) {
|
return message;
|
||||||
view.sendToRenderer(DESKTOP_SOURCES_RESULT, message);
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
log.error('desktopCapturer.getSources failed', err);
|
log.error('desktopCapturer.getSources failed', err);
|
||||||
|
|
||||||
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
|
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
|
||||||
this.win?.webContents.send(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');
|
log.debug('createCallsWidgetWindow');
|
||||||
|
|
||||||
// trying to join again the call we are already in should not be allowed.
|
// trying to join again the call we are already in should not be allowed.
|
||||||
if (this.options?.callID === msg.callID) {
|
if (this.options?.callID === msg.callID) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
// to switch from one call to another we need to wait for the existing
|
// to switch from one call to another we need to wait for the existing
|
||||||
// window to be fully closed.
|
// window to be fully closed.
|
||||||
await this.close();
|
await this.close();
|
||||||
|
|
||||||
const currentView = ViewManager.getView(viewId);
|
const currentView = ViewManager.getViewByWebContentsId(event.sender.id);
|
||||||
if (!currentView) {
|
if (!currentView) {
|
||||||
log.error('unable to create calls widget window: currentView is missing');
|
log.error('unable to create calls widget window: currentView is missing');
|
||||||
|
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;
|
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, {
|
this.init(currentView, {
|
||||||
callID: msg.callID,
|
callID: msg.callID,
|
||||||
title: msg.title,
|
title: msg.title,
|
||||||
rootID: msg.rootID,
|
rootID: msg.rootID,
|
||||||
channelURL: msg.channelURL,
|
channelURL: msg.channelURL,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
private handleDesktopSourcesModalRequest = () => {
|
return promise;
|
||||||
log.debug('handleDesktopSourcesModalRequest');
|
|
||||||
|
|
||||||
if (!this.serverID) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerViewState.switchServer(this.serverID);
|
|
||||||
MainWindow.get()?.focus();
|
|
||||||
this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleCallsLeave = () => {
|
private handleCallsLeave = () => {
|
||||||
|
@ -498,9 +475,34 @@ export class CallsWidgetWindow {
|
||||||
this.close();
|
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');
|
log.debug('handleCallsWidgetChannelLinkClick');
|
||||||
|
|
||||||
|
if (!this.isCallsWidget(event.sender.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.serverID) {
|
if (!this.serverID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -509,41 +511,6 @@ export class CallsWidgetWindow {
|
||||||
MainWindow.get()?.focus();
|
MainWindow.get()?.focus();
|
||||||
this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL);
|
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();
|
const callsWidgetWindow = new CallsWidgetWindow();
|
||||||
|
|
|
@ -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
|
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
disableBlinkFeatures: 'Auxclick',
|
disableBlinkFeatures: 'Auxclick',
|
||||||
preload: getLocalPreload('desktopAPI.js'),
|
preload: getLocalPreload('internalAPI.js'),
|
||||||
spellcheck: typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker,
|
spellcheck: typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -43,7 +43,7 @@ export class SettingsWindow {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('internalAPI.js');
|
||||||
const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker);
|
const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker);
|
||||||
this.win = new BrowserWindow({
|
this.win = new BrowserWindow({
|
||||||
parent: mainWindow,
|
parent: mainWindow,
|
||||||
|
|
|
@ -8,34 +8,3 @@ export type CallsWidgetWindowConfig = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CallsJoinCallMessage = 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<void>);
|
|
||||||
|
|
24
src/types/externalAPI.ts
Normal file
24
src/types/externalAPI.ts
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -3,10 +3,6 @@
|
||||||
|
|
||||||
import {NotificationConstructorOptions} from 'electron/common';
|
import {NotificationConstructorOptions} from 'electron/common';
|
||||||
|
|
||||||
export type MentionData = {
|
export type MentionOptions = NotificationConstructorOptions & {
|
||||||
soundName: string;
|
soundName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MentionOptions = NotificationConstructorOptions & {
|
|
||||||
data: MentionData;
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,9 +15,8 @@ const base = require('./webpack.config.base');
|
||||||
module.exports = merge(base, {
|
module.exports = merge(base, {
|
||||||
entry: {
|
entry: {
|
||||||
index: './src/main/app/index.ts',
|
index: './src/main/app/index.ts',
|
||||||
desktopAPI: './src/main/preload/desktopAPI.js',
|
internalAPI: './src/main/preload/internalAPI.js',
|
||||||
preload: './src/main/preload/mattermost.js',
|
externalAPI: './src/main/preload/externalAPI.ts',
|
||||||
callsWidget: './src/main/preload/callsWidget.js',
|
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
'macos-notification-state': 'require("macos-notification-state")',
|
'macos-notification-state': 'require("macos-notification-state")',
|
||||||
|
|
Loading…
Reference in a new issue