diff --git a/browser/base/content/newtab/newTab.css b/browser/base/content/newtab/newTab.css index ba4c8376610..1b7966fd72d 100644 --- a/browser/base/content/newtab/newTab.css +++ b/browser/base/content/newtab/newTab.css @@ -353,7 +353,7 @@ input[type=button] { } @media not all and (max-resolution: 1dppx) { - #newtab-search-icon.magnifier { + #newtab-search-logo.magnifier { background-image: url("chrome://browser/skin/magnifier@2x.png"); } } diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index 9cfc8b92e16..b628aea36b1 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -1040,6 +1040,8 @@ [currentEngine.name], 1); document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine-name") .setAttribute("value", headerText); + document.getAnonymousElementByAttribute(this, "anonid", "searchbar-engine") + .engine = currentEngine; // Update the 'Search for with:" header. let headerSearchText = @@ -1198,11 +1200,13 @@ return; // ignore right clicks. let button = event.originalTarget; - if (button.localName != "button" || !button.engine) + let engine = button.engine || button.parentNode.engine; + + if (!engine) return; let searchbar = document.getElementById("searchbar"); - searchbar.handleSearchCommand(event, button.engine); + searchbar.handleSearchCommand(event, engine); ]]> { + return parseInt(v, 10) == 100; + }); yield promiseTabLoadEvent(tab1, "about:home"); + yield attributeChangePromise; is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:home"); yield promiseTabHistoryNavigation(-1, function() { return parseInt(zoomResetButton.label, 10) == 110; diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js index 9dfe6bc3eca..e331f2e497c 100644 --- a/browser/components/customizableui/test/head.js +++ b/browser/components/customizableui/test/head.js @@ -461,6 +461,34 @@ function promiseTabHistoryNavigation(aDirection = -1, aConditionFn) { return deferred.promise; } +/** + * Wait for an attribute on a node to change + * + * @param aNode Node on which the mutation is expected + * @param aAttribute The attribute we're interested in + * @param aFilterFn A function to check if the new value is what we want. + * @return {Promise} resolved when the requisite mutation shows up. + */ +function promiseAttributeMutation(aNode, aAttribute, aFilterFn) { + return new Promise((resolve, reject) => { + info("waiting for mutation of attribute '" + aAttribute + "'."); + let obs = new MutationObserver((mutations) => { + for (let mut of mutations) { + let attr = mut.attributeName; + let newValue = mut.target.getAttribute(attr); + if (aFilterFn(newValue)) { + ok(true, "mutation occurred: attribute '" + attr + "' changed to '" + newValue + "' from '" + mut.oldValue + "'."); + obs.disconnect(); + resolve(); + } else { + info("Ignoring mutation that produced value " + newValue + " because of filter."); + } + } + }); + obs.observe(aNode, {attributeFilter: [aAttribute]}); + }); +} + function popupShown(aPopup) { return promisePopupEvent(aPopup, "shown"); } diff --git a/browser/components/loop/content/conversation.html b/browser/components/loop/content/conversation.html index 9e2255f799b..28a43eb62c5 100644 --- a/browser/components/loop/content/conversation.html +++ b/browser/components/loop/content/conversation.html @@ -37,6 +37,8 @@ + + diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index ad1400331bd..ac0e7293b81 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -730,6 +730,15 @@ loop.panel = (function(_, mozL10n) { this.stopListening(this.props.store); }, + componentWillUpdate: function(nextProps, nextState) { + // If we've just created a room, close the panel - the store will open + // the room. + if (this.state.pendingCreation && + !nextState.pendingCreation && !nextState.error) { + this.closeWindow(); + } + }, + _onStoreStateChanged: function() { this.setState(this.props.store.getStoreState()); }, @@ -747,8 +756,6 @@ loop.panel = (function(_, mozL10n) { }, handleCreateButtonClick: function() { - this.closeWindow(); - this.props.dispatcher.dispatch(new sharedActions.CreateRoom({ nameTemplate: mozL10n.get("rooms_default_room_name_template"), roomOwner: this.props.userDisplayName @@ -1003,7 +1010,8 @@ loop.panel = (function(_, mozL10n) { var notifications = new sharedModels.NotificationCollection(); var dispatcher = new loop.Dispatcher(); var roomStore = new loop.store.RoomStore(dispatcher, { - mozLoop: navigator.mozLoop + mozLoop: navigator.mozLoop, + notifications: notifications }); React.renderComponent(PanelView({ diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index 864d86adf58..1847458806c 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -730,6 +730,15 @@ loop.panel = (function(_, mozL10n) { this.stopListening(this.props.store); }, + componentWillUpdate: function(nextProps, nextState) { + // If we've just created a room, close the panel - the store will open + // the room. + if (this.state.pendingCreation && + !nextState.pendingCreation && !nextState.error) { + this.closeWindow(); + } + }, + _onStoreStateChanged: function() { this.setState(this.props.store.getStoreState()); }, @@ -747,8 +756,6 @@ loop.panel = (function(_, mozL10n) { }, handleCreateButtonClick: function() { - this.closeWindow(); - this.props.dispatcher.dispatch(new sharedActions.CreateRoom({ nameTemplate: mozL10n.get("rooms_default_room_name_template"), roomOwner: this.props.userDisplayName @@ -1003,7 +1010,8 @@ loop.panel = (function(_, mozL10n) { var notifications = new sharedModels.NotificationCollection(); var dispatcher = new loop.Dispatcher(); var roomStore = new loop.store.RoomStore(dispatcher, { - mozLoop: navigator.mozLoop + mozLoop: navigator.mozLoop, + notifications: notifications }); React.renderComponent( + + diff --git a/browser/components/loop/content/shared/js/actions.js b/browser/components/loop/content/shared/js/actions.js index 22d2ad8e49c..6b1f95ee55d 100644 --- a/browser/components/loop/content/shared/js/actions.js +++ b/browser/components/loop/content/shared/js/actions.js @@ -197,12 +197,23 @@ loop.shared.actions = (function() { roomOwner: String }), + /** + * When a room has been created. + * XXX: should move to some roomActions module - refs bug 1079284 + */ + CreatedRoom: Action.define("createdRoom", { + roomToken: String + }), + /** * Rooms creation error. * XXX: should move to some roomActions module - refs bug 1079284 */ CreateRoomError: Action.define("createRoomError", { - error: Error + // There's two types of error possible - one thrown by our code (and Error) + // and the other is an Object about the error codes from the server as + // returned by the Hawk request. + error: Object }), /** @@ -218,7 +229,10 @@ loop.shared.actions = (function() { * XXX: should move to some roomActions module - refs bug 1079284 */ DeleteRoomError: Action.define("deleteRoomError", { - error: Error + // There's two types of error possible - one thrown by our code (and Error) + // and the other is an Object about the error codes from the server as + // returned by the Hawk request. + error: Object }), /** diff --git a/browser/components/loop/content/shared/js/activeRoomStore.js b/browser/components/loop/content/shared/js/activeRoomStore.js index df909a16436..05713606a4c 100644 --- a/browser/components/loop/content/shared/js/activeRoomStore.js +++ b/browser/components/loop/content/shared/js/activeRoomStore.js @@ -21,31 +21,7 @@ loop.store.ActiveRoomStore = (function() { ROOM_FULL: 202 }; - var ROOM_STATES = loop.store.ROOM_STATES = { - // The initial state of the room - INIT: "room-init", - // The store is gathering the room data - GATHER: "room-gather", - // The store has got the room data - READY: "room-ready", - // Obtaining media from the user - MEDIA_WAIT: "room-media-wait", - // The room is known to be joined on the loop-server - JOINED: "room-joined", - // The room is connected to the sdk server. - SESSION_CONNECTED: "room-session-connected", - // There are participants in the room. - HAS_PARTICIPANTS: "room-has-participants", - // There was an issue with the room - FAILED: "room-failed", - // The room is full - FULL: "room-full", - // The room conversation has ended - ENDED: "room-ended", - // The window is closing - CLOSING: "room-closing" - }; - + var ROOM_STATES = loop.store.ROOM_STATES; /** * Active room store. * diff --git a/browser/components/loop/content/shared/js/fxOSActiveRoomStore.js b/browser/components/loop/content/shared/js/fxOSActiveRoomStore.js new file mode 100644 index 00000000000..553025fae8c --- /dev/null +++ b/browser/components/loop/content/shared/js/fxOSActiveRoomStore.js @@ -0,0 +1,164 @@ +/* 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/. */ + +/* global loop:true */ + +var loop = loop || {}; +loop.store = loop.store || {}; + +loop.store.FxOSActiveRoomStore = (function() { + "use strict"; + var sharedActions = loop.shared.actions; + var ROOM_STATES = loop.store.ROOM_STATES; + + var FxOSActiveRoomStore = loop.store.createStore({ + actions: [ + "fetchServerData" + ], + + initialize: function(options) { + if (!options.mozLoop) { + throw new Error("Missing option mozLoop"); + } + this._mozLoop = options.mozLoop; + }, + + /** + * Returns initial state data for this active room. + */ + getInitialStoreState: function() { + return { + roomState: ROOM_STATES.INIT, + audioMuted: false, + videoMuted: false, + failureReason: undefined + }; + }, + + /** + * Registers the actions with the dispatcher that this store is interested + * in after the initial setup has been performed. + */ + _registerPostSetupActions: function() { + this.dispatcher.register(this, [ + "joinRoom" + ]); + }, + + /** + * Execute fetchServerData event action from the dispatcher. Although + * this is to fetch the server data - for rooms on the standalone client, + * we don't actually need to get any data. Therefore we just save the + * data that is given to us for when the user chooses to join the room. + * + * @param {sharedActions.FetchServerData} actionData + */ + fetchServerData: function(actionData) { + if (actionData.windowType !== "room") { + // Nothing for us to do here, leave it to other stores. + return; + } + + this._registerPostSetupActions(); + + this.setStoreState({ + roomToken: actionData.token, + roomState: ROOM_STATES.READY + }); + + }, + + /** + * Handles the action to join to a room. + */ + joinRoom: function() { + // Reset the failure reason if necessary. + if (this.getStoreState().failureReason) { + this.setStoreState({failureReason: undefined}); + } + + this._setupOutgoingRoom(true); + }, + + /** + * Sets up an outgoing room. It will try launching the activity to let the + * FirefoxOS loop app handle the call. If the activity fails: + * - if installApp is true, then it'll try to install the FirefoxOS loop + * app. + * - if installApp is false, then it'll just log and error and fail. + * + * @param {boolean} installApp + */ + _setupOutgoingRoom: function(installApp) { + var request = new MozActivity({ + name: "room-call", + data: { + type: "loop/rToken", + token: this.getStoreState("roomToken") + } + }); + + request.onsuccess = function() {}; + + request.onerror = (function(event) { + if (!installApp) { + // This really should not happen ever. + console.error( + "Unexpected activity launch error after the app has been installed"); + return; + } + if (event.target.error.name !== "NO_PROVIDER") { + console.error ("Unexpected " + event.target.error.name); + return; + } + // We need to install the FxOS app. + this.setStoreState({ + marketplaceSrc: loop.config.marketplaceUrl, + onMarketplaceMessage: this._onMarketplaceMessage.bind(this) + }); + }).bind(this); + }, + + /** + * This method will handle events generated on the marketplace frame. It + * will launch the FirefoxOS loop app installation, and receive the result + * of the installation. + * + * @param {DOMEvent} event + */ + _onMarketplaceMessage: function(event) { + var message = event.data; + switch (message.name) { + case "loaded": + var marketplace = window.document.getElementById("marketplace"); + // Once we have it loaded, we request the installation of the FxOS + // Loop client app. We will be receiving the result of this action + // via postMessage from the child iframe. + marketplace.contentWindow.postMessage({ + "name": "install-package", + "data": { + "product": { + "name": loop.config.fxosApp.name, + "manifest_url": loop.config.fxosApp.manifestUrl, + "is_packaged": true + } + } + }, "*"); + break; + case "install-package": + window.removeEventListener("message", this.onMarketplaceMessage); + if (message.error) { + console.error(message.error.error); + return; + } + // We installed the FxOS app, so we can continue with the call + // process. + this._setupOutgoingRoom(false); + break; + } + } + }); + + return FxOSActiveRoomStore; +})(); diff --git a/browser/components/loop/content/shared/js/roomStates.js b/browser/components/loop/content/shared/js/roomStates.js new file mode 100644 index 00000000000..439046161e7 --- /dev/null +++ b/browser/components/loop/content/shared/js/roomStates.js @@ -0,0 +1,33 @@ +/* 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/. */ + +/* global loop:true */ + +var loop = loop || {}; +loop.store = loop.store || {}; + +loop.store.ROOM_STATES = { + // The initial state of the room + INIT: "room-init", + // The store is gathering the room data + GATHER: "room-gather", + // The store has got the room data + READY: "room-ready", + // Obtaining media from the user + MEDIA_WAIT: "room-media-wait", + // The room is known to be joined on the loop-server + JOINED: "room-joined", + // The room is connected to the sdk server. + SESSION_CONNECTED: "room-session-connected", + // There are participants in the room. + HAS_PARTICIPANTS: "room-has-participants", + // There was an issue with the room + FAILED: "room-failed", + // The room is full + FULL: "room-full", + // The room conversation has ended + ENDED: "room-ended", + // The window is closing + CLOSING: "room-closing" +}; diff --git a/browser/components/loop/content/shared/js/roomStore.js b/browser/components/loop/content/shared/js/roomStore.js index ca05036a299..c9b23c607c6 100644 --- a/browser/components/loop/content/shared/js/roomStore.js +++ b/browser/components/loop/content/shared/js/roomStore.js @@ -7,7 +7,7 @@ var loop = loop || {}; loop.store = loop.store || {}; -(function() { +(function(mozL10n) { "use strict"; /** @@ -67,6 +67,8 @@ loop.store = loop.store || {}; * - {mozLoop} mozLoop The MozLoop API object. * - {ActiveRoomStore} activeRoomStore An optional substore for active room * state. + * - {Notifications} notifications An optional notifications item that is + * required if create actions are to be used */ loop.store.RoomStore = loop.store.createStore({ /** @@ -89,6 +91,7 @@ loop.store = loop.store || {}; */ actions: [ "createRoom", + "createdRoom", "createRoomError", "copyRoomUrl", "deleteRoom", @@ -107,6 +110,7 @@ loop.store = loop.store || {}; throw new Error("Missing option mozLoop"); } this._mozLoop = options.mozLoop; + this._notifications = options.notifications; if (options.activeRoomStore) { this.activeRoomStore = options.activeRoomStore; @@ -257,7 +261,10 @@ loop.store = loop.store || {}; * @param {sharedActions.CreateRoom} actionData The new room information. */ createRoom: function(actionData) { - this.setStoreState({pendingCreation: true}); + this.setStoreState({ + pendingCreation: true, + error: null, + }); var roomCreationData = { roomName: this._generateNewRoomName(actionData.nameTemplate), @@ -266,19 +273,32 @@ loop.store = loop.store || {}; expiresIn: this.defaultExpiresIn }; + this._notifications.remove("create-room-error"); + this._mozLoop.rooms.create(roomCreationData, function(err, createdRoom) { - this.setStoreState({pendingCreation: false}); if (err) { this.dispatchAction(new sharedActions.CreateRoomError({error: err})); return; } - // Opens the newly created room - this.dispatchAction(new sharedActions.OpenRoom({ + + this.dispatchAction(new sharedActions.CreatedRoom({ roomToken: createdRoom.roomToken })); }.bind(this)); }, + /** + * Executed when a room has been created + */ + createdRoom: function(actionData) { + this.setStoreState({pendingCreation: false}); + + // Opens the newly created room + this.dispatchAction(new sharedActions.OpenRoom({ + roomToken: actionData.roomToken + })); + }, + /** * Executed when a room creation error occurs. * @@ -289,6 +309,13 @@ loop.store = loop.store || {}; error: actionData.error, pendingCreation: false }); + + // XXX Needs a more descriptive error - bug 1109151. + this._notifications.set({ + id: "create-room-error", + level: "error", + message: mozL10n.get("generic_failure_title") + }); }, /** @@ -406,4 +433,4 @@ loop.store = loop.store || {}; this.setStoreState({error: actionData.error}); } }); -})(); +})(document.mozL10n || navigator.mozL10n); diff --git a/browser/components/loop/jar.mn b/browser/components/loop/jar.mn index 01425e94c65..2b4bf87d181 100644 --- a/browser/components/loop/jar.mn +++ b/browser/components/loop/jar.mn @@ -66,22 +66,24 @@ browser.jar: content/browser/loop/shared/img/telefonica@2x.png (content/shared/img/telefonica@2x.png) # Shared scripts - content/browser/loop/shared/js/actions.js (content/shared/js/actions.js) - content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js) - content/browser/loop/shared/js/store.js (content/shared/js/store.js) - content/browser/loop/shared/js/roomStore.js (content/shared/js/roomStore.js) - content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js) - content/browser/loop/shared/js/feedbackStore.js (content/shared/js/feedbackStore.js) - content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js) - content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js) - content/browser/loop/shared/js/models.js (content/shared/js/models.js) - content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js) - content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js) - content/browser/loop/shared/js/views.js (content/shared/js/views.js) - content/browser/loop/shared/js/feedbackViews.js (content/shared/js/feedbackViews.js) - content/browser/loop/shared/js/utils.js (content/shared/js/utils.js) - content/browser/loop/shared/js/validate.js (content/shared/js/validate.js) - content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js) + content/browser/loop/shared/js/actions.js (content/shared/js/actions.js) + content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js) + content/browser/loop/shared/js/store.js (content/shared/js/store.js) + content/browser/loop/shared/js/roomStore.js (content/shared/js/roomStore.js) + content/browser/loop/shared/js/roomStates.js (content/shared/js/roomStates.js) + content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js) + content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js) + content/browser/loop/shared/js/feedbackStore.js (content/shared/js/feedbackStore.js) + content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js) + content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js) + content/browser/loop/shared/js/models.js (content/shared/js/models.js) + content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js) + content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js) + content/browser/loop/shared/js/views.js (content/shared/js/views.js) + content/browser/loop/shared/js/feedbackViews.js (content/shared/js/feedbackViews.js) + content/browser/loop/shared/js/utils.js (content/shared/js/utils.js) + content/browser/loop/shared/js/validate.js (content/shared/js/validate.js) + content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js) # Shared libs #ifdef DEBUG diff --git a/browser/components/loop/standalone/Makefile b/browser/components/loop/standalone/Makefile index 8071f7c54aa..87c177f34f1 100644 --- a/browser/components/loop/standalone/Makefile +++ b/browser/components/loop/standalone/Makefile @@ -83,6 +83,7 @@ config: @echo "loop.config.learnMoreUrl = '`echo $(LOOP_PRODUCT_HOMEPAGE_URL)`';" >> content/config.js @echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js @echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js + @echo "loop.config.fxosApp.rooms = true;" >> content/config.js @echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js @echo "loop.config.roomsSupportUrl = 'https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc';" >> content/config.js @echo "loop.config.guestSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';" >> content/config.js diff --git a/browser/components/loop/standalone/content/index.html b/browser/components/loop/standalone/content/index.html index c9f8aaf0746..9768200cb3e 100644 --- a/browser/components/loop/standalone/content/index.html +++ b/browser/components/loop/standalone/content/index.html @@ -98,12 +98,15 @@ + + + diff --git a/browser/components/loop/standalone/content/js/fxOSMarketplace.js b/browser/components/loop/standalone/content/js/fxOSMarketplace.js new file mode 100644 index 00000000000..c84b0e52bff --- /dev/null +++ b/browser/components/loop/standalone/content/js/fxOSMarketplace.js @@ -0,0 +1,37 @@ +/** @jsx React.DOM */ + +var loop = loop || {}; +loop.fxOSMarketplaceViews = (function() { + "use strict"; + + /** + * The Firefox Marketplace exposes a web page that contains a postMesssage + * based API that wraps a small set of functionality from the WebApps API + * that allow us to request the installation of apps given their manifest + * URL. We will be embedding the content of this web page within an hidden + * iframe in case that we need to request the installation of the FxOS Loop + * client. + */ + var FxOSHiddenMarketplaceView = React.createClass({displayName: 'FxOSHiddenMarketplaceView', + render: function() { + return React.DOM.iframe({id: "marketplace", src: this.props.marketplaceSrc, hidden: true}); + }, + + componentDidUpdate: function() { + // This happens only once when we change the 'src' property of the iframe. + if (this.props.onMarketplaceMessage) { + // The reason for listening on the global window instead of on the + // iframe content window is because the Marketplace is doing a + // window.top.postMessage. + // XXX Bug 1097703: This should be changed to an action when the old + // style URLs go away. + window.addEventListener("message", this.props.onMarketplaceMessage); + } + } + }); + + return { + FxOSHiddenMarketplaceView: FxOSHiddenMarketplaceView + }; + +})(); diff --git a/browser/components/loop/standalone/content/js/fxOSMarketplace.jsx b/browser/components/loop/standalone/content/js/fxOSMarketplace.jsx new file mode 100644 index 00000000000..2fd8d9f2a55 --- /dev/null +++ b/browser/components/loop/standalone/content/js/fxOSMarketplace.jsx @@ -0,0 +1,37 @@ +/** @jsx React.DOM */ + +var loop = loop || {}; +loop.fxOSMarketplaceViews = (function() { + "use strict"; + + /** + * The Firefox Marketplace exposes a web page that contains a postMesssage + * based API that wraps a small set of functionality from the WebApps API + * that allow us to request the installation of apps given their manifest + * URL. We will be embedding the content of this web page within an hidden + * iframe in case that we need to request the installation of the FxOS Loop + * client. + */ + var FxOSHiddenMarketplaceView = React.createClass({ + render: function() { + return