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