diff --git a/i18n/en.json b/i18n/en.json index 97393128..1c0cd443 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -81,6 +81,12 @@ "main.menus.app.view": "&View", "main.menus.app.view.actualSize": "Actual Size", "main.menus.app.view.clearCacheAndReload": "Clear Cache and Reload", + "main.menus.app.view.developerModeBrowserOnly": "Browser Only Mode", + "main.menus.app.view.developerModeDisableContextMenu": "Disable Context Menu", + "main.menus.app.view.developerModeDisableNotificationStorage": "Disable Notification Storage", + "main.menus.app.view.developerModeDisableUserActivityMonitor": "Disable User Activity Monitor", + "main.menus.app.view.developerModeForceLegacyAPI": "Force Legacy API", + "main.menus.app.view.developerModeForceNewAPI": "Force New API", "main.menus.app.view.devToolsAppWrapper": "Developer Tools for Application Wrapper", "main.menus.app.view.devToolsCurrentCallWidget": "Developer Tools for Call Widget", "main.menus.app.view.devToolsCurrentServer": "Developer Tools for Current Server", @@ -151,6 +157,7 @@ "renderer.components.configureServer.url.urlNotMatched": "The server URL provided does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}", "renderer.components.configureServer.url.urlUpdated": "The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}", "renderer.components.configureServer.url.validating": "Validating...", + "renderer.components.developerModeIndicator.tooltip": "Developer mode is enabled. You should only have this enabled if a Mattermost developer has instructed you to.", "renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}", "renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.", "renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:", diff --git a/src/common/communication.ts b/src/common/communication.ts index 49b2bddd..7ad4fbfc 100644 --- a/src/common/communication.ts +++ b/src/common/communication.ts @@ -193,3 +193,7 @@ export const GET_MEDIA_ACCESS_STATUS = 'get-media-access-status'; export const LEGACY_OFF = 'legacy-off'; export const GET_NONCE = 'get-nonce'; + +export const DEVELOPER_MODE_UPDATED = 'developer-mode-updated'; +export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled'; +export const GET_DEVELOPER_MODE_SETTING = 'get-developer-mode-setting'; diff --git a/src/main/UserActivityMonitor.ts b/src/main/UserActivityMonitor.ts index eef410ad..c37b3ab5 100644 --- a/src/main/UserActivityMonitor.ts +++ b/src/main/UserActivityMonitor.ts @@ -82,6 +82,7 @@ export class UserActivityMonitor extends EventEmitter { */ stopMonitoring() { clearInterval(this.systemIdleTimeIntervalID); + this.systemIdleTimeIntervalID = -1; } /** diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index a972b926..b8809082 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -29,6 +29,7 @@ import { TOGGLE_SECURE_INPUT, GET_APP_INFO, SHOW_SETTINGS_WINDOW, + DEVELOPER_MODE_UPDATED, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -43,6 +44,7 @@ import {setupBadge} from 'main/badge'; import CertificateManager from 'main/certificateManager'; import {configPath, updatePaths} from 'main/constants'; import CriticalErrorHandler from 'main/CriticalErrorHandler'; +import DeveloperMode from 'main/developerMode'; import downloadsManager from 'main/downloadsManager'; import i18nManager from 'main/i18nManager'; import NonceManager from 'main/nonceManager'; @@ -405,14 +407,16 @@ async function initializeAfterAppReady() { // Call this to initiate a permissions check for DND state getDoNotDisturb(); - // 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.userIsActive, status.idleTime, status.isSystemEvent); - }); + DeveloperMode.switchOff('disableUserActivityMonitor', () => { + // listen for status updates and pass on to renderer + UserActivityMonitor.on('status', onUserActivityStatus); - // start monitoring user activity (needs to be started after the app is ready) - UserActivityMonitor.startMonitoring(); + // start monitoring user activity (needs to be started after the app is ready) + UserActivityMonitor.startMonitoring(); + }, () => { + UserActivityMonitor.off('status', onUserActivityStatus); + UserActivityMonitor.stopMonitoring(); + }); if (shouldShowTrayIcon()) { Tray.init(Config.trayIconTheme); @@ -430,6 +434,7 @@ async function initializeAfterAppReady() { } handleUpdateMenuEvent(); + DeveloperMode.on(DEVELOPER_MODE_UPDATED, handleUpdateMenuEvent); ipcMain.emit('update-dict'); @@ -445,6 +450,15 @@ async function initializeAfterAppReady() { handleMainWindowIsShown(); } +function onUserActivityStatus(status: { + userIsActive: boolean; + idleTime: number; + isSystemEvent: boolean; +}) { + log.debug('UserActivityMonitor.on(status)', status); + ViewManager.sendToAllViews(USER_ACTIVITY_UPDATE, status.userIsActive, status.idleTime, status.isSystemEvent); +} + function handleStartDownload() { if (updateManager) { updateManager.handleDownload(); diff --git a/src/main/constants.ts b/src/main/constants.ts index 79d053d7..93dffe2b 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -20,6 +20,7 @@ export let boundsInfoPath = ''; export let migrationInfoPath = ''; export let downloadsJson = ''; export let permissionsJson = ''; +export let developerModeJson = ''; export function updatePaths(emit = false) { userDataPath = app.getPath('userData'); @@ -33,6 +34,7 @@ export function updatePaths(emit = false) { migrationInfoPath = path.resolve(userDataPath, 'migration-info.json'); downloadsJson = path.resolve(userDataPath, 'downloads.json'); permissionsJson = path.resolve(userDataPath, 'permissions.json'); + developerModeJson = path.resolve(userDataPath, 'developerMode.json'); if (emit) { ipcMain.emit(UPDATE_PATHS); diff --git a/src/main/developerMode.test.js b/src/main/developerMode.test.js new file mode 100644 index 00000000..a713d94c --- /dev/null +++ b/src/main/developerMode.test.js @@ -0,0 +1,34 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {DeveloperMode} from './developerMode'; + +jest.mock('fs', () => ({ + readFileSync: jest.fn(), + writeFile: jest.fn(), +})); + +jest.mock('electron', () => ({ + ipcMain: { + on: jest.fn(), + handle: jest.fn(), + }, +})); + +describe('main/developerMode', () => { + it('should toggle values correctly', () => { + const developerMode = new DeveloperMode('file.json'); + + // Should be false unless developer mode is enabled + developerMode.toggle('setting1'); + expect(developerMode.get('setting1')).toBe(false); + + developerMode.enabled = () => true; + + developerMode.toggle('setting1'); + expect(developerMode.get('setting1')).toBe(true); + + developerMode.toggle('setting1'); + expect(developerMode.get('setting1')).toBe(false); + }); +}); diff --git a/src/main/developerMode.ts b/src/main/developerMode.ts new file mode 100644 index 00000000..c8496ba7 --- /dev/null +++ b/src/main/developerMode.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ipcMain} from 'electron'; +import {EventEmitter} from 'events'; + +import {DEVELOPER_MODE_UPDATED, IS_DEVELOPER_MODE_ENABLED, UPDATE_PATHS, GET_DEVELOPER_MODE_SETTING} from 'common/communication'; +import JsonFileManager from 'common/JsonFileManager'; +import {developerModeJson} from 'main/constants'; + +import type {DeveloperSettings} from 'types/settings'; + +export class DeveloperMode extends EventEmitter { + private json: JsonFileManager; + + constructor(file: string) { + super(); + this.json = new JsonFileManager(file); + + ipcMain.handle(IS_DEVELOPER_MODE_ENABLED, this.enabled); + ipcMain.handle(GET_DEVELOPER_MODE_SETTING, (_, setting) => this.get(setting)); + } + + enabled = () => process.env.MM_DESKTOP_DEVELOPER_MODE === 'true'; + + toggle = (setting: keyof DeveloperSettings) => { + if (!this.enabled()) { + return; + } + + this.json.setValue(setting, !this.json.getValue(setting)); + this.emit(DEVELOPER_MODE_UPDATED, {[setting]: this.json.getValue(setting)}); + }; + + get = (setting: keyof DeveloperSettings) => { + if (!this.enabled()) { + return false; + } + + return this.json.getValue(setting); + }; + + switchOff = ( + setting: keyof DeveloperSettings, + onStart: () => void, + onStop: () => void, + ) => { + if (!this.get(setting)) { + onStart(); + } + + this.on(DEVELOPER_MODE_UPDATED, (settings: DeveloperSettings) => { + if (typeof settings[setting] !== 'undefined') { + if (settings[setting]) { + onStop(); + } else { + onStart(); + } + } + }); + }; +} + +let developerMode = new DeveloperMode(developerModeJson); +ipcMain.on(UPDATE_PATHS, () => { + developerMode = new DeveloperMode(developerModeJson); +}); +export default developerMode; diff --git a/src/main/menus/app.ts b/src/main/menus/app.ts index 818a0e70..68b718af 100644 --- a/src/main/menus/app.ts +++ b/src/main/menus/app.ts @@ -3,7 +3,7 @@ // See LICENSE.txt for license information. 'use strict'; -import type {MenuItemConstructorOptions, MenuItem, WebContents} from 'electron'; +import type {MenuItemConstructorOptions, MenuItem, BrowserWindow} from 'electron'; import {app, ipcMain, Menu, session, shell, clipboard} from 'electron'; import log from 'electron-log'; @@ -15,6 +15,7 @@ import {t} from 'common/utils/util'; import {getViewDisplayName} from 'common/views/View'; import type {ViewType} from 'common/views/View'; import type {UpdateManager} from 'main/autoUpdater'; +import DeveloperMode from 'main/developerMode'; import Diagnostics from 'main/diagnostics'; import downloadsManager from 'main/downloadsManager'; import {localizeMessage} from 'main/i18nManager'; @@ -139,7 +140,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { }], }); - const devToolsSubMenu = [ + const devToolsSubMenu: Electron.MenuItemConstructorOptions[] = [ { label: localizeMessage('main.menus.app.view.devToolsAppWrapper', 'Developer Tools for Application Wrapper'), accelerator: (() => { @@ -148,13 +149,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { } return 'Ctrl+Shift+I'; })(), - click(item: Electron.MenuItem, focusedWindow?: WebContents) { + click(item: Electron.MenuItem, focusedWindow?: BrowserWindow) { if (focusedWindow) { // toggledevtools opens it in the last known position, so sometimes it goes below the browserview - if (focusedWindow.isDevToolsOpened()) { - focusedWindow.closeDevTools(); + if (focusedWindow.webContents.isDevToolsOpened()) { + focusedWindow.webContents.closeDevTools(); } else { - focusedWindow.openDevTools({mode: 'detach'}); + focusedWindow.webContents.openDevTools({mode: 'detach'}); } } }, @@ -176,6 +177,60 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { }); } + if (DeveloperMode.enabled()) { + devToolsSubMenu.push(...[ + separatorItem, + { + label: localizeMessage('main.menus.app.view.developerModeBrowserOnly', 'Browser Only Mode'), + type: 'checkbox' as const, + checked: DeveloperMode.get('browserOnly'), + click() { + DeveloperMode.toggle('browserOnly'); + }, + }, + { + label: localizeMessage('main.menus.app.view.developerModeDisableNotificationStorage', 'Disable Notification Storage'), + type: 'checkbox' as const, + checked: DeveloperMode.get('disableNotificationStorage'), + click() { + DeveloperMode.toggle('disableNotificationStorage'); + }, + }, + { + label: localizeMessage('main.menus.app.view.developerModeDisableUserActivityMonitor', 'Disable User Activity Monitor'), + type: 'checkbox' as const, + checked: DeveloperMode.get('disableUserActivityMonitor'), + click() { + DeveloperMode.toggle('disableUserActivityMonitor'); + }, + }, + { + label: localizeMessage('main.menus.app.view.developerModeDisableContextMenu', 'Disable Context Menu'), + type: 'checkbox' as const, + checked: DeveloperMode.get('disableContextMenu'), + click() { + DeveloperMode.toggle('disableContextMenu'); + }, + }, + { + label: localizeMessage('main.menus.app.view.developerModeForceLegacyAPI', 'Force Legacy API'), + type: 'checkbox' as const, + checked: DeveloperMode.get('forceLegacyAPI'), + click() { + DeveloperMode.toggle('forceLegacyAPI'); + }, + }, + { + label: localizeMessage('main.menus.app.view.developerModeForceNewAPI', 'Force New API'), + type: 'checkbox' as const, + checked: DeveloperMode.get('forceNewAPI'), + click() { + DeveloperMode.toggle('forceNewAPI'); + }, + }, + ]); + } + const viewSubMenu = [{ label: localizeMessage('main.menus.app.view.find', 'Find..'), accelerator: 'CmdOrCtrl+F', diff --git a/src/main/notifications/index.test.ts b/src/main/notifications/index.test.ts index e18d231d..29d2e1d3 100644 --- a/src/main/notifications/index.test.ts +++ b/src/main/notifications/index.test.ts @@ -106,7 +106,9 @@ jest.mock('../windows/mainWindow', () => ({ show: jest.fn(), sendToRenderer: jest.fn(), })); - +jest.mock('main/developerMode', () => ({ + switchOff: (_: string, onStart: () => void) => onStart(), +})); jest.mock('main/i18nManager', () => ({ localizeMessage: jest.fn(), })); diff --git a/src/main/notifications/index.ts b/src/main/notifications/index.ts index 8f5ced38..47eb9dd6 100644 --- a/src/main/notifications/index.ts +++ b/src/main/notifications/index.ts @@ -8,6 +8,7 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state import {PLAY_SOUND, NOTIFICATION_CLICKED, BROWSER_HISTORY_PUSH, OPEN_NOTIFICATION_PREFERENCES} from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; +import DeveloperMode from 'main/developerMode'; import getLinuxDoNotDisturb from './dnd-linux'; import getWindowsDoNotDisturb from './dnd-windows'; @@ -22,13 +23,23 @@ import MainWindow from '../windows/mainWindow'; const log = new Logger('Notifications'); class NotificationManager { - private mentionsPerChannel: Map = new Map(); - private allActiveNotifications: Map = new Map(); + private mentionsPerChannel?: Map; + private allActiveNotifications?: Map; private upgradeNotification?: NewVersionNotification; private restartToUpgradeNotification?: UpgradeNotification; constructor() { ipcMain.on(OPEN_NOTIFICATION_PREFERENCES, this.openNotificationPreferences); + + DeveloperMode.switchOff('disableNotificationStorage', () => { + this.mentionsPerChannel = new Map(); + this.allActiveNotifications = new Map(); + }, () => { + this.mentionsPerChannel?.clear(); + delete this.mentionsPerChannel; + this.allActiveNotifications?.clear(); + delete this.allActiveNotifications; + }); } public async displayMention(title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, soundName: string) { @@ -68,12 +79,12 @@ class NotificationManager { } const mention = new Mention(options, channelId, teamId); - this.allActiveNotifications.set(mention.uId, mention); + this.allActiveNotifications?.set(mention.uId, mention); mention.on('click', () => { log.debug('notification click', serverName, mention.uId); - this.allActiveNotifications.delete(mention.uId); + this.allActiveNotifications?.delete(mention.uId); // Show the window after navigation has finished to avoid the focus handler // being called before the current channel has updated @@ -87,7 +98,7 @@ class NotificationManager { }); mention.on('close', () => { - this.allActiveNotifications.delete(mention.uId); + this.allActiveNotifications?.delete(mention.uId); }); return new Promise((resolve) => { @@ -107,12 +118,12 @@ class NotificationManager { // On Windows, manually dismiss notifications from the same channel and only show the latest one if (process.platform === 'win32') { const mentionKey = `${mention.teamId}:${mention.channelId}`; - if (this.mentionsPerChannel.has(mentionKey)) { + if (this.mentionsPerChannel?.has(mentionKey)) { log.debug(`close ${mentionKey}`); - this.mentionsPerChannel.get(mentionKey)?.close(); - this.mentionsPerChannel.delete(mentionKey); + this.mentionsPerChannel?.get(mentionKey)?.close(); + this.mentionsPerChannel?.delete(mentionKey); } - this.mentionsPerChannel.set(mentionKey, mention); + this.mentionsPerChannel?.set(mentionKey, mention); } const notificationSound = mention.getNotificationSound(); if (notificationSound) { @@ -127,7 +138,7 @@ class NotificationManager { mention.on('failed', (_, error) => { failed = true; - this.allActiveNotifications.delete(mention.uId); + this.allActiveNotifications?.delete(mention.uId); clearTimeout(timeout); // Special case for Windows - means that notifications are disabled at the OS level @@ -156,7 +167,7 @@ class NotificationManager { } const download = new DownloadNotification(fileName, serverName); - this.allActiveNotifications.set(download.uId, download); + this.allActiveNotifications?.set(download.uId, download); download.on('show', () => { flashFrame(true); @@ -164,15 +175,15 @@ class NotificationManager { download.on('click', () => { shell.showItemInFolder(path.normalize()); - this.allActiveNotifications.delete(download.uId); + this.allActiveNotifications?.delete(download.uId); }); download.on('close', () => { - this.allActiveNotifications.delete(download.uId); + this.allActiveNotifications?.delete(download.uId); }); download.on('failed', () => { - this.allActiveNotifications.delete(download.uId); + this.allActiveNotifications?.delete(download.uId); }); download.show(); } diff --git a/src/main/preload/externalAPI.ts b/src/main/preload/externalAPI.ts index 28a71aaf..3bfc76e0 100644 --- a/src/main/preload/externalAPI.ts +++ b/src/main/preload/externalAPI.ts @@ -43,89 +43,99 @@ import { UNREADS_AND_MENTIONS, LEGACY_OFF, TAB_LOGIN_CHANGED, + GET_DEVELOPER_MODE_SETTING, } from 'common/communication'; import type {ExternalAPI} from 'types/externalAPI'; -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); +let legacyEnabled = false; +let legacyOff: () => void; + +ipcRenderer.invoke(GET_DEVELOPER_MODE_SETTING, 'forceLegacyAPI').then((force) => { + if (force) { + return; + } + + 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 = { + 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(); - } + // 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), + 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), + // Session + setSessionExpired: (isExpired) => ipcRenderer.send(SESSION_EXPIRED, isExpired), + onUserActivityUpdate: (listener) => createListener(USER_ACTIVITY_UPDATE, listener), - onLogin: () => ipcRenderer.send(TAB_LOGIN_CHANGED, true), - onLogout: () => ipcRenderer.send(TAB_LOGIN_CHANGED, false), + onLogin: () => ipcRenderer.send(TAB_LOGIN_CHANGED, true), + onLogout: () => ipcRenderer.send(TAB_LOGIN_CHANGED, false), - // Unreads/mentions/notifications - sendNotification: (title, body, channelId, teamId, url, silent, soundName) => - ipcRenderer.invoke(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), + // Unreads/mentions/notifications + sendNotification: (title, body, channelId, teamId, url, silent, soundName) => + ipcRenderer.invoke(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), + // 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 - joinCall: (opts) => ipcRenderer.invoke(CALLS_JOIN_CALL, opts), - leaveCall: () => ipcRenderer.send(CALLS_LEAVE_CALL), + // Calls + joinCall: (opts) => ipcRenderer.invoke(CALLS_JOIN_CALL, opts), + leaveCall: () => ipcRenderer.send(CALLS_LEAVE_CALL), - callsWidgetConnected: (callID, sessionID) => ipcRenderer.send(CALLS_JOINED_CALL, callID, sessionID), - resizeCallsWidget: (width, height) => ipcRenderer.send(CALLS_WIDGET_RESIZE, width, height), + callsWidgetConnected: (callID, sessionID) => ipcRenderer.send(CALLS_JOINED_CALL, callID, sessionID), + resizeCallsWidget: (width, height) => ipcRenderer.send(CALLS_WIDGET_RESIZE, width, height), - sendCallsError: (err, callID, errMsg) => ipcRenderer.send(CALLS_ERROR, err, callID, errMsg), - onCallsError: (listener) => createListener(CALLS_ERROR, listener), + sendCallsError: (err, callID, errMsg) => ipcRenderer.send(CALLS_ERROR, err, callID, errMsg), + onCallsError: (listener) => createListener(CALLS_ERROR, listener), - getDesktopSources: (opts) => ipcRenderer.invoke(GET_DESKTOP_SOURCES, opts), - openScreenShareModal: () => ipcRenderer.send(DESKTOP_SOURCES_MODAL_REQUEST), - onOpenScreenShareModal: (listener) => createListener(DESKTOP_SOURCES_MODAL_REQUEST, listener), + getDesktopSources: (opts) => ipcRenderer.invoke(GET_DESKTOP_SOURCES, opts), + openScreenShareModal: () => ipcRenderer.send(DESKTOP_SOURCES_MODAL_REQUEST), + onOpenScreenShareModal: (listener) => createListener(DESKTOP_SOURCES_MODAL_REQUEST, listener), - shareScreen: (sourceID, withAudio) => ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio), - onScreenShared: (listener) => createListener(CALLS_WIDGET_SHARE_SCREEN, listener), + shareScreen: (sourceID, withAudio) => ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, sourceID, withAudio), + onScreenShared: (listener) => createListener(CALLS_WIDGET_SHARE_SCREEN, listener), - sendJoinCallRequest: (callId) => ipcRenderer.send(CALLS_JOIN_REQUEST, callId), - onJoinCallRequest: (listener) => createListener(CALLS_JOIN_REQUEST, listener), + sendJoinCallRequest: (callId) => ipcRenderer.send(CALLS_JOIN_REQUEST, callId), + onJoinCallRequest: (listener) => createListener(CALLS_JOIN_REQUEST, listener), - openLinkFromCalls: (url) => ipcRenderer.send(CALLS_LINK_CLICK, url), + openLinkFromCalls: (url) => ipcRenderer.send(CALLS_LINK_CLICK, url), - focusPopout: () => ipcRenderer.send(CALLS_POPOUT_FOCUS), + focusPopout: () => ipcRenderer.send(CALLS_POPOUT_FOCUS), - openThreadForCalls: (threadID) => ipcRenderer.send(CALLS_WIDGET_OPEN_THREAD, threadID), - onOpenThreadForCalls: (listener) => createListener(CALLS_WIDGET_OPEN_THREAD, listener), + openThreadForCalls: (threadID) => ipcRenderer.send(CALLS_WIDGET_OPEN_THREAD, threadID), + onOpenThreadForCalls: (listener) => createListener(CALLS_WIDGET_OPEN_THREAD, listener), - openStopRecordingModal: (channelID) => ipcRenderer.send(CALLS_WIDGET_OPEN_STOP_RECORDING_MODAL, channelID), - onOpenStopRecordingModal: (listener) => createListener(CALLS_WIDGET_OPEN_STOP_RECORDING_MODAL, listener), + openStopRecordingModal: (channelID) => ipcRenderer.send(CALLS_WIDGET_OPEN_STOP_RECORDING_MODAL, channelID), + onOpenStopRecordingModal: (listener) => createListener(CALLS_WIDGET_OPEN_STOP_RECORDING_MODAL, listener), - openCallsUserSettings: () => ipcRenderer.send(CALLS_WIDGET_OPEN_USER_SETTINGS), - onOpenCallsUserSettings: (listener) => createListener(CALLS_WIDGET_OPEN_USER_SETTINGS, listener), + openCallsUserSettings: () => ipcRenderer.send(CALLS_WIDGET_OPEN_USER_SETTINGS), + onOpenCallsUserSettings: (listener) => createListener(CALLS_WIDGET_OPEN_USER_SETTINGS, listener), - // Utility - unregister: (channel) => ipcRenderer.removeAllListeners(channel), -}; -contextBridge.exposeInMainWorld('desktopAPI', desktopAPI); + // Utility + unregister: (channel) => ipcRenderer.removeAllListeners(channel), + }; + contextBridge.exposeInMainWorld('desktopAPI', desktopAPI); +}); // Specific info for the testing environment if (process.env.NODE_ENV === 'test') { @@ -181,312 +191,318 @@ 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.warn('The guest is not assumed as mattermost-webapp'); +ipcRenderer.invoke(GET_DEVELOPER_MODE_SETTING, 'forceNewAPI').then((force) => { + if (force) { return; } - watchReactAppUntilInitialized(() => { - console.warn('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); - } -}; + /**************************************************************************** + * 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 + **************************************************************************** + */ -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; -}; + /** + * Legacy helper functions + */ -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(); + const onLoad = () => { + if (document.getElementById('root') === null) { + console.warn('The guest is not assumed as mattermost-webapp'); + return; } - }, interval); -}; - -const checkUnread = () => { - if (isReactAppInitialized()) { - findUnread(); - } else { watchReactAppUntilInitialized(() => { + console.warn('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; + 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.invoke(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); + } + } }); - 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, + // Legacy support to hold the full channel object so that it can be used for the click event + const channels: Map = new Map(); + ipcRenderer.on(NOTIFICATION_CLICKED, (event, channelId, teamId, url) => { + const channel = channels.get(channelId) ?? {id: channelId}; + channels.delete(channelId); + window.postMessage( + { + type: NOTIFICATION_CLICKED, + message: { + channel, + teamId, + url, }, - window.location.origin || '*', - ); - }); - break; - } - case 'dispatch-notification': { - const {title, body, channel, teamId, url, silent, data: messageData} = message; - channels.set(channel.id, channel); - ipcRenderer.invoke(NOTIFY_MENTION, title, body, channel.id, teamId, url, silent, messageData.soundName); - break; - } - case BROWSER_HISTORY_PUSH: { - const {path} = message as {path: string}; - ipcRenderer.send(BROWSER_HISTORY_PUSH, path); - break; - } - case 'history-button': { - ipcRenderer.invoke(REQUEST_BROWSER_HISTORY_STATUS).then(sendHistoryButtonReturn); - break; - } - case CALLS_LINK_CLICK: { - ipcRenderer.send(CALLS_LINK_CLICK, message.link); - break; - } - case GET_DESKTOP_SOURCES: { - ipcRenderer.invoke(GET_DESKTOP_SOURCES, message).then(sendDesktopSourcesResult); - break; - } - case CALLS_WIDGET_SHARE_SCREEN: { - ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, message.sourceID, message.withAudio); - break; - } - case CALLS_JOIN_CALL: { - ipcRenderer.invoke(CALLS_JOIN_CALL, message).then(sendCallsJoinedCall); - break; - } - case CALLS_JOINED_CALL: { - ipcRenderer.send(CALLS_JOINED_CALL, message.callID, message.sessionID); - break; - } - case CALLS_JOIN_REQUEST: { - ipcRenderer.send(CALLS_JOIN_REQUEST, message.callID); - break; - } - case CALLS_WIDGET_RESIZE: { - ipcRenderer.send(CALLS_WIDGET_RESIZE, message.width, message.height); - break; - } - case CALLS_ERROR: { - ipcRenderer.send(CALLS_ERROR, message.err, message.callID, message.errMsg); - break; - } - case CALLS_WIDGET_CHANNEL_LINK_CLICK: - case CALLS_LEAVE_CALL: - case DESKTOP_SOURCES_MODAL_REQUEST: - case CALLS_POPOUT_FOCUS: { - ipcRenderer.send(type); - } - } -}); - -// Legacy support to hold the full channel object so that it can be used for the click event -const channels: Map = new Map(); -ipcRenderer.on(NOTIFICATION_CLICKED, (event, channelId, teamId, url) => { - const channel = channels.get(channelId) ?? {id: channelId}; - channels.delete(channelId); - window.postMessage( - { - type: NOTIFICATION_CLICKED, - message: { - channel, - teamId, - url, }, - }, - window.location.origin, - ); -}); + window.location.origin, + ); + }); -ipcRenderer.on(BROWSER_HISTORY_PUSH, (event, pathName) => { - window.postMessage( - { - type: 'browser-history-push-return', - message: { - pathName, + ipcRenderer.on(BROWSER_HISTORY_PUSH, (event, pathName) => { + window.postMessage( + { + type: 'browser-history-push-return', + message: { + pathName, + }, }, - }, - window.location.origin, - ); -}); + window.location.origin, + ); + }); -const sendHistoryButtonReturn = (status: {canGoBack: boolean; canGoForward: boolean}) => { - window.postMessage( - { - type: 'history-button-return', - message: { - enableBack: status.canGoBack, - enableForward: status.canGoForward, + const sendHistoryButtonReturn = (status: {canGoBack: boolean; canGoForward: boolean}) => { + window.postMessage( + { + type: 'history-button-return', + message: { + enableBack: status.canGoBack, + enableForward: status.canGoForward, + }, }, - }, - window.location.origin, - ); -}; + window.location.origin, + ); + }; -ipcRenderer.on(BROWSER_HISTORY_STATUS_UPDATED, (event, canGoBack, canGoForward) => sendHistoryButtonReturn({canGoBack, canGoForward})); + 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 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, - ); -}; + 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(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 + */ + + legacyEnabled = true; + ipcRenderer.on(IS_UNREAD, checkUnread); + const unreadInterval = setInterval(getUnreadCount, 1000); + window.addEventListener('storage', onStorageChanged); + window.addEventListener('load', onLoad); + + 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'); + }; }); - -ipcRenderer.on(DESKTOP_SOURCES_MODAL_REQUEST, () => { - window.postMessage( - { - type: DESKTOP_SOURCES_MODAL_REQUEST, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(CALLS_WIDGET_SHARE_SCREEN, (_, sourceID, withAudio) => { - window.postMessage( - { - type: CALLS_WIDGET_SHARE_SCREEN, - message: {sourceID, withAudio}, - }, - window.location.origin, - ); -}); - -ipcRenderer.on(CALLS_ERROR, (_, err, callID, errMsg) => { - window.postMessage( - { - type: CALLS_ERROR, - message: {err, callID, errMsg}, - }, - window.location.origin, - ); -}); - -// push user activity updates to the webapp -ipcRenderer.on(USER_ACTIVITY_UPDATE, (event, userIsActive, isSystemEvent) => { - if (window.location.origin !== 'null') { - window.postMessage({type: USER_ACTIVITY_UPDATE, message: {userIsActive, manual: isSystemEvent}}, window.location.origin); - } -}); - -/** - * Legacy functionality that needs to be disabled with the new API - */ - -let legacyEnabled = true; -ipcRenderer.on(IS_UNREAD, checkUnread); -const unreadInterval = setInterval(getUnreadCount, 1000); -window.addEventListener('storage', onStorageChanged); -window.addEventListener('load', onLoad); - -function legacyOff() { - ipcRenderer.send(LEGACY_OFF); - ipcRenderer.off(IS_UNREAD, checkUnread); - clearInterval(unreadInterval); - window.removeEventListener('storage', onStorageChanged); - window.removeEventListener('load', onLoad); - - legacyEnabled = false; - console.log('New API preload initialized'); -} diff --git a/src/main/preload/internalAPI.js b/src/main/preload/internalAPI.js index 6762f8ee..d2d7cf40 100644 --- a/src/main/preload/internalAPI.js +++ b/src/main/preload/internalAPI.js @@ -92,6 +92,7 @@ import { GET_MEDIA_ACCESS_STATUS, VIEW_FINISHED_RESIZING, GET_NONCE, + IS_DEVELOPER_MODE_ENABLED, } from 'common/communication'; console.log('Preload initialized'); @@ -126,6 +127,7 @@ contextBridge.exposeInMainWorld('desktop', { checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES), updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems), getNonce: () => ipcRenderer.invoke(GET_NONCE), + isDeveloperModeEnabled: () => ipcRenderer.invoke(IS_DEVELOPER_MODE_ENABLED), updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder), updateTabOrder: (serverId, viewOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, viewOrder), diff --git a/src/main/utils.ts b/src/main/utils.ts index ea6b29dd..b98469cb 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -88,12 +88,16 @@ export function getLocalPreload(file: string) { return path.join(app.getAppPath(), file); } -export function composeUserAgent() { +export function composeUserAgent(browserMode?: boolean) { const baseUserAgent = app.userAgentFallback.split(' '); // filter out the Mattermost tag that gets added earlier on const filteredUserAgent = baseUserAgent.filter((ua) => !ua.startsWith('Mattermost')); + if (browserMode) { + return filteredUserAgent.join(' '); + } + return `${filteredUserAgent.join(' ')} Mattermost/${app.getVersion()}`; } diff --git a/src/main/views/MattermostBrowserView.test.js b/src/main/views/MattermostBrowserView.test.js index 2bb6b78d..bb300b8c 100644 --- a/src/main/views/MattermostBrowserView.test.js +++ b/src/main/views/MattermostBrowserView.test.js @@ -58,6 +58,9 @@ jest.mock('../utils', () => ({ composeUserAgent: () => 'Mattermost/5.0.0', shouldHaveBackBar: jest.fn(), })); +jest.mock('main/developerMode', () => ({ + get: jest.fn(), +})); const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'}); const view = new MessagingView(server, true); diff --git a/src/main/views/MattermostBrowserView.ts b/src/main/views/MattermostBrowserView.ts index 3b4bb641..a53da872 100644 --- a/src/main/views/MattermostBrowserView.ts +++ b/src/main/views/MattermostBrowserView.ts @@ -24,6 +24,7 @@ import ServerManager from 'common/servers/serverManager'; import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants'; import {isInternalURL, parseURL} from 'common/utils/url'; import type {MattermostView} from 'common/views/View'; +import DeveloperMode from 'main/developerMode'; import {getServerAPI} from 'main/server/serverAPI'; import MainWindow from 'main/windows/mainWindow'; @@ -52,7 +53,7 @@ export class MattermostBrowserView extends EventEmitter { private atRoot: boolean; private options: BrowserViewConstructorOptions; private removeLoading?: NodeJS.Timeout; - private contextMenu: ContextMenu; + private contextMenu?: ContextMenu; private status?: Status; private retryLoad?: NodeJS.Timeout; private maxRetries: number; @@ -65,7 +66,7 @@ export class MattermostBrowserView extends EventEmitter { const preload = getLocalPreload('externalAPI.js'); this.options = Object.assign({}, options); this.options.webPreferences = { - preload, + preload: DeveloperMode.get('browserOnly') ? undefined : preload, additionalArguments: [ `version=${app.getVersion()}`, `appName=${app.name}`, @@ -99,7 +100,10 @@ export class MattermostBrowserView extends EventEmitter { WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents); - this.contextMenu = new ContextMenu({}, this.browserView); + if (!DeveloperMode.get('disableContextMenu')) { + this.contextMenu = new ContextMenu({}, this.browserView); + } + this.maxRetries = MAX_SERVER_RETRIES; this.altPressStatus = false; @@ -192,7 +196,7 @@ export class MattermostBrowserView extends EventEmitter { loadURL = this.view.url.toString(); } this.log.verbose(`Loading ${loadURL}`); - const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()}); + const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))}); loading.then(this.loadSuccess(loadURL)).catch((err) => { if (err.code && err.code.startsWith('ERR_CERT')) { MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); @@ -427,7 +431,7 @@ export class MattermostBrowserView extends EventEmitter { if (!this.browserView || !this.browserView.webContents) { return; } - const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()}); + const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))}); loading.then(this.loadSuccess(loadURL)).catch((err) => { if (this.maxRetries-- > 0) { this.loadRetry(loadURL, err); diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index 65d2402c..0cb7abd9 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -34,6 +34,7 @@ import { LEGACY_OFF, UNREADS_AND_MENTIONS, TAB_LOGIN_CHANGED, + DEVELOPER_MODE_UPDATED, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -45,10 +46,13 @@ import Utils from 'common/utils/util'; import type {MattermostView} from 'common/views/View'; import {TAB_MESSAGING} from 'common/views/View'; import {flushCookiesStore} from 'main/app/utils'; +import DeveloperMode from 'main/developerMode'; import {localizeMessage} from 'main/i18nManager'; import PermissionsManager from 'main/permissionsManager'; import MainWindow from 'main/windows/mainWindow'; +import type {DeveloperSettings} from 'types/settings'; + import LoadingScreen from './loadingScreen'; import {MattermostBrowserView} from './MattermostBrowserView'; import modalManager from './modalManager'; @@ -91,6 +95,7 @@ export class ViewManager { ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId)); ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration); + DeveloperMode.on(DEVELOPER_MODE_UPDATED, this.handleDeveloperModeUpdated); } private init = () => { @@ -99,6 +104,17 @@ export class ViewManager { this.showInitial(); }; + private handleDeveloperModeUpdated = (json: DeveloperSettings) => { + log.debug('handleDeveloperModeUpdated', json); + + if (['browserOnly', 'disableContextMenu', 'forceLegacyAPI', 'forceNewAPI'].some((key) => Object.hasOwn(json, key))) { + this.views.forEach((view) => view.destroy()); + this.views = new Map(); + this.closedViews = new Map(); + this.init(); + } + }; + getView = (viewId: string) => { return this.views.get(viewId); }; diff --git a/src/renderer/components/DeveloperModeIndicator.tsx b/src/renderer/components/DeveloperModeIndicator.tsx new file mode 100644 index 00000000..68d1d0df --- /dev/null +++ b/src/renderer/components/DeveloperModeIndicator.tsx @@ -0,0 +1,36 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import classNames from 'classnames'; +import React from 'react'; +// eslint-disable-next-line no-restricted-imports +import {OverlayTrigger, Tooltip} from 'react-bootstrap'; +import {FormattedMessage} from 'react-intl'; + +import 'renderer/css/components/DeveloperModeIndicator.scss'; + +export default function DeveloperModeIndicator({developerMode, darkMode}: {developerMode: boolean; darkMode: boolean}) { + if (!developerMode) { + return null; + } + + return ( + + + + } + > +
+ + +
+
+ ); +} + diff --git a/src/renderer/components/MainPage.tsx b/src/renderer/components/MainPage.tsx index 536b9727..e35057b8 100644 --- a/src/renderer/components/MainPage.tsx +++ b/src/renderer/components/MainPage.tsx @@ -12,6 +12,7 @@ import {injectIntl} from 'react-intl'; import type {UniqueView, UniqueServer} from 'types/config'; import type {DownloadedItems} from 'types/downloads'; +import DeveloperModeIndicator from './DeveloperModeIndicator'; import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton'; import ErrorView from './ErrorView'; import ExtraBar from './ExtraBar'; @@ -55,6 +56,7 @@ type State = { showDownloadsBadge: boolean; hasDownloads: boolean; threeDotsIsFocused: boolean; + developerMode: boolean; }; type TabViewStatus = { @@ -88,6 +90,7 @@ class MainPage extends React.PureComponent { showDownloadsBadge: false, hasDownloads: false, threeDotsIsFocused: false, + developerMode: false, }; } @@ -263,6 +266,10 @@ class MainPage extends React.PureComponent { } window.addEventListener('click', this.handleCloseDropdowns); + + window.desktop.isDeveloperModeEnabled().then((developerMode) => { + this.setState({developerMode}); + }); } componentWillUnmount() { @@ -471,6 +478,10 @@ class MainPage extends React.PureComponent { /> )} {tabsRow} + {downloadsDropdownButton} {window.process.platform !== 'darwin' && this.state.fullScreen && diff --git a/src/renderer/css/components/DeveloperModeIndicator.scss b/src/renderer/css/components/DeveloperModeIndicator.scss new file mode 100644 index 00000000..50fd421a --- /dev/null +++ b/src/renderer/css/components/DeveloperModeIndicator.scss @@ -0,0 +1,54 @@ +.DeveloperModeIndicator { + align-items: center; + background: transparent; + border-radius: 4px; + border: none; + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + margin: 4px; + position: relative; + width: 32px; + + i { + color: rgba(63, 67, 80, 0.56); + cursor: pointer; + font-size: 21px; + line-height: 21px; + } + + &:hover, &:focus { + i { + color: rgba(63, 67, 80, 0.78); + } + } + + .DeveloperModeIndicator__badge { + background: rgba(210, 75, 78, 1); + border-radius: 10px; + width: 10px; + height: 10px; + position: absolute; + top: 5px; + right: 5px; + } + + &.darkMode { + i { + color: rgba(221, 223, 228, 0.56); + } + + &:hover, &:focus { + i { + color: rgba(221, 223, 228, 0.78); + } + } + } +} + +#DeveloperModeIndicator__tooltip { + > .tooltip-inner { + max-width: none; + } +} diff --git a/src/types/settings.ts b/src/types/settings.ts index 65c2074b..c780b608 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -8,3 +8,12 @@ export type SaveQueueItem = { key: keyof CombinedConfig; data: CombinedConfig[keyof CombinedConfig]; }; + +export type DeveloperSettings = { + browserOnly?: boolean; + disableNotificationStorage?: boolean; + disableUserActivityMonitor?: boolean; + disableContextMenu?: boolean; + forceLegacyAPI?: boolean; + forceNewAPI?: boolean; +}; diff --git a/src/types/window.ts b/src/types/window.ts index c5c39182..055b4308 100644 --- a/src/types/window.ts +++ b/src/types/window.ts @@ -45,6 +45,7 @@ declare global { checkForUpdates: () => void; updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void; getNonce: () => Promise; + isDeveloperModeEnabled: () => Promise; updateServerOrder: (serverOrder: string[]) => Promise; updateTabOrder: (serverId: string, viewOrder: string[]) => Promise;