diff --git a/app/browser/reducers/passwordManagerReducer.js b/app/browser/reducers/passwordManagerReducer.js index 43ed331805a..c1e98daeee0 100644 --- a/app/browser/reducers/passwordManagerReducer.js +++ b/app/browser/reducers/passwordManagerReducer.js @@ -211,7 +211,7 @@ const init = () => { if (!(message in passwordCallbacks)) { // Notification not shown already - appActions.showMessageBox({ + appActions.showNotification({ buttons: [ {text: locale.translation('yes')}, {text: locale.translation('no')}, @@ -228,7 +228,7 @@ const init = () => { passwordCallbacks[message] = (buttonIndex) => { delete passwordCallbacks[message] - appActions.hideMessageBox(message) + appActions.hideNotification(message) if (buttonIndex === 1) { return diff --git a/app/browser/reducers/tabMessageBoxReducer.js b/app/browser/reducers/tabMessageBoxReducer.js new file mode 100644 index 00000000000..c50ee8ee403 --- /dev/null +++ b/app/browser/reducers/tabMessageBoxReducer.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict' + +const appConstants = require('../../../js/constants/appConstants') +const tabMessageBox = require('../tabMessageBox') +const tabMessageBoxState = require('../../common/state/tabMessageBoxState') + +const tabMessageBoxReducer = (state, action) => { + switch (action.actionType) { + case appConstants.APP_SET_STATE: + state = tabMessageBox.init(state, action) + break + case appConstants.APP_TAB_UPDATED: + state = tabMessageBox.onTabUpdated(state, action) + break + case appConstants.APP_TAB_CLOSED: + state = tabMessageBox.onTabClosed(state, action) + break + case appConstants.APP_TAB_MESSAGE_BOX_SHOWN: + state = tabMessageBoxState.show(state, action) + break + case appConstants.APP_TAB_MESSAGE_BOX_DISMISSED: + state = tabMessageBox.close(state, action) + break + case appConstants.APP_TAB_MESSAGE_BOX_UPDATED: + state = tabMessageBoxState.update(state, action) + break + } + return state +} + +module.exports = tabMessageBoxReducer diff --git a/app/browser/tabMessageBox.js b/app/browser/tabMessageBox.js new file mode 100644 index 00000000000..1b0d78323ba --- /dev/null +++ b/app/browser/tabMessageBox.js @@ -0,0 +1,132 @@ +const appActions = require('../../js/actions/appActions') +const tabMessageBoxState = require('../common/state/tabMessageBoxState') +const {makeImmutable} = require('../common/state/immutableUtil') + +// callbacks for alert, confirm, etc. +let messageBoxCallbacks = {} + +const cleanupCallback = (tabId) => { + if (messageBoxCallbacks[tabId]) { + delete messageBoxCallbacks[tabId] + return true + } + return false +} + +const tabMessageBox = { + init: (state, action) => { + process.on('window-alert', (webContents, extraData, title, message, defaultPromptText, + shouldDisplaySuppressCheckbox, isBeforeUnloadDialog, isReload, cb) => { + const tabId = webContents.getId() + const detail = { + message, + title, + buttons: ['ok'], + suppress: false, + showSuppress: shouldDisplaySuppressCheckbox + } + + tabMessageBox.show(tabId, detail, cb) + }) + + process.on('window-confirm', (webContents, extraData, title, message, defaultPromptText, + shouldDisplaySuppressCheckbox, isBeforeUnloadDialog, isReload, cb) => { + const tabId = webContents.getId() + const detail = { + message, + title, + buttons: ['ok', 'cancel'], + cancelId: 1, + suppress: false, + showSuppress: shouldDisplaySuppressCheckbox + } + + tabMessageBox.show(tabId, detail, cb) + }) + + process.on('window-prompt', (webContents, extraData, title, message, defaultPromptText, + shouldDisplaySuppressCheckbox, isBeforeUnloadDialog, isReload, cb) => { + console.warn('window.prompt is not supported yet') + let suppress = false + cb(false, '', suppress) + }) + + return state + }, + + show: (tabId, detail, cb) => { + if (cb) { + messageBoxCallbacks[tabId] = cb + } + setImmediate(() => { + appActions.tabMessageBoxShown(tabId, detail) + }) + }, + + close: (state, action) => { + action = makeImmutable(action) + const tabId = action.get('tabId') + const detail = action.get('detail') + const cb = messageBoxCallbacks[tabId] + let suppress = false + let result = true + state = tabMessageBoxState.removeDetail(state, action) + if (cb) { + cleanupCallback(tabId) + if (detail) { + if (detail.has('suppress')) { + suppress = detail.get('suppress') + } + if (detail.has('result')) { + result = detail.get('result') + } + cb(result, '', suppress) + } else { + cb(false, '', false) + } + } + return state + }, + + onTabClosed: (state, action) => { + action = makeImmutable(action) + const tabId = action.getIn(['tabValue', 'tabId']) + if (tabId) { + // remove callback; call w/ defaults + const cb = messageBoxCallbacks[tabId] + if (cb) { + cleanupCallback(tabId) + cb(false, '', false) + } + } + return state + }, + + onTabUpdated: (state, action) => { + action = makeImmutable(action) + const tabId = action.getIn(['tabValue', 'tabId']) + const detail = tabMessageBoxState.getDetail(state, tabId) + if (detail && detail.get('opener')) { + const url = action.getIn(['tabValue', 'url']) + // check if user has navigated away from site which opened the alert + if (url && url !== detail.get('opener')) { + const removeAction = makeImmutable({tabId: tabId}) + // remove detail from state + state = tabMessageBoxState.removeDetail(state, removeAction) + // remove callback; call w/ defaults + const cb = messageBoxCallbacks[tabId] + if (cb) { + cleanupCallback(tabId) + cb(false, '', false) + } + } + } + return state + }, + + getCallbacks: () => { + return messageBoxCallbacks + } +} + +module.exports = tabMessageBox diff --git a/app/common/state/tabMessageBoxState.js b/app/common/state/tabMessageBoxState.js new file mode 100644 index 00000000000..74a3c79a46e --- /dev/null +++ b/app/common/state/tabMessageBoxState.js @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const tabState = require('./tabState') +const {makeImmutable} = require('./immutableUtil') + +const messageBoxDetail = 'messageBoxDetail' + +const tabMessageBoxState = { + show: (state, action) => { + state = makeImmutable(state) + action = makeImmutable(action) + const tabId = action.get('tabId') + let tabValue = tabId && tabState.getByTabId(state, tabId) + + if (!tabValue) { + return state + } + + let detail = action.get('detail') + if (!detail || detail.size === 0) { + tabValue = tabValue.delete(messageBoxDetail) + } else { + detail = detail.set('opener', tabValue.get('url')) + tabValue = tabValue.set(messageBoxDetail, detail) + } + return tabState.updateTab(state, {tabValue, replace: true}) + }, + + getDetail: (state, tabId) => { + if (typeof tabId !== 'number') { + return null + } + + const tabValue = tabState.getByTabId(state, tabId) + if (tabValue) { + return tabValue.get(messageBoxDetail) || null + } + return null + }, + + update: (state, action) => { + return tabMessageBoxState.show(state, action) + }, + + removeDetail: (state, action) => { + state = makeImmutable(state) + action = makeImmutable(action) + const tabId = action.get('tabId') + let tabValue = tabId && tabState.getByTabId(state, tabId) + + if (!tabValue) { + return state + } + + tabValue = tabValue.delete(messageBoxDetail) + return tabState.updateTab(state, {tabValue, replace: true}) + } +} + +module.exports = tabMessageBoxState diff --git a/app/common/state/tabState.js b/app/common/state/tabState.js index 9ddd08e18d2..73ba0e78c5e 100644 --- a/app/common/state/tabState.js +++ b/app/common/state/tabState.js @@ -180,9 +180,22 @@ const api = { return state.set('tabs', tabs) }, + removeTabField: (state, field) => { + state = makeImmutable(state) + + let tabs = state.get('tabs') + for (let i = 0; i < tabs.size; i++) { + tabs = tabs.deleteIn([i, field]) + } + return state.set('tabs', tabs) + }, + getPersistentState: (state) => { - // TODO(bridiver) - handle restoring tabs state = makeImmutable(state) + + state = api.removeTabField(state, 'messageBoxDetail') + + // TODO(bridiver) - handle restoring tabs return state.delete('tabs') } } diff --git a/app/extensions/brave/locales/en-US/app.properties b/app/extensions/brave/locales/en-US/app.properties index 41b89cf35b1..3d0c3ab20e4 100644 --- a/app/extensions/brave/locales/en-US/app.properties +++ b/app/extensions/brave/locales/en-US/app.properties @@ -247,4 +247,5 @@ widevinePanelTitle=Brave needs to install Google Widevine to proceed installAndAllow=Install and Allow rememberThisDecision=Remember this decision for {{origin}} copyToClipboard.title=Copy to clipboard +preventMoreAlerts=Prevent this page from creating additional dialogs copied=Copied! diff --git a/app/filtering.js b/app/filtering.js index 4e2d3a0a9ca..b0f6bb47f98 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -426,7 +426,7 @@ function registerPermissionHandler (session, partition) { permissionCallbacks[message](0, false) } - appActions.showMessageBox({ + appActions.showNotification({ buttons: [ {text: locale.translation('deny')}, {text: locale.translation('allow')} @@ -440,8 +440,8 @@ function registerPermissionHandler (session, partition) { permissionCallbacks[message] = (buttonIndex, persist) => { permissionCallbacks[message] = null - // hide the message box if this was triggered automatically - appActions.hideMessageBox(message) + // hide the notification if this was triggered automatically + appActions.hideNotification(message) const result = !!(buttonIndex) cb(result) if (persist) { diff --git a/app/importer.js b/app/importer.js index 471f204a811..578d9cc7d1e 100644 --- a/app/importer.js +++ b/app/importer.js @@ -9,7 +9,6 @@ const importer = electron.importer const dialog = electron.dialog const BrowserWindow = electron.BrowserWindow const session = electron.session -const Immutable = require('immutable') const siteUtil = require('../js/state/siteUtil') const AppStore = require('../js/stores/appStore') const siteTags = require('../js/constants/siteTags') @@ -17,8 +16,9 @@ const appActions = require('../js/actions/appActions') const messages = require('../js/constants/messages') const settings = require('../js/constants/settings') const getSetting = require('../js/settings').getSetting -const path = require('path') const locale = require('./locale') +const tabMessageBox = require('./browser/tabMessageBox') +const {makeImmutable} = require('./common/state/immutableUtil') var isMergeFavorites = false var isImportingBookmarks = false @@ -88,7 +88,7 @@ importer.on('add-history-page', (e, history, visitSource) => { } sites.push(site) } - appActions.addSite(Immutable.fromJS(sites)) + appActions.addSite(makeImmutable(sites)) }) importer.on('add-homepage', (e, detail) => { @@ -172,8 +172,8 @@ importer.on('add-bookmarks', (e, bookmarks, topLevelFolder) => { sites.push(site) } } - importedSites = Immutable.fromJS(sites) - appActions.addSite(Immutable.fromJS(sites)) + importedSites = makeImmutable(sites) + appActions.addSite(makeImmutable(sites)) }) importer.on('add-favicons', (e, detail) => { @@ -228,32 +228,32 @@ importer.on('add-cookies', (e, cookies) => { } }) +const getActiveTabId = () => { + const tabs = makeImmutable(AppStore.getState()).get('tabs') + const activeTab = tabs.find((tab) => tab.get('active')) + return activeTab && activeTab.get('id') +} + const showImportWarning = function () { - // The timeout is in case there's a call just after the modal to hide the menu. - // showMessageBox is a modal and blocks everything otherwise, so menu would remain open - // while the dialog is displayed. - setTimeout(() => { - dialog.showMessageBox({ - title: 'Brave', + const tabId = getActiveTabId() + if (tabId) { + tabMessageBox.show(tabId, { message: `${locale.translation('closeFirefoxWarning')}`, - icon: path.join(__dirname, '..', 'app', 'extensions', 'brave', 'img', 'braveAbout.png'), + title: 'Brave', buttons: [locale.translation('closeFirefoxWarningOk')] }) - }, 50) + } } const showImportSuccess = function () { - // The timeout is in case there's a call just after the modal to hide the menu. - // showMessageBox is a modal and blocks everything otherwise, so menu would remain open - // while the dialog is displayed. - setTimeout(() => { - dialog.showMessageBox({ - title: 'Brave', + const tabId = getActiveTabId() + if (tabId) { + tabMessageBox.show(tabId, { message: `${locale.translation('importSuccess')}`, - icon: path.join(__dirname, '..', 'app', 'extensions', 'brave', 'img', 'braveAbout.png'), + title: 'Brave', buttons: [locale.translation('importSuccessOk')] }) - }, 50) + } } importer.on('show-warning-dialog', (e) => { diff --git a/app/index.js b/app/index.js index d0e3b938535..e00547ec6f1 100644 --- a/app/index.js +++ b/app/index.js @@ -49,7 +49,6 @@ if (process.platform === 'win32') { const electron = require('electron') const app = electron.app const BrowserWindow = electron.BrowserWindow -const dialog = electron.dialog const ipcMain = electron.ipcMain const Immutable = require('immutable') const Updater = require('./updater') @@ -287,52 +286,6 @@ app.on('ready', () => { } }) - process.on('window-alert', - (webContents, extraData, title, message, defaultPromptText, - shouldDisplaySuppressCheckbox, isBeforeUnloadDialog, isReload, cb) => { - let suppress = false - const buttons = ['OK'] - if (!webContents || webContents.isDestroyed()) { - cb(false, '', suppress) - } else { - cb(true, '', suppress) - } - - const hostWebContents = webContents.hostWebContents || webContents - dialog.showMessageBox(BrowserWindow.fromWebContents(hostWebContents), { - message, - title, - buttons: buttons - }) - }) - - process.on('window-confirm', - (webContents, extraData, title, message, defaultPromptText, - shouldDisplaySuppressCheckbox, isBeforeUnloadDialog, isReload, cb) => { - let suppress = false - const buttons = ['OK', 'Cancel'] - if (!webContents || webContents.isDestroyed()) { - cb(false, '', suppress) - } - - const hostWebContents = webContents.hostWebContents || webContents - const response = dialog.showMessageBox(BrowserWindow.fromWebContents(hostWebContents), { - message, - title, - buttons: buttons, - cancelId: 1 - }) - cb(!response, '', suppress) - }) - - process.on('window-prompt', - (webContents, extraData, title, message, defaultPromptText, - shouldDisplaySuppressCheckbox, isBeforeUnloadDialog, isReload, cb) => { - console.warn('window.prompt is not supported yet') - let suppress = false - cb(false, '', suppress) - }) - process.on(messages.UNDO_CLOSED_WINDOW, () => { if (lastWindowState) { appActions.newWindow(undefined, undefined, lastWindowState) @@ -398,9 +351,9 @@ app.on('ready', () => { var message = locale.translation('prefsRestart') if (prefsRestartLastValue[config] !== undefined && prefsRestartLastValue[config] !== value) { delete prefsRestartLastValue[config] - appActions.hideMessageBox(message) + appActions.hideNotification(message) } else { - appActions.showMessageBox({ + appActions.showNotification({ buttons: [ {text: locale.translation('yes')}, {text: locale.translation('no')} @@ -419,7 +372,7 @@ app.on('ready', () => { app.quit() } else { delete prefsRestartLastValue[config] - appActions.hideMessageBox(message) + appActions.hideNotification(message) } } if (prefsRestartLastValue[config] === undefined) { diff --git a/app/ledger.js b/app/ledger.js index f82da008b80..0a118027551 100644 --- a/app/ledger.js +++ b/app/ledger.js @@ -503,7 +503,7 @@ if (ipc) { ipc.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex) => { const win = electron.BrowserWindow.getFocusedWindow() if (message === addFundsMessage) { - appActions.hideMessageBox(message) + appActions.hideNotification(message) // See showNotificationAddFunds() for buttons. // buttonIndex === 1 is "Later"; the timestamp until which to delay is set // in showNotificationAddFunds() when triggering this notification. @@ -517,7 +517,7 @@ if (ipc) { } } } else if (message === reconciliationMessage) { - appActions.hideMessageBox(message) + appActions.hideNotification(message) // buttonIndex === 1 is Dismiss if (buttonIndex === 0) { appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) @@ -526,12 +526,12 @@ if (ipc) { 'about:preferences#payments', { singleFrame: true }) } } else if (message === notificationPaymentDoneMessage) { - appActions.hideMessageBox(message) + appActions.hideNotification(message) if (buttonIndex === 0) { appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) } } else if (message === notificationTryPaymentsMessage) { - appActions.hideMessageBox(message) + appActions.hideNotification(message) if (buttonIndex === 1 && win) { win.webContents.send(messages.SHORTCUT_NEW_FRAME, 'about:preferences#payments', { singleFrame: true }) @@ -1944,7 +1944,7 @@ const showDisabledNotifications = () => { return } notificationTryPaymentsMessage = locale.translation('notificationTryPayments') - appActions.showMessageBox({ + appActions.showNotification({ greeting: locale.translation('updateHello'), message: notificationTryPaymentsMessage, buttons: [ @@ -2002,7 +2002,7 @@ const showNotificationAddFunds = () => { appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) addFundsMessage = addFundsMessage || locale.translation('addFundsNotification') - appActions.showMessageBox({ + appActions.showNotification({ greeting: locale.translation('updateHello'), message: addFundsMessage, buttons: [ @@ -2026,7 +2026,7 @@ const showNotificationReviewPublishers = (nextTime) => { appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime) reconciliationMessage = reconciliationMessage || locale.translation('reconciliationNotification') - appActions.showMessageBox({ + appActions.showNotification({ greeting: locale.translation('updateHello'), message: reconciliationMessage, buttons: [ @@ -2047,8 +2047,8 @@ const showNotificationPaymentDone = (transactionContributionFiat) => { .replace(/{{\s*amount\s*}}/, transactionContributionFiat.amount) .replace(/{{\s*currency\s*}}/, transactionContributionFiat.currency) // Hide the 'waiting for deposit' message box if it exists - appActions.hideMessageBox(addFundsMessage) - appActions.showMessageBox({ + appActions.hideNotification(addFundsMessage) + appActions.showNotification({ greeting: locale.translation('updateHello'), message: notificationPaymentDoneMessage, buttons: [ diff --git a/app/renderer/components/messageBox.js b/app/renderer/components/messageBox.js new file mode 100644 index 00000000000..01a33ae85a5 --- /dev/null +++ b/app/renderer/components/messageBox.js @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const React = require('react') +const ImmutableComponent = require('../../../js/components/immutableComponent') +const Dialog = require('../../../js/components/dialog') +const Button = require('../../../js/components/button') +const SwitchControl = require('../../../js/components/switchControl') +const appActions = require('../../../js/actions/appActions') +const KeyCodes = require('../../common/constants/keyCodes') +const config = require('../../../js/constants/config') +const {makeImmutable} = require('../../common/state/immutableUtil') +const {StyleSheet, css} = require('aphrodite') +const commonStyles = require('./styles/commonStyles') + +class MessageBox extends ImmutableComponent { + constructor () { + super() + this.onKeyDown = this.onKeyDown.bind(this) + this.onSuppressChanged = this.onSuppressChanged.bind(this) + } + + componentWillMount () { + document.addEventListener('keydown', this.onKeyDown) + } + componentWillUnmount () { + document.removeEventListener('keydown', this.onKeyDown) + } + + get tabId () { + return this.props.tabId || '' + } + + get title () { + const msgBoxTitle = (this.props.detail && this.props.detail.get('title')) || '' + return msgBoxTitle.replace(config.braveExtensionId, 'Brave') + } + + get message () { + return (this.props.detail && this.props.detail.get('message')) || '' + } + + get buttons () { + return (this.props.detail && this.props.detail.get('buttons')) || makeImmutable(['ok']) + } + + get cancelId () { + return this.props.detail && this.props.detail.get('cancelId') + } + + get suppress () { + return (this.props.detail && this.props.detail.get('suppress')) || false + } + + get showSuppress () { + return (this.props.detail && this.props.detail.get('showSuppress')) || false + } + + onKeyDown (e) { + if (this.props.isActive) { + switch (e.keyCode) { + case KeyCodes.ENTER: + this.onDismiss(this.tabId) + break + case KeyCodes.ESC: + this.onDismiss(this.tabId, this.cancelId) + break + } + } + } + + onSuppressChanged (e) { + const detail = this.props.detail.toJS() + detail.suppress = !detail.suppress + appActions.tabMessageBoxUpdated(this.tabId, detail) + } + + onDismiss (tabId, buttonId) { + const response = { + suppress: this.suppress, + result: true + } + + if (typeof this.cancelId === 'number') { + response.result = buttonId !== this.cancelId + } + + appActions.tabMessageBoxDismissed(this.tabId, response) + } + + get messageBoxButtons () { + const buttons = [] + + for (let index = (this.buttons.size - 1); index > -1; index--) { + buttons.push(