From 051b4656cb91cd69ab5dc5ce44478529c6910b26 Mon Sep 17 00:00:00 2001 From: ruoxijiang Date: Thu, 30 Jul 2020 00:45:00 +0800 Subject: [PATCH 1/2] DC-1907 New Generic Dialog Window --- .../composer-templates/lib/template-store.es6 | 2 +- .../composer-translate/lib/main.jsx | 2 +- .../components/messages/MessagesTopBar.jsx | 2 +- .../utils/electron-utils.es6 | 2 +- .../jira-plugin/lib/jira-detail.jsx | 2 +- .../message-window/lib/main.es6 | 14 ++ .../lib/message-window-root.jsx | 137 +++++++++++ .../message-window/package.json | 15 ++ .../preferences-account-details.jsx | 35 +-- .../lib/components/preferences-keymaps.jsx | 13 +- app/src/app-env.es6 | 217 ++++++++++++------ app/src/browser/application.es6 | 100 ++++---- app/src/browser/mailspring-window.es6 | 6 +- app/src/browser/window-manager.es6 | 161 ++++++++++++- app/src/components/editable-list.jsx | 20 +- app/src/components/message-window.jsx | 134 +++++++++++ app/src/constant.es6 | 14 ++ app/src/flux/action-bridge.es6 | 16 +- app/src/flux/actions.es6 | 26 ++- app/src/flux/stores/app-message-store.es6 | 108 +++++++++ app/src/flux/stores/attachment-store.es6 | 22 +- app/src/flux/stores/draft-cache-store.es6 | 20 +- app/src/flux/stores/draft-store.es6 | 145 ++++++------ app/src/flux/stores/message-store.es6 | 5 +- app/src/global/mailspring-component-kit.es6 | 1 + app/src/global/mailspring-exports.es6 | 1 - app/src/sheet-container.jsx | 67 +++++- app/src/sheet-toolbar.jsx | 42 ++-- app/static/components/message-window.less | 78 +++++++ .../images/message-window/logo-edison@2x.png | Bin 0 -> 109105 bytes app/static/index.less | 1 + .../lib/my-composer-button.jsx | 9 +- 32 files changed, 1138 insertions(+), 279 deletions(-) create mode 100644 app/internal_packages/message-window/lib/main.es6 create mode 100644 app/internal_packages/message-window/lib/message-window-root.jsx create mode 100755 app/internal_packages/message-window/package.json create mode 100644 app/src/components/message-window.jsx create mode 100644 app/static/components/message-window.less create mode 100644 app/static/images/message-window/logo-edison@2x.png diff --git a/app/internal_packages/composer-templates/lib/template-store.es6 b/app/internal_packages/composer-templates/lib/template-store.es6 index f0df4b86af..dc09c7fc0c 100644 --- a/app/internal_packages/composer-templates/lib/template-store.es6 +++ b/app/internal_packages/composer-templates/lib/template-store.es6 @@ -432,7 +432,7 @@ class TemplateStore extends MailspringStore { AppEnv.reportError(new Error('Template Creation Error'), { errorData: message, }); - remote.dialog.showErrorBox('Template Creation Error', message); + AppEnv.showErrorBox('Template Creation Error', message); } _getPureBodyForDraft(body) { diff --git a/app/internal_packages/composer-translate/lib/main.jsx b/app/internal_packages/composer-translate/lib/main.jsx index 7e86c47040..2b58a19928 100644 --- a/app/internal_packages/composer-translate/lib/main.jsx +++ b/app/internal_packages/composer-translate/lib/main.jsx @@ -54,7 +54,7 @@ class TranslateButton extends React.Component { _onError(error) { Actions.closePopover(); const dialog = require('electron').remote.dialog; - dialog.showErrorBox('Language Conversion Failed', error.toString()); + AppEnv.showErrorDialog({ title: 'Language Conversion Failed', message: error.toString() }); } _onTranslate = async lang => { diff --git a/app/internal_packages/edison-beijing-chat/components/messages/MessagesTopBar.jsx b/app/internal_packages/edison-beijing-chat/components/messages/MessagesTopBar.jsx index 11df729365..1125e01037 100644 --- a/app/internal_packages/edison-beijing-chat/components/messages/MessagesTopBar.jsx +++ b/app/internal_packages/edison-beijing-chat/components/messages/MessagesTopBar.jsx @@ -72,7 +72,7 @@ export default class MessagesTopBar extends Component { return; } if (!conversationName.trim()) { - dialog.showMessageBox({ + AppEnv.showMessageBox({ type: 'warning', message: 'Group name should NOT be empty or blank.', buttons: ['OK'], diff --git a/app/internal_packages/edison-beijing-chat/utils/electron-utils.es6 b/app/internal_packages/edison-beijing-chat/utils/electron-utils.es6 index 5275eb9576..3eed8841b4 100644 --- a/app/internal_packages/edison-beijing-chat/utils/electron-utils.es6 +++ b/app/internal_packages/edison-beijing-chat/utils/electron-utils.es6 @@ -17,7 +17,7 @@ export const postNotification = (title, body, onActivate = () => {}) => { }; export const alert = message => { - dialog.showMessageBox({ + AppEnv.showMessageBox({ type: 'warning', message, buttons: ['OK'], diff --git a/app/internal_packages/jira-plugin/lib/jira-detail.jsx b/app/internal_packages/jira-plugin/lib/jira-detail.jsx index a645a6116f..e5303ec687 100644 --- a/app/internal_packages/jira-plugin/lib/jira-detail.jsx +++ b/app/internal_packages/jira-plugin/lib/jira-detail.jsx @@ -265,7 +265,7 @@ export default class JiraDetail extends Component { currentWin.previewFile(path); }; _showDialog(message, type = 'info') { - remote.dialog.showMessageBox({ + AppEnv.showMessageBox({ type, buttons: ['OK'], message, diff --git a/app/internal_packages/message-window/lib/main.es6 b/app/internal_packages/message-window/lib/main.es6 new file mode 100644 index 0000000000..7d4077cad3 --- /dev/null +++ b/app/internal_packages/message-window/lib/main.es6 @@ -0,0 +1,14 @@ +import { WorkspaceStore, ComponentRegistry } from 'mailspring-exports'; +import MessageWindowRoot from './message-window-root'; + +export function activate() { + WorkspaceStore.defineSheet('Main', { root: true }, { list: ['Center'] }); + + ComponentRegistry.register(MessageWindowRoot, { + location: WorkspaceStore.Location.Center, + }); +} + +export function deactivate() {} + +export function serialize() {} diff --git a/app/internal_packages/message-window/lib/message-window-root.jsx b/app/internal_packages/message-window/lib/message-window-root.jsx new file mode 100644 index 0000000000..dae36cb513 --- /dev/null +++ b/app/internal_packages/message-window/lib/message-window-root.jsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { MessageWindow } from 'mailspring-component-kit'; +import { Actions, Constant } from 'mailspring-exports'; +import { ipcRenderer } from 'electron'; +const minimumDetailHeight = 28; +export default class MessageWindowRoot extends React.PureComponent { + static displayName = 'MessageWindowRoot'; + static containerRequired = false; + + constructor(props) { + super(props); + this.state = this.defaultState(); + this._mounted = false; + this._details = null; + } + defaultState = () => { + return { + title: '', + detail: '', + buttons: [ + { label: 'Ok', order: 0, originalIndex: 0 }, + { label: 'Cancel', order: 1, originalIndex: 1 }, + ], + checkboxLabel: '', + checkboxChecked: false, + defaultId: 0, + cancelId: 1, + sourceWindowKey: '', + requestId: '', + }; + }; + componentDidMount() { + this._mounted = true; + ipcRenderer.on('reserveWindow-popout', this._onShowMessages); + } + componentDidUpdate(prevProps, prevState, snapshot) { + AppEnv.logDebug(`requestId ${this.state.requestId}`); + this._updateBrowserWindowHeight(); + if (this.state.requestId.length > 0) { + this.windowShow(); + } + } + + componentWillUnmount() { + this._mounted = false; + ipcRenderer.removeListener('reserveWindow-popout', this._onShowMessages); + } + _onShowMessages = ( + event, + { + title = '', + details = '', + checkLabel = '', + buttons, + defaultId, + cancelId, + targetWindowKey = '', + sourceWindowKey = '', + requestId = '', + } = {} + ) => { + // const currentKey = AppEnv.getCurrentWindowKey(); + // if (targetWindowKey !== currentKey) { + // AppEnv.logDebug( + // `not for current window ${currentKey}, targetWindowKey ${targetWindowKey}, ignoring` + // ); + // return; + // } + if (this._mounted) { + // this._updateBrowserWindowWidth(buttons.length, title.length); + this.setState({ + title, + details, + checkLabel, + defaultId, + cancelId, + buttons, + sourceWindowKey, + requestId, + }); + if (typeof title === 'string' && title.length > 0) { + AppEnv.setWindowTitle(title); + } + } + AppEnv.logDebug(`MessageWindow: on show Message ${requestId}`); + }; + _updateBrowserWindowHeight = () => { + const currentSize = AppEnv.getSize(); + if (this._details) { + const detailsHeight = this._details.getBoundingClientRect().height; + const extraHeight = detailsHeight - minimumDetailHeight; + // console.log( + // `update height detailHeight: ${detailsHeight}, current height: ${currentSize.height}, extraHeight: ${extraHeight}` + // ); + if ( + extraHeight > 0 && + currentSize.height - detailsHeight <= + Constant.MessageWindowSize.height - minimumDetailHeight + ) { + currentSize.height += extraHeight; + AppEnv.setSize(currentSize.width, currentSize.height); + } + } + }; + _updateBrowserWindowWidth = (buttonsSize, titleLength) => { + const currentSize = AppEnv.getSize(); + const extraTitleLength = Math.ceil((titleLength - 55) * 5.5); + const extraButtonLength = (buttonsSize - 2) * 112; + if (extraTitleLength > extraButtonLength && extraTitleLength > 0) { + currentSize.width += extraTitleLength; + } else if (extraButtonLength >= extraTitleLength && extraButtonLength > 0) { + currentSize.width += extraButtonLength; + } + AppEnv.setSize(currentSize.width, currentSize.height); + }; + windowShow = () => { + AppEnv.center(); + AppEnv.show(); + }; + windowHide = () => { + AppEnv.hide(); + if (this._mounted) { + this.setState(this.defaultState()); + AppEnv.setSize(Constant.MessageWindowSize.width, Constant.MessageWindowSize.height); + } + }; + _onCancel = () => { + this.windowHide(); + }; + _onClick = () => { + this.windowHide(); + }; + + render() { + return ; + } +} diff --git a/app/internal_packages/message-window/package.json b/app/internal_packages/message-window/package.json new file mode 100755 index 0000000000..4778df6aa0 --- /dev/null +++ b/app/internal_packages/message-window/package.json @@ -0,0 +1,15 @@ +{ + "name": "messageWindow", + "version": "0.1.0", + "main": "./lib/main", + "description": "Message Window", + "syncInit": false, + "license": "GPL-3.0", + "private": true, + "engines": { + "mailspring": "*" + }, + "windowTypes": { + "messageWindow": true + } +} diff --git a/app/internal_packages/preferences/lib/components/preferences-account-details.jsx b/app/internal_packages/preferences/lib/components/preferences-account-details.jsx index 5dfaa584dd..fdacaecc87 100644 --- a/app/internal_packages/preferences/lib/components/preferences-account-details.jsx +++ b/app/internal_packages/preferences/lib/components/preferences-account-details.jsx @@ -220,30 +220,31 @@ class PreferencesAccountDetails extends Component { if (drafts.length > 0) { details = `There are ${drafts.length} draft(s) for this account that are currently open.\n Do you want to proceed with deleting account ${account.emailAddress}?\n Deleting account will also close these drafts.`; } - const chosen = remote.dialog.showMessageBoxSync({ + AppEnv.showMessageBox({ type: 'info', message: 'Are you sure?', detail: details, buttons: ['Delete', 'Cancel'], defaultId: 0, cancelId: 1, - }); - if (chosen !== 0) { - return; - } - const openWindowsCount = AppEnv.getOpenWindowsCountByAccountId(account.id); - if (openWindowsCount > 0) { - AppEnv.closeWindowsByAccountId(account.id, 'account deleted'); - } - const index = this.props.accounts.indexOf(account); - if (account && typeof onRemoveAccount === 'function') { - // Move the selection 1 up or down after deleting - const newIndex = index === 0 ? index + 1 : index - 1; - onRemoveAccount(account); - if (this.props.accounts[newIndex] && typeof onSelectAccount === 'function') { - onSelectAccount(this.props.accounts[newIndex]); + }).then(({ response } = {}) => { + if (response !== 0) { + return; } - } + const openWindowsCount = AppEnv.getOpenWindowsCountByAccountId(account.id); + if (openWindowsCount > 0) { + AppEnv.closeWindowsByAccountId(account.id, 'account deleted'); + } + const index = this.props.accounts.indexOf(account); + if (account && typeof onRemoveAccount === 'function') { + // Move the selection 1 up or down after deleting + const newIndex = index === 0 ? index + 1 : index - 1; + onRemoveAccount(account); + if (this.props.accounts[newIndex] && typeof onSelectAccount === 'function') { + onSelectAccount(this.props.accounts[newIndex]); + } + } + }); }; // Renderers diff --git a/app/internal_packages/preferences/lib/components/preferences-keymaps.jsx b/app/internal_packages/preferences/lib/components/preferences-keymaps.jsx index 4742b79b17..f6a3c5e6da 100644 --- a/app/internal_packages/preferences/lib/components/preferences-keymaps.jsx +++ b/app/internal_packages/preferences/lib/components/preferences-keymaps.jsx @@ -38,19 +38,20 @@ export class PreferencesKeymapsHearder extends React.Component { } _onDeleteUserKeymap() { - const chosen = remote.dialog.showMessageBoxSync(AppEnv.getCurrentWindow(), { + AppEnv.showMessageBox({ type: 'info', message: 'Are you sure?', detail: 'Delete your custom key bindings and reset to the template defaults?', buttons: ['Cancel', 'Reset'], defaultId: 1, cancelId: 0, + blockWindowKey: AppEnv.getCurrentWindowKey(), + }).then(({ response }) => { + if (response === 1) { + const keymapsFile = AppEnv.keymaps.getUserKeymapPath(); + fs.writeFileSync(keymapsFile, '{}'); + } }); - - if (chosen === 1) { - const keymapsFile = AppEnv.keymaps.getUserKeymapPath(); - fs.writeFileSync(keymapsFile, '{}'); - } } render() { diff --git a/app/src/app-env.es6 b/app/src/app-env.es6 index 1619972ba9..e0a6baf27f 100644 --- a/app/src/app-env.es6 +++ b/app/src/app-env.es6 @@ -13,6 +13,8 @@ import WindowEventHandler from './window-event-handler'; import { createHash } from 'crypto'; import { dirExists, autoGenerateFileName, transfornImgToBase64 } from './fs-utils'; import RegExpUtils from './regexp-utils'; +import { WindowTypes } from './constant'; + import { WindowLevel } from './constant'; //Hinata gets special treatment for logging and other debugging purposes @@ -29,6 +31,11 @@ let getDeviceHash = null; const WebServerApiKey = 'bdH0VGExAEIhPq0z5vwdyVuHVzWx0hcR'; const WebServerRoot = 'https://web-marketing.edison.tech/'; const type = 'mac'; +let actions = null; +const Actions = () => { + actions = actions || require('mailspring-exports').Actions; + return actions; +}; function ensureInteger(f, fallback) { let int = f; @@ -623,17 +630,23 @@ export default class AppEnvConstructor { } isComposerWindow() { - return this.getWindowType() === 'composer'; + return this.getWindowType() === WindowTypes.COMPOSER_WINDOW; } isThreadWindow() { - return this.getWindowType() === 'thread-popout'; + return this.getWindowType() === WindowTypes.THREAD_WINDOW; } isOnboardingWindow() { - return this.getWindowType() === 'onboarding'; + return this.getWindowType() === WindowTypes.ONBOARDING_WINDOW; } isBugReportingWindow() { - return this.getWindowType() === 'bugreport'; + return this.getWindowType() === WindowTypes.BUG_REPORT_WINDOW; + } + isMessageWindow() { + return this.getWindowType() === WindowTypes.MESSAGE_WINDOW; + } + isDialogWindow() { + return this.getWindowType() === WindowTypes.DIALOG_WINDOW; } isDisableZoomWindow() { @@ -767,7 +780,16 @@ export default class AppEnvConstructor { } hide() { - return this.getCurrentWindow().hide(); + if (this.isMainWindow()) { + this.getCurrentWindow().hide(); + } else { + ipcRenderer.send( + 'call-window-manager-method', + 'hideReserveWindow', + this.getCurrentWindowKey(), + this.getWindowType() + ); + } } quit() { @@ -789,6 +811,9 @@ export default class AppEnvConstructor { setSize(width, height) { return this.getCurrentWindow().setSize(ensureInteger(width, 100), ensureInteger(height, 100)); } + setParentWindow(parent) { + this.getCurrentWindow().setParentWindow(parent); + } setMinimumWidth(minWidth) { const win = this.getCurrentWindow(); @@ -826,6 +851,14 @@ export default class AppEnvConstructor { getCurrentWindow() { return this.constructor.getCurrentWindow(); } + getCurrentWindowKey() { + const currentBrowserWindow = this.getCurrentWindow(); + const current = this.getOpenWindows().find(w => w.browserWindow === currentBrowserWindow); + if (current) { + return current.windowKey; + } + return ''; + } getOpenWindows(type = 'all') { try { return remote.getGlobal('application').windowManager.getOpenWindows(type); @@ -892,7 +925,20 @@ export default class AppEnvConstructor { // Extended: Show the current window. show() { - return ipcRenderer.send('call-window-method', 'show'); + const win = this.getCurrentWindow(); + if (win) { + if (this.isMainWindow()) { + win.show(); + } else { + this.logDebug(`showReserveWindow ${this.getCurrentWindowKey()}, ${this.getWindowType()}`); + ipcRenderer.send( + 'call-window-manager-method', + 'showReserveWindow', + this.getCurrentWindowKey(), + this.getWindowType() + ); + } + } } restore() { @@ -910,10 +956,10 @@ export default class AppEnvConstructor { return this.getCurrentWindow().isVisible(); } - // Extended: Hide the current window. - hide() { - return ipcRenderer.send('call-window-method', 'hide'); - } + // // Extended: Hide the current window. + // hide() { + // return ipcRenderer.send('call-window-method', 'hide'); + // } // Extended: Reload the current window. reload() { @@ -1075,7 +1121,7 @@ export default class AppEnvConstructor { } async startWindow() { - const { windowType } = this.getLoadSettings(); + const { windowType, windowKey } = this.getLoadSettings(); this.themes.loadStaticStylesheets(); this.initializeBasicSheet(); @@ -1090,6 +1136,7 @@ export default class AppEnvConstructor { this.menu.update(); ipcRenderer.send('window-command', 'window:loaded'); + this.logDebug(`This is window ${windowKey}`); }); }); }); @@ -1167,6 +1214,23 @@ export default class AppEnvConstructor { newWindow(options = {}) { return ipcRenderer.send('new-window', options); } + ensureModalMessageWindow = () => { + if (this.isMessageWindow()) { + this.logDebug(`This is Message window, ignoring`); + return; + } + const options = {}; + const currentKey = this.getCurrentWindowKey(); + if (currentKey) { + options.windowKey = `messageWindow-${currentKey}`; + options.parentWindowKey = currentKey; + ipcRenderer.send('ensure-modal-message-window', options); + } else { + this.logError( + new Error(`Unable to create modal message window, cannot get current window key`) + ); + } + }; updateWindowKey({ oldKey, newKey, newOptions = {} } = {}) { const opts = { oldKey, newKey, newOptions }; return ipcRenderer.send('update-window-key', opts); @@ -1290,8 +1354,12 @@ export default class AppEnvConstructor { } showOpenDialog(options, callback) { + let win = null; + if (options.modal) { + win = this.getCurrentWindow(); + } return remote.dialog - .showOpenDialog(this.getCurrentWindow(), { + .showOpenDialog(win, { ...options, securityScopedBookmarks: !!process.mas, }) @@ -1307,9 +1375,13 @@ export default class AppEnvConstructor { }); } - showImageSelectionDialog(cb) { + showImageSelectionDialog(cb, modal = false) { + let win = null; + if (modal) { + win = this.getCurrentWindow(); + } return remote.dialog - .showOpenDialog(this.getCurrentWindow(), { + .showOpenDialog(win, { properties: ['openFile', 'multiSelections'], filters: [ { @@ -1327,9 +1399,13 @@ export default class AppEnvConstructor { }); } - showBase64ImageTransformDialog(cb, maxSize = 0) { + showBase64ImageTransformDialog(cb, maxSize = 0, modal = false) { + let win = null; + if (modal) { + win = this.getCurrentWindow(); + } return remote.dialog - .showOpenDialog(this.getCurrentWindow(), { + .showOpenDialog(win, { properties: ['openFile'], filters: [ { @@ -1379,7 +1455,7 @@ export default class AppEnvConstructor { resolve(path.join(downloadPath, fileNewName)); } else { resolve(''); - remote.dialog.showErrorBox('File Save Error', errorMsg); + this.showErrorDialog({ title: 'File Save Error', message: errorMsg }); } return; } catch (e) { @@ -1397,18 +1473,20 @@ export default class AppEnvConstructor { title: options.title || 'Save File', securityScopedBookmarks: !!process.mas, }; - remote.dialog - .showSaveDialog(this.getCurrentWindow(), optionTmp) - .then(({ canceled, filePath, bookmark }) => { - if (canceled) { - resolve(''); - } else { - if (bookmark) { - this.setBookMarkForPath(filePath, bookmark); - } - resolve(filePath); + let win = null; + if (options.modal) { + win = this.getCurrentWindow(); + } + remote.dialog.showSaveDialog(win, optionTmp).then(({ canceled, filePath, bookmark }) => { + if (canceled) { + resolve(''); + } else { + if (bookmark) { + this.setBookMarkForPath(filePath, bookmark); } - }); + resolve(filePath); + } + }); }); } @@ -1427,7 +1505,7 @@ export default class AppEnvConstructor { resolve(downloadPath); } else { resolve(''); - remote.dialog.showErrorBox('File Save Error', errorMsg); + this.showErrorDialog({ title: 'File Save Error', message: errorMsg }); } return; } catch (e) { @@ -1446,9 +1524,12 @@ export default class AppEnvConstructor { properties: ['openDirectory', 'createDirectory'], securityScopedBookmarks: !!process.mas, }; - + let win = null; + if (options.modal) { + win = this.getCurrentWindow(); + } return remote.dialog - .showOpenDialog(this.getCurrentWindow(), optionTmp) + .showOpenDialog(win, optionTmp) .then(({ canceled, filePaths, bookmarks }) => { if (canceled) { resolve(''); @@ -1504,7 +1585,7 @@ export default class AppEnvConstructor { return remote.getGlobal('application').getMainWindow(); } - showErrorDialog(messageData, { showInMainWindow, detail } = {}) { + showErrorDialog(messageData, { showInMainWindow, detail, blockWindowKey = '' } = {}) { let message; let title; if (_.isString(messageData) || _.isNumber(messageData)) { @@ -1517,53 +1598,48 @@ export default class AppEnvConstructor { throw new Error('Must pass a valid message to show dialog', message); } - let winToShow = null; if (showInMainWindow) { - winToShow = remote.getGlobal('application').getMainWindow(); + blockWindowKey = 'default'; } if (!detail) { - return remote.dialog.showMessageBox(winToShow, { + return this.showMessageBox({ type: 'warning', buttons: ['Okay'], - message: title, + title, detail: message, + blockWindowKey, }); } - return remote.dialog - .showMessageBox(winToShow, { - type: 'warning', - buttons: ['Okay', 'Show Details'], - message: title, - detail: message, - }) - .then(({ response, ...rest }) => { - if (response === 1) { - const { Actions } = require('mailspring-exports'); - const { CodeSnippet } = require('mailspring-component-kit'); - Actions.openModal({ - component: CodeSnippet({ intro: message, code: detail, className: 'error-details' }), - width: 500, - height: 300, - }); - } - return Promise.resolve({ response, ...rest }); - }); + return this.showMessageBox({ + blockWindowKey, + type: 'warning', + buttons: ['Okay', 'Show Details'], + title, + detail: message, + }).then(({ response, ...rest }) => { + if (response === 1) { + const { Actions } = require('mailspring-exports'); + const { CodeSnippet } = require('mailspring-component-kit'); + Actions.openModal({ + component: CodeSnippet({ intro: message, code: detail, className: 'error-details' }), + width: 500, + height: 300, + }); + } + return Promise.resolve({ response, ...rest }); + }); } showMessageBox({ title = '', - showInMainWindow, + blockWindowKey, detail = '', type = 'question', buttons = ['Okay', 'Cancel'], defaultId = 0, cancelId = 1, } = {}) { - let winToShow = null; - if (showInMainWindow) { - winToShow = remote.getGlobal('application').getMainWindow(); - } if (!Array.isArray(buttons)) { buttons = ['Okay', 'Cancel']; } @@ -1576,13 +1652,20 @@ export default class AppEnvConstructor { if (defaultId < 0 || defaultId > buttons.length - 1) { defaultId = 0; } - return remote.dialog.showMessageBox(winToShow, { - type, - buttons, - message: title, - detail, - defaultId, - cancelId, + return new Promise((resolve, reject) => { + console.log('request Message window'); + Actions().requestMessageWindow( + { + type, + buttons, + title, + details: detail, + defaultId, + cancelId, + }, + blockWindowKey, + resolve + ); }); } diff --git a/app/src/browser/application.es6 b/app/src/browser/application.es6 index 05d14ca3b7..1dfbc07165 100644 --- a/app/src/browser/application.es6 +++ b/app/src/browser/application.es6 @@ -825,6 +825,12 @@ export default class Application extends EventEmitter { title: 'Welcome to EdisonMail', }); } + this.ensureReserveWindowAvailability(); + } + ensureReserveWindowAvailability() { + if (this.windowManager) { + this.windowManager.ensureMessageWindowExists(); + } } ensureMainWindowVisible() { @@ -1575,49 +1581,6 @@ export default class Application extends EventEmitter { } } }); - // ipcMain.on('draft-arp', (event, options) => { - // const mainWindow = this.windowManager.get(WindowManager.MAIN_WINDOW); - // if (mainWindow && mainWindow.browserWindow.webContents) { - // mainWindow.browserWindow.webContents.send('draft-arp', options); - // } - // if (options.threadId) { - // const threadWindow = this.windowManager.get(`thread-${options.threadId}`); - // if (threadWindow && threadWindow.browserWindow.webContents) { - // threadWindow.browserWindow.webContents.send('draft-arp', options); - // } - // } - // if (options.headerMessageId) { - // const composerWindow = this.windowManager.get(`composer-${options.headerMessageId}`); - // if (composerWindow && composerWindow.browserWindow.webContents) { - // composerWindow.browserWindow.webContents.send('draft-arp', options); - // } - // } - // }); - - // ipcMain.on('draft-arp-reply', (event, options) => { - // const mainWindow = this.windowManager.get(WindowManager.MAIN_WINDOW); - // if (mainWindow && mainWindow.browserWindow.webContents) { - // mainWindow.browserWindow.webContents.send('draft-arp-reply', options); - // } - // if (options.threadId) { - // const threadWindow = this.windowManager.get(`thread-${options.threadId}`); - // if (threadWindow && threadWindow.browserWindow.webContents) { - // threadWindow.browserWindow.webContents.send('draft-arp-reply', options); - // } - // } - // }); - // ipcMain.on('draft-delete', (event, options) => { - // const mainWindow = this.windowManager.get(WindowManager.MAIN_WINDOW); - // if (mainWindow && mainWindow.browserWindow.webContents) { - // mainWindow.browserWindow.webContents.send('draft-delete', options); - // } - // if (options.threadId) { - // const threadWindow = this.windowManager.get(`thread-${options.threadId}`); - // if (threadWindow && threadWindow.browserWindow.webContents) { - // threadWindow.browserWindow.webContents.send('draft-delete', options); - // } - // } - // }); ipcMain.on('update-window-key', (event, options) => { const win = options.oldKey ? this.windowManager.get(options.oldKey) : null; if (win) { @@ -1630,6 +1593,35 @@ export default class Application extends EventEmitter { } } }); + ipcMain.on('reserveWindow-popout', (event, options) => { + const win = this.windowManager.getAvailableReserveWindow(options.windowType); + + if (!win || !win.browserWindow.webContents) { + this.logDebug(`on rebroadcast to reserveWindow, no window found`); + return; + } + if (BrowserWindow.fromWebContents(event.sender) === win) { + this.logDebug(`on rebroadcast to reserveWindow, from same window, ignoring`); + return; + } + this.logDebug(`on rebroadcast to reserveWindow, ${win.windowKey}`); + win.browserWindow.webContents.send('reserveWindow-popout', options); + }); + + ipcMain.on('ensure-modal-message-window', (event, options) => { + const win = options.windowKey ? this.windowManager.get(options.windowKey) : null; + let parent = options.parentWindowKey ? this.windowManager.get(options.parentWindowKey) : null; + let parentWindow = parent ? parent.browserWindow : null; + if (!win && parentWindow) { + this.windowManager.newMessageWindow({ + modal: true, + parentWindow, + windowKey: options.windowKey, + }); + } else { + this.logWarning(`windowKey: ${options.windowKey}, parentKey ${options.parentWindowKey}`); + } + }); ipcMain.on('new-window', (event, options) => { const win = options.windowKey ? this.windowManager.get(options.windowKey) : null; @@ -1770,6 +1762,13 @@ export default class Application extends EventEmitter { } win[method](...args); }); + ipcMain.on('call-window-manager-method', (event, method, ...args) => { + const winManager = this.windowManager; + if (!winManager[method]) { + console.error(`Method ${method} does not exist on BrowserWindow!`); + } + winManager[method](...args); + }); ipcMain.on('call-devtools-webcontents-method', (event, method, ...args) => { // If devtools aren't open the `webContents::devToolsWebContents` will be null @@ -1805,6 +1804,21 @@ export default class Application extends EventEmitter { } mainWindow.browserWindow.webContents.send('action-bridge-message', ...args); }); + ipcMain.on('action-bridge-rebroadcast-to-messageWindow', (event, ...args) => { + const messageWindows = this.windowManager.getOpenWindows(WindowManager.MESSAGE_WINDOW); + messageWindows.forEach(messageWindow => { + if (!messageWindow || !messageWindow.browserWindow.webContents) { + this.logDebug(`on rebroadcast to messageWindow, no window found`); + return; + } + if (BrowserWindow.fromWebContents(event.sender) === messageWindow) { + this.logDebug(`on rebroadcast to messageWindow, from same window, ignoring`); + return; + } + this.logDebug(`on rebroadcast to messageWindow, ${messageWindow.windowKey}`); + messageWindow.browserWindow.webContents.send('action-bridge-message', ...args); + }); + }); ipcMain.on('write-text-to-selection-clipboard', (event, selectedText) => { clipboard = require('electron').clipboard; diff --git a/app/src/browser/mailspring-window.es6 b/app/src/browser/mailspring-window.es6 index 7aee052e7e..40d6645de5 100644 --- a/app/src/browser/mailspring-window.es6 +++ b/app/src/browser/mailspring-window.es6 @@ -13,7 +13,7 @@ module.exports = class MailspringWindow extends EventEmitter { constructor(settings = {}) { super(); - let frame, height, pathToOpen, resizable, title, width, autoHideMenuBar, titleBarStyle; + let frame, height, pathToOpen, resizable, title, width, autoHideMenuBar, titleBarStyle, modal, parentWindow; this.browserWindow = null; this.loaded = null; this.isSpec = null; @@ -39,6 +39,8 @@ module.exports = class MailspringWindow extends EventEmitter { configDirPath: this.configDirPath, autoHideMenuBar, titleBarStyle, + modal, + parentWindow, } = settings); if (!this.accountId) { this.accountId = 'all'; @@ -61,6 +63,8 @@ module.exports = class MailspringWindow extends EventEmitter { width, height, resizable, + modal, + parent: parentWindow, acceptFirstMouse: true, webPreferences: { directWrite: true, diff --git a/app/src/browser/window-manager.es6 b/app/src/browser/window-manager.es6 index 2fcbbbf315..76d8e739b7 100644 --- a/app/src/browser/window-manager.es6 +++ b/app/src/browser/window-manager.es6 @@ -1,11 +1,14 @@ import _ from 'underscore'; import { app } from 'electron'; import WindowLauncher from './window-launcher'; +import { MessageWindowSize, WindowTypes } from '../constant'; -const MAIN_WINDOW = 'default'; -const SPEC_WINDOW = 'spec'; -const ONBOARDING_WINDOW = 'onboarding'; -const BUG_REPORT_WINDOW = 'bugreport'; +const MAIN_WINDOW = WindowTypes.MAIN_WINDOW; +const SPEC_WINDOW = WindowTypes.SPEC_WINDOW; +const ONBOARDING_WINDOW = WindowTypes.ONBOARDING_WINDOW; +const BUG_REPORT_WINDOW = WindowTypes.BUG_REPORT_WINDOW; +const MESSAGE_WINDOW = WindowTypes.MESSAGE_WINDOW; +const DIALOG_WINDOW = WindowTypes.DIALOG_WINDOW; export default class WindowManager { constructor({ @@ -19,6 +22,8 @@ export default class WindowManager { }) { this.initializeInBackground = initializeInBackground; this._windows = {}; + this._reserveWindowTracking = {}; + this._freeReservedWindowsTimer = null; const onCreatedHotWindow = win => { this._registerWindow(win); @@ -34,6 +39,34 @@ export default class WindowManager { onCreatedHotWindow, }); } + _freeReservedWindows = () => { + const types = Object.keys(this._reserveWindowTracking); + types.forEach(type => { + console.log(`extra free reserve window found, ${type}`); + const keys = Object.keys(this._reserveWindowTracking[type]); + let count = 0; + keys.forEach(key => { + console.log( + `extra free reserve window found, ${type}:${key},count ${count}, ${this._reserveWindowTracking[type][key]} ` + ); + if (this._reserveWindowTracking[type][key]) { + count = count + 1; + } + if (count > 2) { + console.log(`extra free reserve window found, ${type}:${key}, destroying`); + this.destroyWindow(key); + delete this._reserveWindowTracking[type][key]; + } + }); + }); + console.log(`Cleaning up reserve window complete`); + this._freeReservedWindowsTimer = null; + }; + _initiateFreeReservedWindows = () => { + if (!this._freeReservedWindowsTimer) { + this._freeReservedWindowsTimer = setTimeout(this._freeReservedWindows, 3000); + } + }; get(windowKey) { return this._windows[windowKey]; @@ -168,6 +201,81 @@ export default class WindowManager { return win; } + newMessageWindow(options = {}) { + options.windowType = MESSAGE_WINDOW; + options.windowKey = MESSAGE_WINDOW; + options.height = MessageWindowSize.height; + options.width = MessageWindowSize.width; + this.newHiddenReserveWindow(options); + } + newDialogWindow(options = {}) { + options.windowType = DIALOG_WINDOW; + options.windowKey = DIALOG_WINDOW; + options.height = MessageWindowSize.height; + options.width = MessageWindowSize.width; + this.newHiddenReserveWindow(options); + } + + newHiddenReserveWindow(options = {}) { + options.coldStartOnly = true; + const numOfWindows = this.getOpenWindowCount(options.windowType); + options.windowKey = `${options.windowKey}-${numOfWindows}`; + options.resizable = false; + options.hidden = true; + const win = this.newWindow(options); + if (win && win.browserWindow) { + win.browserWindow.hide(); + } + if (!this._reserveWindowTracking[options.windowType]) { + this._reserveWindowTracking[options.windowType] = {}; + } + this._reserveWindowTracking[options.windowType][options.windowKey] = true; + } + getAvailableReserveWindow(type) { + if (!this._reserveWindowTracking[type]) { + return null; + } + const keys = Object.keys(this._reserveWindowTracking[type]); + for (let i = 0; i < keys.length; i++) { + if (keys[i] && this._reserveWindowTracking[type][keys[i]]) { + const win = this.get(keys[i]); + if (!win) { + delete this._reserveWindowTracking[type][keys[i]]; + } else { + return win; + } + } + } + return null; + } + showReserveWindow(windowKey, windowType) { + console.log(`windowKey ${windowKey}, ${windowType}`); + const win = this.get(windowKey); + if (win && win.browserWindow) { + win.browserWindow.show(); + this._reserveWindowTracking[windowType][windowKey] = false; + setTimeout(() => { + if (win && win.browserWindow) { + win.browserWindow.setOpacity(1); + } + if (windowType === MESSAGE_WINDOW) { + this.ensureMessageWindowExists(); + } else if (windowType === DIALOG_WINDOW) { + this.ensureDialogWindowExists(); + } + }, 200); + } + } + hideReserveWindow(key, type) { + const win = this.get(key); + if (win && win.browserWindow) { + console.log(`hide ${key}, ${type}`); + win.browserWindow.hide(); + win.browserWindow.setOpacity(0); + this._reserveWindowTracking[type][key] = true; + this._initiateFreeReservedWindows(); + } + } _registerWindow = win => { if (!win.windowKey) { @@ -186,6 +294,9 @@ export default class WindowManager { _didCreateNewWindow = win => { win.browserWindow.on('closed', () => { delete this._windows[win.windowKey]; + if (this._reserveWindowTracking[win.windowType]) { + delete this._reserveWindowTracking[win.windowType][win.windowKey]; + } this.quitWinLinuxIfNoWindows(); }); @@ -204,6 +315,27 @@ export default class WindowManager { } return null; }; + ensureMessageWindowExists() { + this.ensureReserveWindowAvailable(MESSAGE_WINDOW, { + windowKey: MESSAGE_WINDOW, + height: MessageWindowSize.height, + width: MessageWindowSize.width, + }); + } + ensureDialogWindowExists() { + this.ensureReserveWindowAvailable(DIALOG_WINDOW, { + windowKey: DIALOG_WINDOW, + height: MessageWindowSize.height, + width: MessageWindowSize.width, + }); + } + ensureReserveWindowAvailable(windowType, extraOpts = {}) { + const win = this.getAvailableReserveWindow(windowType); + if (!win) { + extraOpts.windowType = windowType; + this.newHiddenReserveWindow(extraOpts); + } + } ensureWindow(windowKey, extraOpts) { const win = this._windows[windowKey]; @@ -249,6 +381,13 @@ export default class WindowManager { win.browserWindow.webContents.send(msg, ...args); } } + destroyWindow(windowKey) { + const win = this.get(windowKey); + if (win && win.browserWindow) { + win.browserWindow.destroy(); + } + delete this._windows[windowKey]; + } destroyAllWindows() { this.windowLauncher.cleanupBeforeAppQuit(); @@ -256,6 +395,7 @@ export default class WindowManager { this._windows[windowKey].browserWindow.destroy(); } this._windows = {}; + this._reserveWindowTracking = {}; } cleanupBeforeAppQuit() { @@ -336,6 +476,17 @@ export default class WindowManager { width: 685, height: 700, }; + coreWinOpts[WindowManager.MESSAGE_WINDOW] = { + windowKey: WindowManager.MESSAGE_WINDOW, + windowType: WindowManager.MESSAGE_WINDOW, + title: 'Message Window', + // hidden: true, // Displayed by PageRouter::_initializeWindowSize + hidden: true, + frame: false, // Always false on Mac, explicitly set for Win & Linux + toolbar: false, + resizable: false, + disableZoom: true, + }; // The SPEC_WINDOW gets passed its own bootstrapScript coreWinOpts[WindowManager.SPEC_WINDOW] = { @@ -359,3 +510,5 @@ WindowManager.MAIN_WINDOW = MAIN_WINDOW; WindowManager.SPEC_WINDOW = SPEC_WINDOW; WindowManager.ONBOARDING_WINDOW = ONBOARDING_WINDOW; WindowManager.BUG_REPORT_WINDOW = BUG_REPORT_WINDOW; +WindowManager.MESSAGE_WINDOW = MESSAGE_WINDOW; +WindowManager.DIALOG_WINDOW = DIALOG_WINDOW; diff --git a/app/src/components/editable-list.jsx b/app/src/components/editable-list.jsx index 8b5c1ecace..073f3b3c5a 100644 --- a/app/src/components/editable-list.jsx +++ b/app/src/components/editable-list.jsx @@ -296,24 +296,28 @@ class EditableList extends Component { // need display confirm dialog if (this.props.getConfirmMessage) { const { message, detail } = this.props.getConfirmMessage(selectedItem); - const chosen = remote.dialog.showMessageBoxSync({ + AppEnv.showMessageBox({ type: 'info', message: message, detail: detail, buttons: ['Delete', 'Cancel'], defaultId: 0, cancelId: 1, + }).then(({ response }) => { + if (response === 0) { + this.props.onDeleteItem(selectedItem, index); + isDeleted = true; + } + if (isDeleted && this.props.items[newIndex]) { + this._selectItem(this.props.items[newIndex], newIndex); + } }); - if (chosen === 0) { - this.props.onDeleteItem(selectedItem, index); - isDeleted = true; - } } else { this.props.onDeleteItem(selectedItem, index); isDeleted = true; - } - if (isDeleted && this.props.items[newIndex]) { - this._selectItem(this.props.items[newIndex], newIndex); + if (isDeleted && this.props.items[newIndex]) { + this._selectItem(this.props.items[newIndex], newIndex); + } } } }; diff --git a/app/src/components/message-window.jsx b/app/src/components/message-window.jsx new file mode 100644 index 0000000000..1a93feb74a --- /dev/null +++ b/app/src/components/message-window.jsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { RetinaImg } from 'mailspring-component-kit'; +import { Actions } from 'mailspring-exports'; +const minimumDetailHeight = 28; +export default class MessageWindow extends React.PureComponent { + static displayName = 'MessageWindow'; + static defaultProps = { + onCanceled: () => {}, + onClicked: () => {}, + style: {}, + title: '', + detail: '', + buttons: [ + { label: 'Ok', order: 0, originalIndex: 0 }, + { label: 'Cancel', order: 1, originalIndex: 1 }, + ], + checkboxLabel: '', + checkboxChecked: false, + defaultId: 0, + cancelId: 1, + sourceWindowKey: '', + requestId: '', + }; + static containerRequired = false; + + constructor(props) { + super(props); + this._buttonClicked = false; + this._mounted = false; + this._details = null; + this._timer = null; + } + componentDidMount() { + this._mounted = true; + Actions.closeMessageWindow.listen(this._onCancel); + document.body.addEventListener('keydown', this._onKeyPress); + } + + componentWillUnmount() { + this._mounted = false; + document.body.removeListener('keydown', this._onKeyPress); + Actions.closeMessageWindow.unlisten(this._onCancel); + } + _allowButtonClick = () => { + if (!this._timer) { + this._timer = setTimeout(() => { + this._timer = null; + }, 300); + return true; + } + return false; + }; + _onCancel = () => { + Actions.messageWindowReply({ + sourceWindowKey: this.props.sourceWindowKey, + response: this.props.cancelId, + checkboxChecked: this.props.checkboxChecked, + requestId: this.props.requestId, + }); + this.props.onCanceled(); + }; + _onClick = originalIndex => { + if (this._allowButtonClick()) { + Actions.messageWindowReply({ + sourceWindowKey: this.props.sourceWindowKey, + response: originalIndex, + checkboxChecked: this.props.checkboxChecked, + requestId: this.props.requestId, + }); + this.props.onClicked(originalIndex); + } + }; + _onKeyPress = e => { + //Ignore key press that are part of composition of CJKT character + //https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onkeydown + if (e.isComposing || e.keyCode === 229) { + return; + } + if (e.key === 'Escape') { + this._onCancel(); + } else if (e.key === 'Enter') { + this._onClick(this.props.defaultId); + } + }; + + renderButtons() { + return this.props.buttons.map(button => { + let className = 'btn'; + if (button.order === 0) { + className += ' default'; + } + return ( + + ); + }); + } + + render() { + return ( +
+
+
+ +
+
+
{this.props.title}
+
(this._details = ref)} + style={{ minHeight: minimumDetailHeight }} + > + {this.props.details} +
+
+
+
{this.renderButtons()}
+
+ ); + } +} diff --git a/app/src/constant.es6 b/app/src/constant.es6 index 8edd6678a4..35fc3e95c5 100644 --- a/app/src/constant.es6 +++ b/app/src/constant.es6 @@ -1,3 +1,17 @@ +export const MessageWindowSize = { + width: 450, + height: 150, +}; +export const WindowTypes = { + MAIN_WINDOW: 'default', + SPEC_WINDOW: 'spec', + ONBOARDING_WINDOW: 'onboarding', + BUG_REPORT_WINDOW: 'bugreport', + MESSAGE_WINDOW: 'messageWindow', + DIALOG_WINDOW: 'dialogWindow', + COMPOSER_WINDOW: 'composer', + THREAD_WINDOW: 'thread-popout', +}; export const OAuthList = [ 'gmail', 'yahoo', diff --git a/app/src/flux/action-bridge.es6 b/app/src/flux/action-bridge.es6 index c784628cfc..380d808ac7 100644 --- a/app/src/flux/action-bridge.es6 +++ b/app/src/flux/action-bridge.es6 @@ -4,11 +4,13 @@ import Utils from './models/utils'; const Role = { MAIN: 'default', SECONDARY: 'secondary', + MESSAGE: 'message', }; const TargetWindows = { ALL: 'all', MAIN: 'default', + MESSAGE: 'messageWindow', }; const printToConsole = false; @@ -33,7 +35,13 @@ class ActionBridge { this.ipc = ipc; this.ipcLastSendTime = null; this.initiatorId = AppEnv.getWindowType(); - this.role = AppEnv.isMainWindow() ? Role.MAIN : Role.SECONDARY; + if (AppEnv.isMainWindow()) { + this.role = Role.MAIN; + } else if (AppEnv.isMessageWindow()) { + this.role = Role.MESSAGE; + } else { + this.role = Role.SECONDARY; + } AppEnv.onBeforeUnload(this.onBeforeUnload); @@ -54,6 +62,12 @@ class ActionBridge { return Actions[name].listen(callback, this); }); } + if (this.role !== Role.MESSAGE) { + Actions.messageWindowActions.forEach(name => { + const callback = (...args) => this.onRebroadcast(TargetWindows.MESSAGE, name, args); + return Actions[name].listen(callback, this); + }); + } } onIPCMessage(event, initiatorId, name, json) { diff --git a/app/src/flux/actions.es6 b/app/src/flux/actions.es6 index 3933f1ae33..6d9cdc4cbe 100644 --- a/app/src/flux/actions.es6 +++ b/app/src/flux/actions.es6 @@ -4,6 +4,7 @@ import { Action } from 'rxjs/internal/scheduler/Action'; const ActionScopeWindow = 'window'; const ActionScopeGlobal = 'global'; const ActionScopeMainWindow = 'main'; +const ActionScopeMessageWindow = 'messageWindow'; /* Public: In the Flux {Architecture.md}, almost every user action @@ -624,6 +625,13 @@ class Actions { // Mute static changeMuteSucceeded = ActionScopeMainWindow; + //App Message Popout Window + static requestMessageWindow = ActionScopeWindow; + static popoutMessageWindow = ActionScopeMessageWindow; + static showMessageWindow = ActionScopeGlobal; + static closeMessageWindow = ActionScopeWindow; + static messageWindowReply = ActionScopeGlobal; + // App Message actions static pushAppMessage = ActionScopeWindow; static pushAppMessages = ActionScopeWindow; @@ -660,11 +668,11 @@ const create = (obj, name, scope) => { obj[name].sync = true; }; -const scopes = { - window: [], - global: [], - main: [], -}; +const scopes = {}; +scopes[ActionScopeMainWindow] = []; +scopes[ActionScopeGlobal] = []; +scopes[ActionScopeWindow] = []; +scopes[ActionScopeMessageWindow] = []; for (const name of Object.getOwnPropertyNames(Actions)) { if ( @@ -676,7 +684,12 @@ for (const name of Object.getOwnPropertyNames(Actions)) { ) { continue; } - if (Actions[name] !== 'window' && Actions[name] !== 'global' && Actions[name] !== 'main') { + if ( + Actions[name] !== ActionScopeWindow && + Actions[name] !== ActionScopeGlobal && + Actions[name] !== ActionScopeMainWindow && + Actions[name] !== ActionScopeMessageWindow + ) { continue; } const scope = Actions[name]; @@ -687,5 +700,6 @@ for (const name of Object.getOwnPropertyNames(Actions)) { Actions.windowActions = scopes.window; Actions.mainWindowActions = scopes.main; Actions.globalActions = scopes.global; +Actions.messageWindowActions = scopes.messageWindow; export default Actions; diff --git a/app/src/flux/stores/app-message-store.es6 b/app/src/flux/stores/app-message-store.es6 index 5577ca86f5..3151d00d7e 100644 --- a/app/src/flux/stores/app-message-store.es6 +++ b/app/src/flux/stores/app-message-store.es6 @@ -1,6 +1,9 @@ import MailspringStore from 'mailspring-store'; import Actions from '../actions'; import uuid from 'uuid'; +import { ipcRenderer } from 'electron'; +import { WindowTypes } from '../../constant'; + const silentTTL = 30 * 60 * 1000; class AppMessageStore extends MailspringStore { static priority = { @@ -18,6 +21,7 @@ class AppMessageStore extends MailspringStore { medium: [], low: [], }; + this._messageWindowPopouts = {}; this._timeouts = {}; this._silentCache = {}; this._mostRecentBlock = null; @@ -30,7 +34,111 @@ class AppMessageStore extends MailspringStore { this.listenTo(Actions.removeAppMessages, this._onPopMessage); this.listenTo(Actions.removeAccount, this._onAccountRemoved); } + if (!AppEnv.isMessageWindow()) { + this.listenTo(Actions.requestMessageWindow, this._onRequestMessageWindow); + this.listenTo(Actions.messageWindowReply, this._onMessageWindowReply); + } } + _onMessageWindowReply = ({ sourceWindowKey, response, checkboxChecked, requestId } = {}) => { + const cb = this._messageWindowPopouts[requestId]; + delete this._messageWindowPopouts[requestId]; + if (cb) { + cb({ response, checkboxChecked }); + } + }; + + _onRequestMessageWindow = (messageData, blockWindowKey = '', cb = () => {}) => { + messageData.sourceWindowKey = AppEnv.getCurrentWindowKey(); + // messageData.targetWindowKey = 'messageWindow'; + if (blockWindowKey) { + messageData.targetWindowKey = blockWindowKey; + } + messageData.requestId = uuid(); + messageData.windowType = WindowTypes.MESSAGE_WINDOW; + messageData = this._validateMessageWindowRequestData(messageData); + this._messageWindowPopouts[messageData.requestId] = cb; + console.log('show message window'); + if (!blockWindowKey) { + ipcRenderer.send('reserveWindow-popout', messageData); + } else { + Actions.showMessageWindow(messageData); + } + }; + _validateMessageWindowRequestData = ({ + title = '', + details = '', + checkLabel = '', + buttons, + defaultId, + cancelId, + targetWindowKey = '', + sourceWindowKey = '', + requestId = '', + ...others + } = {}) => { + if (!sourceWindowKey) { + AppEnv.logError( + `Id is not available, we won't be able to return result to correct window, default results will be returned to main` + ); + sourceWindowKey = 'default'; + } + let buttons_state = []; + if (!Array.isArray(buttons)) { + buttons = ['Ok', 'Cancel']; + defaultId = 0; + cancelId = 1; + AppEnv.logWarning(`MessageWindow: buttons is not array from ${sourceWindowKey}`); + } + if ( + typeof defaultId !== 'number' || + !Number.isInteger(defaultId) || + defaultId < 0 || + defaultId >= buttons.length + ) { + defaultId = 0; + } + if ( + typeof cancelId !== 'number' || + !Number.isInteger(cancelId) || + cancelId < 0 || + cancelId >= buttons.length + ) { + cancelId = 0; + } + let defaultId_state; + let cancelId_state; + if (defaultId < buttons.length) { + buttons_state.push({ label: buttons[defaultId], order: 0, originalIndex: defaultId }); + defaultId_state = defaultId; + } + if (cancelId < buttons.length && cancelId !== defaultId) { + buttons_state.push({ + label: buttons[cancelId], + order: buttons_state.length, + originalIndex: cancelId, + }); + cancelId_state = cancelId; + } else { + cancelId_state = defaultId_state; + } + buttons.forEach((button, index) => { + if (button && ![defaultId, cancelId].includes(index)) { + buttons_state.push({ label: button, order: buttons_state.length, originalIndex: index }); + } + }); + return { + ...others, + title, + details, + checkLabel, + defaultId: defaultId_state, + cancelId: cancelId_state, + buttons: buttons_state, + sourceWindowKey, + targetWindowKey, + requestId, + }; + }; _onPopMessage = blockOrBlocks => { if (!Array.isArray(blockOrBlocks)) { diff --git a/app/src/flux/stores/attachment-store.es6 b/app/src/flux/stores/attachment-store.es6 index c0ea04e651..67b6535cd8 100644 --- a/app/src/flux/stores/attachment-store.es6 +++ b/app/src/flux/stores/attachment-store.es6 @@ -842,7 +842,7 @@ class AccountDrafts { class AttachmentStore extends MailspringStore { constructor() { super(); - + if (!AppEnv.isMessageWindow()) { // viewing messages this.listenTo(Actions.fetchFile, this._fetch); this.listenTo(Actions.fetchAndOpenFile, this._fetchAndOpen); @@ -850,8 +850,7 @@ class AttachmentStore extends MailspringStore { this.listenTo(Actions.fetchAndSaveAllFiles, this._saveAllFilesToUserDir); this.listenTo(Actions.abortFetchFile, this._abortFetchFile); this.listenTo(Actions.fetchAttachments, this._onFetchAttachments); - this.listenTo(Actions.extractTnefFile, this._extractTnefFile); - + this.listenTo(Actions.extractTnefFile, this._extractTnefFile); // sending this.listenTo(Actions.addAttachment, this._onAddAttachment); this.listenTo(Actions.addAttachments, this._onAddAttachments); @@ -878,12 +877,13 @@ class AttachmentStore extends MailspringStore { this._fileSaveSuccess = new Map(); mkdirp(this._filesDirectory); - DatabaseStore.listen(change => { - if (change.objectClass === AttachmentProgress.name) { - this._onPresentChange(change.objects); - } - }); - this._triggerDebounced = _.debounce(() => this.trigger(), 20); + DatabaseStore.listen(change => { + if (change.objectClass === AttachmentProgress.name) { + this._onPresentChange(change.objects); + } + }); + this._triggerDebounced = _.debounce(() => this.trigger(), 20); + } } _onDraftAttachmentStateChanged = data => { console.log(`draft attachment state changed `, data); @@ -1783,7 +1783,7 @@ class AttachmentStore extends MailspringStore { const name = file ? file.displayName() : 'one or more files'; const errorString = error ? error.toString() : ''; - return remote.dialog.showMessageBox({ + return AppEnv.showMessageBox({ type: 'warning', message: 'Download Failed', detail: `Unable to download ${name}. Check your network connection and try again. ${errorString}`, @@ -1802,7 +1802,7 @@ class AttachmentStore extends MailspringStore { } if (message) { - remote.dialog.showMessageBox({ + AppEnv.showMessageBox({ type: 'warning', message: 'Download Failed', detail: `${message}\n\n${error.message}`, diff --git a/app/src/flux/stores/draft-cache-store.es6 b/app/src/flux/stores/draft-cache-store.es6 index b4509db6c0..df364643d3 100644 --- a/app/src/flux/stores/draft-cache-store.es6 +++ b/app/src/flux/stores/draft-cache-store.es6 @@ -15,15 +15,17 @@ class DraftCacheStore extends MailspringStore { constructor() { super(); this.cache = {}; - this.listenTo(Actions.queueTasks, this._taskQueue); - this.listenTo(Actions.queueTask, task => this._taskQueue([task])); - this.listenTo(Actions.requestDraftCacheFromMain, this._onRequestForDraftCache); - this.listenTo(Actions.broadcastDraftCache, this._onBroadcastDraftCacheReceived); - this.listenTo(Actions.draftDeliverySucceeded, this._onSendDraftSuccess); - this.listenTo(Actions.draftDeliveryFailed, this._onSendDraftFailed); - this.listenTo(Actions.cancelOutboxDrafts, this._onRemoveDraft); - this.listenTo(Actions.destroyDraft, this._onRemoveDraft); - this.listenTo(DatabaseStore, this._onDBDataChange); + if (!AppEnv.isMessageWindow()) { + this.listenTo(Actions.queueTasks, this._taskQueue); + this.listenTo(Actions.queueTask, task => this._taskQueue([task])); + this.listenTo(Actions.requestDraftCacheFromMain, this._onRequestForDraftCache); + this.listenTo(Actions.broadcastDraftCache, this._onBroadcastDraftCacheReceived); + this.listenTo(Actions.draftDeliverySucceeded, this._onSendDraftSuccess); + this.listenTo(Actions.draftDeliveryFailed, this._onSendDraftFailed); + this.listenTo(Actions.cancelOutboxDrafts, this._onRemoveDraft); + this.listenTo(Actions.destroyDraft, this._onRemoveDraft); + this.listenTo(DatabaseStore, this._onDBDataChange); + } } _onRemoveDraft = ({ messages = [] } = {}) => { if (!Array.isArray(messages)) { diff --git a/app/src/flux/stores/draft-store.es6 b/app/src/flux/stores/draft-store.es6 index 602517a6e7..467185b107 100644 --- a/app/src/flux/stores/draft-store.es6 +++ b/app/src/flux/stores/draft-store.es6 @@ -47,21 +47,34 @@ Section: Drafts class DraftStore extends MailspringStore { constructor() { super(); - this.listenTo(DatabaseStore, this._onDataChanged); - this.listenTo(Actions.draftDeliveryFailed, this._onSendDraftFailed); - this.listenTo(Actions.draftDeliverySucceeded, this._onSendDraftSuccess); - this.listenTo(Actions.sendingDraft, this._onSendingDraft); - this.listenTo(Actions.destroyDraftFailed, this._onDestroyDraftFailed); - this.listenTo(Actions.destroyDraftSucceeded, this._onDestroyDraftSuccess); - this.listenTo(Actions.destroyDraft, this._onDestroyDrafts); - this.listenTo(Actions.sendDraft, this._onSendDraftAction); - this.listenTo(Actions.changeDraftAccount, this._onDraftAccountChangeAction); - this.listenTo(Actions.draftInlineAttachmentRemoved, this._onInlineItemRemoved); - this.listenTo(Actions.removeAllNoReferenceInLines, this._onRemoveAllNoReferenceInLines); - this.listenTo(Actions.broadcastChangeAccount, this._onBroadcastChangeAccount); - this.listenTo(Actions.broadcastServerDraftSession, this._onSessionForServerDraftReply); - this.listenTo(Actions.composeReply, this._onReply); - this.listenTo(Actions.composeForward, this._onForward); + if (!AppEnv.isMessageWindow()) { + this.listenTo(DatabaseStore, this._onDataChanged); + this.listenTo(Actions.draftDeliveryFailed, this._onSendDraftFailed); + this.listenTo(Actions.draftDeliverySucceeded, this._onSendDraftSuccess); + this.listenTo(Actions.sendingDraft, this._onSendingDraft); + this.listenTo(Actions.destroyDraftFailed, this._onDestroyDraftFailed); + this.listenTo(Actions.destroyDraftSucceeded, this._onDestroyDraftSuccess); + this.listenTo(Actions.destroyDraft, this._onDestroyDrafts); + this.listenTo(Actions.sendDraft, this._onSendDraftAction); + this.listenTo(Actions.changeDraftAccount, this._onDraftAccountChangeAction); + this.listenTo(Actions.draftInlineAttachmentRemoved, this._onInlineItemRemoved); + this.listenTo(Actions.removeAllNoReferenceInLines, this._onRemoveAllNoReferenceInLines); + this.listenTo(Actions.broadcastChangeAccount, this._onBroadcastChangeAccount); + this.listenTo(Actions.broadcastServerDraftSession, this._onSessionForServerDraftReply); + this.listenTo(Actions.composeReply, this._onReply); + this.listenTo(Actions.composeForward, this._onForward); + ipcRenderer.on('action-send-cancelled', (event, messageId, actionKey) => { + AppEnv.debugLog(`Undo Send received ${messageId}`); + if (AppEnv.isMainWindow()) { + AppEnv.debugLog( + `Undo Send received ${messageId} main window sending draftDeliveryCancelled` + ); + Actions.draftDeliveryCancelled({ messageId, actionKey }); + } + this._onSendDraftCancelled({ messageId }); + }); + AppEnv.onBeforeUnload(this._onBeforeUnload); + } if (AppEnv.isMainWindow()) { this.listenTo(Actions.requestSessionForServerDraft, this._onServerDraftSessionRequest); this.listenTo(Actions.toMainSendDraft, this._onSendDraft); @@ -112,23 +125,6 @@ class DraftStore extends MailspringStore { Actions.sendDraft(messageId, { actionKey, delay: 0, source: 'Undo timeout' }); }); } - ipcRenderer.on('action-send-cancelled', (event, messageId, actionKey) => { - AppEnv.debugLog(`Undo Send received ${messageId}`); - if (AppEnv.isMainWindow()) { - AppEnv.debugLog( - `Undo Send received ${messageId} main window sending draftDeliveryCancelled` - ); - Actions.draftDeliveryCancelled({ messageId, actionKey }); - } - this._onSendDraftCancelled({ messageId }); - }); - // popout closed - // ipcRenderer.on('draft-close-window', this._onPopoutClosed); - // ipcRenderer.on('draft-got-new-id', this._onDraftGotNewId); - // ipcRenderer.on('draft-arp', this._onDraftArp); - // ipcRenderer.on('draft-delete', this._onDraftDeleting); - AppEnv.onBeforeUnload(this._onBeforeUnload); - this._draftSessions = {}; this._draftsSending = {}; this._draftSendingTimeouts = {}; @@ -552,24 +548,24 @@ class DraftStore extends MailspringStore { } session.validateDraftForChangeAccount().then(ret => { const { errors, warnings } = ret; - const dialog = remote.dialog; if (warnings.length > 0) { - dialog - .showMessageBox(remote.getCurrentWindow(), { - type: 'warning', - buttons: ['Yes', 'Cancel'], - message: 'Draft not ready, change anyways? This will remove all draft attachments.', - detail: `${warnings.join(' and ')}?`, - }) - .then(({ response } = {}) => { - if (response === 0) { - session.changes.add({ files: [] }); - session.updateAttachments([]); - session.changingAccount(); - Actions.broadcastChangeAccount(data, AppEnv.getWindowLevel()); - return true; - } - }); + AppEnv.showMessageBox({ + blockWindowKey: AppEnv.getCurrentWindowKey(), + type: 'warning', + buttons: ['Yes', 'Cancel'], + title: 'Draft not ready', + detail: `Draft not ready, change anyways? This will remove all draft attachments.\n${warnings.join( + ' and ' + )}?`, + }).then(({ response } = {}) => { + if (response === 0) { + session.changes.add({ files: [] }); + session.updateAttachments([]); + session.changingAccount(); + Actions.broadcastChangeAccount(data, AppEnv.getWindowLevel()); + return true; + } + }); return false; } session.changingAccount(); @@ -948,7 +944,12 @@ class DraftStore extends MailspringStore { }; _onBeforeUnload = readyToUnload => { - if (AppEnv.isOnboardingWindow() || AppEnv.isEmptyWindow() || AppEnv.isBugReportingWindow()) { + if ( + AppEnv.isOnboardingWindow() || + AppEnv.isEmptyWindow() || + AppEnv.isBugReportingWindow() || + AppEnv.isMessageWindow() + ) { console.log(`Is not proper window or is empty window ${AppEnv.isEmptyWindow()}`); return true; } @@ -1757,7 +1758,6 @@ class DraftStore extends MailspringStore { } }; _onDestroyDraftSuccess = ({ messageIds, accountId }) => { - AppEnv.logDebug('destroy draft succeeded'); if (Array.isArray(messageIds)) { const triggerMessageIds = []; messageIds.forEach(id => { @@ -1963,14 +1963,14 @@ class DraftStore extends MailspringStore { const session = this._draftSessions[messageId]; if (session) { session.validateDraftForSending().then(({ errors, warnings }) => { - const dialog = remote.dialog; if (errors.length > 0) { - dialog.showMessageBox(remote.getCurrentWindow(), { + AppEnv.showMessageBox({ + blockWindowKey: AppEnv.getCurrentWindowKey(), type: 'warning', buttons: ['Edit Message', 'Cancel'], defaultId: 0, cancelId: 1, - message: 'Cannot Send', + title: 'Cannot Send', detail: errors[0], }); Actions.draftDeliveryCancelled({ messageId }); @@ -1978,25 +1978,24 @@ class DraftStore extends MailspringStore { } if (warnings.length > 0 && !options.force) { - dialog - .showMessageBox(remote.getCurrentWindow(), { - type: 'warning', - buttons: ['Send Anyway', 'Cancel'], - defaultId: 0, - cancelId: 1, - message: 'Are you sure?', - detail: `Send ${warnings.join(' and ')}?`, - }) - .then(({ response } = {}) => { - if (response === 0) { - options.disableDraftCheck = true; - session.removeMissingAttachments().then(() => { - this._onSendDraftAction(messageId, options); - }); - } else { - Actions.draftDeliveryCancelled({ messageId }); - } - }); + AppEnv.showMessageBox({ + type: 'warning', + buttons: ['Send Anyway', 'Cancel'], + defaultId: 0, + cancelId: 1, + title: 'Are you sure?', + detail: `Send ${warnings.join(' and ')}?`, + blockWindowKey: AppEnv.getCurrentWindowKey(), + }).then(({ response } = {}) => { + if (response === 0) { + options.disableDraftCheck = true; + session.removeMissingAttachments().then(() => { + this._onSendDraftAction(messageId, options); + }); + } else { + Actions.draftDeliveryCancelled({ messageId }); + } + }); return false; } this._reRouteSendDraftAction(messageId, options); diff --git a/app/src/flux/stores/message-store.es6 b/app/src/flux/stores/message-store.es6 index b2ef0b1350..88696c6546 100644 --- a/app/src/flux/stores/message-store.es6 +++ b/app/src/flux/stores/message-store.es6 @@ -21,7 +21,9 @@ class MessageStore extends MailspringStore { constructor() { super(); this._setStoreDefaults(); - this._registerListeners(); + if (!AppEnv.isMessageWindow()) { + this._registerListeners(); + } } findAll() { @@ -211,7 +213,6 @@ class MessageStore extends MailspringStore { } _registerListeners() { - // console.log('register listerners'); AppEnv.onBeforeUnload(this._onWindowClose); if (AppEnv.isMainWindow()) { this._currentWindowLevel = 1; diff --git a/app/src/global/mailspring-component-kit.es6 b/app/src/global/mailspring-component-kit.es6 index f89cb3dc05..57db6a91d6 100644 --- a/app/src/global/mailspring-component-kit.es6 +++ b/app/src/global/mailspring-component-kit.es6 @@ -132,6 +132,7 @@ lazyLoad('MailLabelSet', 'mail-label-set'); lazyLoad('MailImportantIcon', 'mail-important-icon'); lazyLoad('ScenarioEditor', 'scenario-editor'); +lazyLoad('MessageWindow', 'message-window'); // Higher order components lazyLoad('ListensToObservable', 'decorators/listens-to-observable'); diff --git a/app/src/global/mailspring-exports.es6 b/app/src/global/mailspring-exports.es6 index db2456d3bc..16b7ca5520 100644 --- a/app/src/global/mailspring-exports.es6 +++ b/app/src/global/mailspring-exports.es6 @@ -203,7 +203,6 @@ lazyLoad(`InflatesDraftClientId`, 'decorators/inflates-draft-client-id'); lazyLoad(`ExtensionRegistry`, 'registries/extension-registry'); lazyLoad(`MessageViewExtension`, 'extensions/message-view-extension'); lazyLoad(`ComposerExtension`, 'extensions/composer-extension'); - // 3rd party libraries lazyLoadWithGetter(`Rx`, () => require('rx-lite')); lazyLoadWithGetter(`React`, () => require('react')); diff --git a/app/src/sheet-container.jsx b/app/src/sheet-container.jsx index fc41616476..1c34837063 100644 --- a/app/src/sheet-container.jsx +++ b/app/src/sheet-container.jsx @@ -9,7 +9,7 @@ import { DatabaseStore, Actions, } from 'mailspring-exports'; -import { RetinaImg } from 'mailspring-component-kit'; +import { RetinaImg, MessageWindow, FullScreenModal } from 'mailspring-component-kit'; import Sheet from './sheet'; import Toolbar from './sheet-toolbar'; import Flexbox from './components/flexbox'; @@ -23,9 +23,12 @@ export default class SheetContainer extends React.Component { super(props); this._toolbarComponents = null; this.state = this._getStateFromStores(); + this.state.messageWindowData = {}; + this.state.showMessageWindow = false; } componentDidMount() { + Actions.showMessageWindow.listen(this._onShowMessageWindow); ipcRenderer.on('application-activate', this._onAppActive); this.unsubscribe = WorkspaceStore.listen(this._onStoreChange); if (AppEnv.isMainWindow()) { @@ -76,6 +79,7 @@ export default class SheetContainer extends React.Component { if (this.unsubscribe) { this.unsubscribe(); } + Actions.showMessageWindow.unlisten(this._onShowMessageWindow); } _getStateFromStores() { @@ -153,6 +157,66 @@ export default class SheetContainer extends React.Component { ); } + _onShowMessageWindow = ({ + title, + details, + checkLabel, + defaultId, + cancelId, + buttons, + sourceWindowKey, + targetWindowKey, + requestId, + } = {}) => { + const currentKey = AppEnv.getCurrentWindowKey(); + if (targetWindowKey !== currentKey && targetWindowKey !== 'all') { + AppEnv.logDebug( + `not for current window ${currentKey}, targetWindowKey ${targetWindowKey}, ignoring` + ); + return; + } + this.setState({ + messageWindowData: { + title, + details, + checkLabel, + defaultId, + cancelId, + buttons, + sourceWindowKey, + requestId, + }, + showMessageWindow: true, + }); + }; + _onMessageWindowCanceled = () => { + this.setState({ showMessageWindow: false }); + }; + _onMessageWindowClicked = () => { + this.setState({ showMessageWindow: false }); + }; + + _renderBlockingMessage = () => { + if (!this.state.showMessageWindow) { + return null; + } + console.log('block window'); + return ( + + + + ); + }; isValidUser = () => { // beta invite flow @@ -227,6 +291,7 @@ export default class SheetContainer extends React.Component { name="Center" style={{ height: '100%', order: 2, flex: 1, position: 'relative', zIndex: 1 }} > + {this._renderBlockingMessage()} {rootSheet} {popSheet} diff --git a/app/src/sheet-toolbar.jsx b/app/src/sheet-toolbar.jsx index 2c001d8db9..bf1eefc51c 100644 --- a/app/src/sheet-toolbar.jsx +++ b/app/src/sheet-toolbar.jsx @@ -2,18 +2,13 @@ /* eslint global-require: 0 */ import React from 'react'; import PropTypes from 'prop-types'; -import ReactDOM from 'react-dom'; import { remote } from 'electron'; -import { Actions, ComponentRegistry, WorkspaceStore } from 'mailspring-exports'; - +import { ComponentRegistry, WorkspaceStore, Actions } from 'mailspring-exports'; import Flexbox from './components/flexbox'; import RetinaImg from './components/retina-img'; import Utils from './flux/models/utils'; import _ from 'underscore'; -let Category = null; -let FocusedPerspectiveStore = null; - class ToolbarSpacer extends React.Component { static displayName = 'ToolbarSpacer'; static propTypes = { @@ -93,6 +88,13 @@ class ToolbarWindowControls extends React.Component { this.setState({ alt: false }); } }; + _onClose = () => { + if (AppEnv.isMessageWindow()) { + Actions.closeMessageWindow(); + } else { + AppEnv.close(); + } + }; render() { const enabled = @@ -107,17 +109,19 @@ class ToolbarWindowControls extends React.Component { if (this.state.alt) { maxButton = ; + maxButton =