Merge m-c to inbound, a=merge

This commit is contained in:
Wes Kocher 2015-09-28 16:31:29 -07:00
commit 516c6171a4
81 changed files with 1726 additions and 417 deletions

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="828317e64d28138f24d578ab340c2a0ff8552df0"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="f004530b30a63c08a16d82536858600446b2abf5"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>

View File

@ -1,9 +1,9 @@
{
"git": {
"git_revision": "01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c",
"git_revision": "4c0a6d4e8501368db8e5d6029a41db985ef1252a",
"remote": "https://git.mozilla.org/releases/gaia.git",
"branch": ""
},
"revision": "d7e928f87e2cc34121db52e65f2eeb7598a01412",
"revision": "61c5a1255e159b89caebf736d3c009a3778a5c42",
"repo_path": "integration/gaia-central"
}

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>

View File

@ -18,7 +18,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="828317e64d28138f24d578ab340c2a0ff8552df0"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="f004530b30a63c08a16d82536858600446b2abf5"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="4c0a6d4e8501368db8e5d6029a41db985ef1252a"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="9465d16f95ab87636b2ae07538ee88e5aeff2d7d"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -468,6 +468,13 @@ loop.shared.actions = (function() {
// socialShareProviders: Array - Optional.
}),
/**
* Notifies if the user agent will handle the room or not.
*/
UserAgentHandlesRoom: Action.define("userAgentHandlesRoom", {
handlesRoom: Boolean
}),
/**
* Updates the Social API information when it is received.
* XXX: should move to some roomActions module - refs bug 1079284
@ -483,6 +490,16 @@ loop.shared.actions = (function() {
JoinRoom: Action.define("joinRoom", {
}),
/**
* A special action for metrics logging to define what type of join
* occurred when JoinRoom was activated.
* XXX: should move to some roomActions module - refs bug 1079284
*/
MetricsLogJoinRoom: Action.define("metricsLogJoinRoom", {
userAgentHandledRoom: Boolean
// ownRoom: Boolean - Optional. Expected if firefoxHandledRoom is true.
}),
/**
* Starts the process for the user to join the room.
* XXX: should move to some roomActions module - refs bug 1079284

View File

@ -132,6 +132,9 @@ loop.store.ActiveRoomStore = (function() {
videoMuted: false,
remoteVideoEnabled: false,
failureReason: undefined,
// Whether or not Firefox can handle this room in the conversation
// window, rather than us handling it in the standalone.
userAgentHandlesRoom: undefined,
// Tracks if the room has been used during this
// session. 'Used' means at least one call has been placed
// with it. Entering and leaving the room without seeing
@ -237,6 +240,7 @@ loop.store.ActiveRoomStore = (function() {
"roomFailure",
"retryAfterRoomFailure",
"updateRoomInfo",
"userAgentHandlesRoom",
"gotMediaPermission",
"joinRoom",
"joinedRoom",
@ -327,6 +331,9 @@ loop.store.ActiveRoomStore = (function() {
* This action is only used for the standalone UI.
*
* @param {sharedActions.FetchServerData} actionData
* @return {Promise} For testing purposes, returns a promise that is resolved
* once data is received from the server, and it is determined
* if Firefox handles the room or not.
*/
fetchServerData: function(actionData) {
if (actionData.windowType !== "room") {
@ -342,68 +349,144 @@ loop.store.ActiveRoomStore = (function() {
this._registerPostSetupActions();
this._getRoomDataForStandalone(actionData.cryptoKey);
var dataPromise = this._getRoomDataForStandalone(actionData.cryptoKey);
var userAgentHandlesPromise = this._promiseDetectUserAgentHandles();
return Promise.all([dataPromise, userAgentHandlesPromise]).then(function(results) {
results.forEach(function(result) {
this.dispatcher.dispatch(result);
}.bind(this));
}.bind(this));
},
/**
* Gets the room data for the standalone, decrypting it as necessary.
*
* @param {String} roomCryptoKey The crypto key associated to the room.
* @return {Promise} A promise that is resolved once the get
* and decryption is complete.
*/
_getRoomDataForStandalone: function(roomCryptoKey) {
this._mozLoop.rooms.get(this._storeState.roomToken, function(err, result) {
if (err) {
this.dispatchAction(new sharedActions.RoomFailure({
error: err,
failedJoinRequest: false
return new Promise(function(resolve, reject) {
this._mozLoop.rooms.get(this._storeState.roomToken, function(err, result) {
if (err) {
resolve(new sharedActions.RoomFailure({
error: err,
failedJoinRequest: false
}));
return;
}
var roomInfoData = new sharedActions.UpdateRoomInfo({
// If we've got this far, then we want to go to the ready state
// regardless of success of failure. This is because failures of
// crypto don't stop the user using the room, they just stop
// us putting up the information.
roomState: ROOM_STATES.READY,
roomUrl: result.roomUrl
});
if (!result.context && !result.roomName) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_DATA;
resolve(roomInfoData);
return;
}
// This handles 'legacy', non-encrypted room names.
if (result.roomName && !result.context) {
roomInfoData.roomName = result.roomName;
resolve(roomInfoData);
return;
}
if (!crypto.isSupported()) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED;
resolve(roomInfoData);
return;
}
if (!roomCryptoKey) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_CRYPTO_KEY;
resolve(roomInfoData);
return;
}
crypto.decryptBytes(roomCryptoKey, result.context.value)
.then(function(decryptedResult) {
var realResult = JSON.parse(decryptedResult);
roomInfoData.roomDescription = realResult.description;
roomInfoData.roomContextUrls = realResult.urls;
roomInfoData.roomName = realResult.roomName;
resolve(roomInfoData);
}, function(error) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
resolve(roomInfoData);
});
}.bind(this));
}.bind(this));
},
/**
* If the user agent is Firefox, it sends a message to Firefox to see if
* the room can be handled within Firefox rather than the standalone UI.
*
* @return {Promise} A promise that is resolved once it has been determined
* if Firefox can handle the room.
*/
_promiseDetectUserAgentHandles: function() {
return new Promise(function(resolve, reject) {
function resolveWithNotHandlingResponse() {
resolve(new sharedActions.UserAgentHandlesRoom({
handlesRoom: false
}));
}
// If we're not Firefox, don't even try to see if it can be handled
// in the browser.
if (!loop.shared.utils.isFirefox(navigator.userAgent)) {
resolveWithNotHandlingResponse();
return;
}
var roomInfoData = new sharedActions.UpdateRoomInfo({
// If we've got this far, then we want to go to the ready state
// regardless of success of failure. This is because failures of
// crypto don't stop the user using the room, they just stop
// us putting up the information.
roomState: ROOM_STATES.READY,
roomUrl: result.roomUrl
});
// Set up a timer in case older versions of Firefox don't give us a response.
var timer = setTimeout(resolveWithNotHandlingResponse, 250);
var webChannelListenerFunc;
if (!result.context && !result.roomName) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_DATA;
this.dispatcher.dispatch(roomInfoData);
return;
// Listen for the result.
function webChannelListener(e) {
if (e.detail.id !== "loop-link-clicker") {
return;
}
// Stop the default response.
clearTimeout(timer);
// Remove the listener.
window.removeEventListener("WebChannelMessageToContent", webChannelListenerFunc);
// Resolve with the details of if we're able to handle or not.
resolve(new sharedActions.UserAgentHandlesRoom({
handlesRoom: !!e.detail.message && e.detail.message.response
}));
}
// This handles 'legacy', non-encrypted room names.
if (result.roomName && !result.context) {
roomInfoData.roomName = result.roomName;
this.dispatcher.dispatch(roomInfoData);
return;
}
webChannelListenerFunc = webChannelListener.bind(this);
if (!crypto.isSupported()) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED;
this.dispatcher.dispatch(roomInfoData);
return;
}
window.addEventListener("WebChannelMessageToContent", webChannelListenerFunc);
if (!roomCryptoKey) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_CRYPTO_KEY;
this.dispatcher.dispatch(roomInfoData);
return;
}
var dispatcher = this.dispatcher;
crypto.decryptBytes(roomCryptoKey, result.context.value)
.then(function(decryptedResult) {
var realResult = JSON.parse(decryptedResult);
roomInfoData.roomDescription = realResult.description;
roomInfoData.roomContextUrls = realResult.urls;
roomInfoData.roomName = realResult.roomName;
dispatcher.dispatch(roomInfoData);
}, function(error) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
dispatcher.dispatch(roomInfoData);
});
// Now send a message to the chrome to see if it can handle this room.
window.dispatchEvent(new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "loop-link-clicker",
message: {
command: "checkWillOpenRoom",
roomToken: this._storeState.roomToken
}
}
}));
}.bind(this));
},
@ -426,6 +509,18 @@ loop.store.ActiveRoomStore = (function() {
this.setStoreState(newState);
},
/**
* Handles the userAgentHandlesRoom action. Updates the store's data with
* the new state.
*
* @param {sharedActions.userAgentHandlesRoom} actionData
*/
userAgentHandlesRoom: function(actionData) {
this.setStoreState({
userAgentHandlesRoom: actionData.handlesRoom
});
},
/**
* Handles the updateSocialShareInfo action. Updates the room data with new
* Social API info.
@ -477,14 +572,11 @@ loop.store.ActiveRoomStore = (function() {
},
/**
* Handles the action to join to a room.
* Checks that there are audio and video devices available, and joins the
* room if there are. If there aren't then it will dispatch a ConnectionFailure
* action with NO_MEDIA.
*/
joinRoom: function() {
// Reset the failure reason if necessary.
if (this.getStoreState().failureReason) {
this.setStoreState({failureReason: undefined});
}
_checkDevicesAndJoinRoom: function() {
// XXX Ideally we'd do this check before joining a room, but we're waiting
// for the UX for that. See bug 1166824. In the meantime this gives us
// additional information for analysis.
@ -501,6 +593,77 @@ loop.store.ActiveRoomStore = (function() {
}.bind(this));
},
/**
* Hands off the room join to Firefox.
*/
_handoffRoomJoin: function() {
var channelListener;
function handleRoomJoinResponse(e) {
if (e.detail.id !== "loop-link-clicker") {
return;
}
window.removeEventListener("WebChannelMessageToContent", channelListener);
if (!e.detail.message || !e.detail.message.response) {
// XXX Firefox didn't handle this, even though it said it could
// previously. We should add better user feedback here.
console.error("Firefox didn't handle room it said it could.");
} else {
this.dispatcher.dispatch(new sharedActions.JoinedRoom({
apiKey: "",
sessionToken: "",
sessionId: "",
expires: 0
}));
}
}
channelListener = handleRoomJoinResponse.bind(this);
window.addEventListener("WebChannelMessageToContent", channelListener);
// Now we're set up, dispatch an event.
window.dispatchEvent(new window.CustomEvent("WebChannelMessageToChrome", {
detail: {
id: "loop-link-clicker",
message: {
command: "openRoom",
roomToken: this._storeState.roomToken
}
}
}));
},
/**
* Handles the action to join to a room.
*/
joinRoom: function() {
// Reset the failure reason if necessary.
if (this.getStoreState().failureReason) {
this.setStoreState({ failureReason: undefined });
}
// If we're standalone and we know Firefox can handle the room, then hand
// it off.
if (this._storeState.standalone && this._storeState.userAgentHandlesRoom) {
this.dispatcher.dispatch(new sharedActions.MetricsLogJoinRoom({
userAgentHandledRoom: true,
ownRoom: true
}));
this._handoffRoomJoin();
return;
}
this.dispatcher.dispatch(new sharedActions.MetricsLogJoinRoom({
userAgentHandledRoom: false
}));
// Otherwise, we handle the room ourselves.
this._checkDevicesAndJoinRoom();
},
/**
* Handles the action that signifies when media permission has been
* granted and starts joining the room.
@ -540,6 +703,15 @@ loop.store.ActiveRoomStore = (function() {
* @param {sharedActions.JoinedRoom} actionData
*/
joinedRoom: function(actionData) {
// If we're standalone and firefox is handling, then just store the new
// state. No need to do anything else.
if (this._storeState.standalone && this._storeState.userAgentHandlesRoom) {
this.setStoreState({
roomState: ROOM_STATES.JOINED
});
return;
}
this.setStoreState({
apiKey: actionData.apiKey,
sessionToken: actionData.sessionToken,

View File

@ -25,6 +25,27 @@ body,
background: #000;
}
/* Logos */
.loop-logo-text {
background: url("../img/hello-logo-text.svg") no-repeat;
width: 200px;
height: 36px;
}
.loop-logo {
background: url("../shared/img/helloicon.svg") no-repeat;
width: 100px;
height: 100px;
}
.mozilla-logo {
background: url("../img/mozilla-logo.svg#logo") no-repeat;
background-size: contain;
width: 100px;
height: 30px;
}
.room-conversation-wrapper > .beta-logo {
position: fixed;
top: 0;
@ -43,7 +64,7 @@ body,
margin: 0 auto;
height: 30px;
background-size: contain;
background-image: url("../shared/img/mozilla-logo.png");
background-image: url("../img/mozilla-logo.svg#logo-white");
background-repeat: no-repeat;
}
@ -138,6 +159,45 @@ html[dir="rtl"] .rooms-footer .footer-logo {
line-height: 24px;
}
/**
* Handle in Firefox views
*/
.handle-user-agent-view-scroller {
height: 100%;
overflow: scroll;
}
.handle-user-agent-view {
margin: 2rem auto;
width: 500px;
}
.handle-user-agent-view > .info-panel {
padding-bottom: 40px;
font-size: 1.6rem;
}
.handle-user-agent-view > p,
.handle-user-agent-view > .info-panel > p {
margin-top: 0;
margin: 2rem auto;
}
.handle-user-agent-view > .info-panel > button {
width: 80%;
height: 4rem;
font-size: 1.6rem;
font-weight: bold;
}
.handle-user-agent-view > .info-panel > button.disabled {
background-color: #EBEBEB;
border-color: #EBEBEB;
color: #B2B0B3;
font-weight: normal;
}
/* Room wrapper layout */
.room-conversation-wrapper {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 467.5 76"><path fill="#5D5F64" d="M263.4 28.2c-.2.4-.5.8-.8 1.2-.3.3-.7.6-1.2.8-.5.2-.9.3-1.4.3-.5 0-1-.1-1.4-.3-.5-.2-.8-.4-1.2-.8-.3-.3-.6-.7-.8-1.2-.2-.4-.3-.9-.3-1.4 0-.5.1-1 .3-1.4.2-.4.4-.8.8-1.2.3-.3.7-.6 1.2-.8.4-.2.9-.3 1.4-.3.5 0 1 .1 1.4.3.4.2.8.4 1.2.8.3.3.6.7.8 1.2.2.5.3.9.3 1.4 0 .5-.1 1-.3 1.4zm-.7-2.6c-.2-.4-.4-.7-.6-1-.2-.3-.6-.5-.9-.7-.4-.2-.7-.2-1.1-.2-.4 0-.8.1-1.1.2-.4.2-.7.4-.9.7-.3.3-.5.6-.6 1-.2.4-.2.8-.2 1.2 0 .4.1.8.2 1.2.2.4.4.7.6 1 .3.3.6.5.9.7.3.2.7.2 1.1.2.4 0 .8-.1 1.1-.2.4-.2.7-.4.9-.7.3-.3.5-.6.6-1 .1-.4.2-.8.2-1.2.1-.4 0-.8-.2-1.2zm-2.2 2.6c-.1-.3-.3-.5-.4-.6-.1-.1-.2-.3-.3-.4l-.1-.1h-.2v1.8h-.7v-4.1h1.3c.2 0 .4 0 .6.1.2.1.3.1.4.2.1.1.2.2.2.4 0 .1.1.3.1.5 0 .3-.1.6-.3.8-.2.2-.4.3-.8.3l.1.1.2.2c.1.1.1.2.2.2.1.1.1.2.2.3l.6 1h-.8l-.3-.7zm0-2.8c-.1-.1-.3-.2-.6-.2h-.4v1.3h.8c.1 0 .2-.1.2-.1.1-.1.2-.3.2-.5s0-.3-.2-.5zM35.6 13.9h-24v19h19.5v9.4H11.6v32.1H0V4.3h37.1l-1.5 9.6zM41.7 8.2c0-4.1 3.2-7.5 7.4-7.5 3.9 0 7.3 3.2 7.3 7.5 0 4.1-3.3 7.4-7.5 7.4-4.1 0-7.2-3.4-7.2-7.4zm1.6 66.2V24l11.2-2v52.4H43.3zM89.2 32.9c-1.1-.4-1.9-.7-3.1-.7-4.7 0-8.6 3.4-9.6 7.6v34.6H65.3V38.3c0-6.5-.7-10.6-1.8-13.8l10.2-2.6c1.2 2.3 1.9 5.3 1.9 8.1 4-5.6 8.1-8.2 13.1-8.2 1.6 0 2.6.2 3.9.8l-3.4 10.3zM103.3 51.8v.8c0 7.1 2.6 14.6 12.7 14.6 4.8 0 8.9-1.7 12.7-5.1l4.4 6.8c-5.4 4.6-11.5 6.8-18.4 6.8-14.6 0-23.7-10.4-23.7-26.8 0-9 1.9-15 6.4-20 4.2-4.8 9.2-6.9 15.7-6.9 5.1 0 9.7 1.3 14.1 5.3 4.5 4 6.7 10.3 6.7 22.3v2.3h-30.6zm9.8-21.4c-6.3 0-9.7 5-9.7 13.3h18.9c0-8.4-3.6-13.3-9.2-13.3zM165.5 10c-2.5-1.2-4-1.8-6.2-1.8-3.8 0-6.3 2.6-6.3 7.2v7.8h13.4l-2.8 7.7h-10.4v43.5h-11V30.9h-4.8v-7.7h5s-.3-2.8-.3-7.6C142.1 5 148.5 0 157.6 0c4.4 0 8 .9 11.5 2.9l-3.6 7.1zM210.1 49c0 16.5-8.8 26.7-22.7 26.7-13.9 0-22.6-10.4-22.6-26.8S173.6 22 187.2 22c14.6 0 22.9 10.8 22.9 27zm-32.9-.8c0 14.9 3.7 19.2 10.4 19.2 6.6 0 10.2-5.4 10.2-18.2 0-14.5-4-18.8-10.5-18.8-7 0-10.1 5.3-10.1 17.8z"/><path fill="#5D5F64" d="M243.6 74.4c-1.8-2.9-10.1-17.3-11.1-19.1-1.9 3.8-9.2 16.3-11.1 19.1h-14.1l19-27.8-14.8-22 12-2.4c2.3 3.8 6.9 11.8 9.3 16.6 1.4-3.3 6.6-13.6 8.1-15.7h13l-15.1 23.3 18.7 27.9h-13.9z"/><g fill="#5D5F64"><path d="M280.6 4.5h8.2v29.3h29.5V4.5h8.4v70.1h-8.4V40.7h-29.5v33.9h-8.2V4.5zM371.5 64.3l3.1 5.1c-4.5 4.1-10.6 6.3-17.2 6.3-14.1 0-22.6-10.2-22.6-27.1 0-8.6 1.8-14.1 6.1-19.2 4.1-4.8 9.1-7.1 15.2-7.1 5.5 0 10.3 1.9 13.8 5.5 4.4 4.5 5.6 9.3 5.8 21.5v1.1H344v1.2c0 4.8.6 8.5 2.3 11.1 2.9 4.4 7.6 6.2 12.7 6.2 4.9.2 8.9-1.3 12.5-4.6zM344 44.5h23.3c-.1-5.5-.8-8.9-2.3-11.3-1.7-2.8-5.3-4.5-9.2-4.5-7.3-.1-11.4 5.2-11.8 15.8zM393.5 64.4c0 4 .6 5.1 2.9 5.1.3 0 1-.2 1-.2l1.6 5.2c-2 .9-3 1.1-5.1 1.1-2.5 0-4.5-.7-6-2.1-1.6-1.4-2.5-3.6-2.5-7.3v-54c0-6.6-1.2-10.4-1.2-10.4l8-1.5s1.3 4.3 1.3 12.1v52zM413.9 64.4c0 4 .6 5.1 2.9 5.1.3 0 1-.2 1-.2l1.6 5.2c-2 .9-3 1.1-5.1 1.1-2.5 0-4.5-.7-6-2.1-1.6-1.4-2.5-3.6-2.5-7.3v-54c0-6.6-1.2-10.4-1.2-10.4l8-1.5s1.3 4.3 1.3 12.1v52zM445.3 22.2c8.5 0 14 3.9 17.5 8.9 3.2 4.6 4.7 10.6 4.7 18.9 0 17-9.1 26-21.9 26-14 0-22-10.3-22-27.1 0-16.6 8.3-26.7 21.7-26.7zm-.1 6.5c-4.5 0-8.7 2.1-10.4 5.5-1.6 3.2-2.5 7.3-2.5 13.3 0 7.2 1.2 13.5 3.2 16.7 1.8 3.1 5.9 5.1 10.3 5.1 5.3 0 9.3-2.8 11-7.7 1.1-3.2 1.5-6 1.5-11 0-7.2-.7-12-2.4-15.3-2-4.5-6.5-6.6-10.7-6.6z"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1 @@
<svg width="568" height="148" viewBox="0 0 568 148" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>Fill 1 Copy</title><style>use:not(:target) { display: none; } use { fill: #383838; } use[id$=&quot;-white&quot;] { fill: #fff; }</style><defs><path id="mozilla-logo" d="M23.39 42.294c1.72 2.656 2.478 5.026 3.44 10.024 6.728-6.568 15.025-10.024 24.08-10.024 8.18 0 14.89 2.656 20.085 8.077 1.4 1.36 2.75 3.113 3.913 4.833 9.038-9.257 17.132-12.91 27.976-12.91 7.722 0 15.042 2.31 19.51 6.156 5.583 4.823 7.353 10.622 7.353 24.124v70.244h-25.092V77.606c0-11.82-1.4-14.107-8.13-14.107-4.822 0-11.6 3.29-17.166 8.305v71.013H54.872V78.533c0-12.326-1.787-15.22-8.97-15.22-4.773 0-11.384 2.472-16.95 7.514v71.99H3.694V73.913c0-14.266-.978-20.43-3.693-25.277l23.39-6.34zm152.244 28.92c-1.772 5.228-2.715 12.168-2.715 22.016 0 11.357 1.162 19.89 3.27 24.89 2.327 5.406 8.146 8.104 13.12 8.104 11.197 0 15.986-10.025 15.986-33.372 0-13.324-1.737-22.025-5.193-26.476-2.48-3.255-6.51-5.194-11.164-5.194-6.19 0-11.216 3.844-13.306 10.032zm46.49-14.265c7.893 9.257 11.418 20.057 11.418 36.07 0 16.982-3.895 28.582-12.412 38.212-7.486 8.483-17.352 13.71-32.563 13.71-26.863 0-44.384-20.076-44.384-51.13 0-31.08 17.706-51.738 44.384-51.738 14.08 0 25.077 4.84 33.558 14.875zm94.588-12.935V61.77l-43.64 63.096h45.344l-6.19 17.952h-72.713v-16.012l46.46-64.645H243.39V44.016h73.322zm40.694-2.328v101.13h-25.853V45.75l25.853-4.063zm3.035-24.874c0 8.89-7.066 15.987-16.002 15.987-8.652 0-15.767-7.098-15.767-15.987 0-8.87 7.353-16.03 16.204-16.03 8.67 0 15.567 7.16 15.567 16.03zm44.048 8.89v76.98c0 17.006.203 19.292 1.755 21.99.977 1.753 3.068 2.698 5.227 2.698.927 0 1.483 0 2.883-.354l4.42 15.42c-4.42 1.73-9.832 2.693-15.43 2.693-11.03 0-19.9-5.197-22.97-13.477-1.94-5.024-2.36-8.127-2.36-22.208V35.7c0-12.918-.337-20.81-1.3-29.722L403.157 0c.926 5.397 1.33 11.77 1.33 25.702zm53.625 0v76.98c0 17.006.22 19.292 1.806 21.99.91 1.753 3 2.698 5.16 2.698.977 0 1.567 0 2.933-.354l4.402 15.42c-4.402 1.73-9.816 2.693-15.432 2.693-11.01 0-19.897-5.197-22.983-13.477-1.973-5.024-2.294-8.127-2.294-22.208V35.7c0-12.918-.387-20.81-1.383-29.722L456.73 0c1.064 5.397 1.383 11.77 1.383 25.702zm74.082 73.894c-17.875 0-24.148 3.254-24.148 15.076 0 7.688 4.89 12.9 11.45 12.9 4.806 0 9.664-2.513 13.492-6.746l.42-21.23h-1.215zM498.687 47.69c9.613-4.064 17.878-5.785 26.983-5.785 16.628 0 27.993 6.157 31.888 17.167 1.282 4.05 1.872 7.135 1.755 17.757L558.687 110v1.752c0 10.607 1.755 14.672 9.313 20.255l-13.73 15.85c-6.038-2.53-11.416-6.98-13.93-11.982-1.905 1.948-4.047 3.837-6.003 5.202-4.79 3.476-11.787 5.414-19.883 5.414-22.005 0-33.96-11.215-33.96-30.86 0-23.196 16.053-34.015 47.485-34.015 1.888 0 3.676 0 5.818.22v-4.03c0-11.02-2.142-14.69-11.653-14.69-8.196 0-17.926 4.03-28.517 11.19l-11.012-18.533c5.245-3.29 9.108-5.203 16.07-8.087z"/></defs><use id="logo" xlink:href="#mozilla-logo"/><use id="logo-white" xlink:href="#mozilla-logo"/></svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -44,7 +44,7 @@ loop.store.StandaloneMetricsStore = (function() {
"connectedToSdkServers",
"connectionFailure",
"gotMediaPermission",
"joinRoom",
"metricsLogJoinRoom",
"joinedRoom",
"leaveRoom",
"mediaConnected",
@ -144,10 +144,20 @@ loop.store.StandaloneMetricsStore = (function() {
/**
* Handles the user clicking the join room button.
*
* @param {sharedActions.MetricsLogJoinRoom} actionData
*/
joinRoom: function() {
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Join the conversation");
metricsLogJoinRoom: function(actionData) {
var label;
if (actionData.userAgentHandledRoom) {
label = actionData.ownRoom ? "Joined own room in Firefox" :
"Joined in Firefox";
} else {
label = "Join the conversation";
}
this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button, label);
},
/**

View File

@ -14,6 +14,103 @@ loop.standaloneRoomViews = (function(mozL10n) {
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var ToSView = React.createClass({displayName: "ToSView",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
_getContent: function() {
// We use this technique of static markup as it means we get
// just one overall string for L10n to define the structure of
// the whole item.
return mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": React.renderToStaticMarkup(
React.createElement("a", {href: loop.config.legalWebsiteUrl, rel: "noreferrer", target: "_blank"},
mozL10n.get("terms_of_use_link_text")
)
),
"privacy_notice_url": React.renderToStaticMarkup(
React.createElement("a", {href: loop.config.privacyWebsiteUrl, rel: "noreferrer", target: "_blank"},
mozL10n.get("privacy_notice_link_text")
)
)
});
},
recordClick: function(event) {
// Check for valid href, as this is clicking on the paragraph -
// so the user may be clicking on the text rather than the link.
if (event.target && event.target.href) {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: event.target.href
}));
}
},
render: function() {
return (
React.createElement("p", {
className: "terms-service",
dangerouslySetInnerHTML: {__html: this._getContent()},
onClick: this.recordClick})
);
}
});
var StandaloneHandleUserAgentView = React.createClass({displayName: "StandaloneHandleUserAgentView",
mixins: [
loop.store.StoreMixin("activeRoomStore")
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
getInitialState: function() {
return this.getStoreState();
},
handleJoinButton: function() {
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
},
render: function() {
var buttonMessage = this.state.roomState === ROOM_STATES.JOINED ?
mozL10n.get("rooms_room_joined_own_conversation_label") :
mozL10n.get("rooms_room_join_label");
var buttonClasses = React.addons.classSet({
btn: true,
"btn-info": true,
disabled: this.state.roomState === ROOM_STATES.JOINED
});
// The extra scroller div here is for providing a scroll view for shorter
// screens, as the common.css specifies overflow:hidden for the body which
// we need in some places.
return (
React.createElement("div", {className: "handle-user-agent-view-scroller"},
React.createElement("div", {className: "handle-user-agent-view"},
React.createElement("div", {className: "info-panel"},
React.createElement("p", {className: "loop-logo-text", title: mozL10n.get("clientShortname2") }),
React.createElement("p", {className: "roomName"}, this.state.roomName),
React.createElement("p", {className: "loop-logo"}),
React.createElement("button", {
className: buttonClasses,
onClick: this.handleJoinButton},
buttonMessage
)
),
React.createElement(ToSView, {
dispatcher: this.props.dispatcher}),
React.createElement("p", {className: "mozilla-logo"})
)
)
);
}
});
/**
* Handles display of failures, determining the correct messages and
* displaying the retry button at appropriate times.
@ -306,41 +403,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
_getContent: function() {
// We use this technique of static markup as it means we get
// just one overall string for L10n to define the structure of
// the whole item.
return mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": React.renderToStaticMarkup(
React.createElement("a", {href: loop.config.legalWebsiteUrl, rel: "noreferrer", target: "_blank"},
mozL10n.get("terms_of_use_link_text")
)
),
"privacy_notice_url": React.renderToStaticMarkup(
React.createElement("a", {href: loop.config.privacyWebsiteUrl, rel: "noreferrer", target: "_blank"},
mozL10n.get("privacy_notice_link_text")
)
)
});
},
recordClick: function(event) {
// Check for valid href, as this is clicking on the paragraph -
// so the user may be clicking on the text rather than the link.
if (event.target && event.target.href) {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: event.target.href
}));
}
},
render: function() {
return (
React.createElement("footer", {className: "rooms-footer"},
React.createElement("div", {className: "footer-logo"}),
React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()},
onClick: this.recordClick})
React.createElement(ToSView, {
dispatcher: this.props.dispatcher})
)
);
}
@ -596,11 +664,50 @@ loop.standaloneRoomViews = (function(mozL10n) {
}
});
var StandaloneRoomControllerView = React.createClass({displayName: "StandaloneRoomControllerView",
mixins: [
loop.store.StoreMixin("activeRoomStore")
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
isFirefox: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return this.getStoreState();
},
render: function() {
// If we don't know yet, don't display anything.
if (this.state.userAgentHandlesRoom === undefined) {
return null;
}
if (this.state.userAgentHandlesRoom) {
return (
React.createElement(StandaloneHandleUserAgentView, {
dispatcher: this.props.dispatcher})
);
}
return (
React.createElement(StandaloneRoomView, {
activeRoomStore: this.getStore(),
dispatcher: this.props.dispatcher,
isFirefox: this.props.isFirefox})
);
}
});
return {
StandaloneHandleUserAgentView: StandaloneHandleUserAgentView,
StandaloneRoomControllerView: StandaloneRoomControllerView,
StandaloneRoomFailureView: StandaloneRoomFailureView,
StandaloneRoomFooter: StandaloneRoomFooter,
StandaloneRoomHeader: StandaloneRoomHeader,
StandaloneRoomInfoArea: StandaloneRoomInfoArea,
StandaloneRoomView: StandaloneRoomView
StandaloneRoomView: StandaloneRoomView,
ToSView: ToSView
};
})(navigator.mozL10n);

View File

@ -14,6 +14,103 @@ loop.standaloneRoomViews = (function(mozL10n) {
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var ToSView = React.createClass({
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
_getContent: function() {
// We use this technique of static markup as it means we get
// just one overall string for L10n to define the structure of
// the whole item.
return mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": React.renderToStaticMarkup(
<a href={loop.config.legalWebsiteUrl} rel="noreferrer" target="_blank">
{mozL10n.get("terms_of_use_link_text")}
</a>
),
"privacy_notice_url": React.renderToStaticMarkup(
<a href={loop.config.privacyWebsiteUrl} rel="noreferrer" target="_blank">
{mozL10n.get("privacy_notice_link_text")}
</a>
)
});
},
recordClick: function(event) {
// Check for valid href, as this is clicking on the paragraph -
// so the user may be clicking on the text rather than the link.
if (event.target && event.target.href) {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: event.target.href
}));
}
},
render: function() {
return (
<p
className="terms-service"
dangerouslySetInnerHTML={{__html: this._getContent()}}
onClick={this.recordClick}></p>
);
}
});
var StandaloneHandleUserAgentView = React.createClass({
mixins: [
loop.store.StoreMixin("activeRoomStore")
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
getInitialState: function() {
return this.getStoreState();
},
handleJoinButton: function() {
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
},
render: function() {
var buttonMessage = this.state.roomState === ROOM_STATES.JOINED ?
mozL10n.get("rooms_room_joined_own_conversation_label") :
mozL10n.get("rooms_room_join_label");
var buttonClasses = React.addons.classSet({
btn: true,
"btn-info": true,
disabled: this.state.roomState === ROOM_STATES.JOINED
});
// The extra scroller div here is for providing a scroll view for shorter
// screens, as the common.css specifies overflow:hidden for the body which
// we need in some places.
return (
<div className="handle-user-agent-view-scroller">
<div className="handle-user-agent-view">
<div className="info-panel">
<p className="loop-logo-text" title={ mozL10n.get("clientShortname2") }></p>
<p className="roomName">{ this.state.roomName }</p>
<p className="loop-logo" />
<button
className={buttonClasses}
onClick={this.handleJoinButton}>
{buttonMessage}
</button>
</div>
<ToSView
dispatcher={this.props.dispatcher} />
<p className="mozilla-logo" />
</div>
</div>
);
}
});
/**
* Handles display of failures, determining the correct messages and
* displaying the retry button at appropriate times.
@ -306,41 +403,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
_getContent: function() {
// We use this technique of static markup as it means we get
// just one overall string for L10n to define the structure of
// the whole item.
return mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": React.renderToStaticMarkup(
<a href={loop.config.legalWebsiteUrl} rel="noreferrer" target="_blank">
{mozL10n.get("terms_of_use_link_text")}
</a>
),
"privacy_notice_url": React.renderToStaticMarkup(
<a href={loop.config.privacyWebsiteUrl} rel="noreferrer" target="_blank">
{mozL10n.get("privacy_notice_link_text")}
</a>
)
});
},
recordClick: function(event) {
// Check for valid href, as this is clicking on the paragraph -
// so the user may be clicking on the text rather than the link.
if (event.target && event.target.href) {
this.props.dispatcher.dispatch(new sharedActions.RecordClick({
linkInfo: event.target.href
}));
}
},
render: function() {
return (
<footer className="rooms-footer">
<div className="footer-logo" />
<p dangerouslySetInnerHTML={{__html: this._getContent()}}
onClick={this.recordClick}></p>
<ToSView
dispatcher={this.props.dispatcher} />
</footer>
);
}
@ -596,11 +664,50 @@ loop.standaloneRoomViews = (function(mozL10n) {
}
});
var StandaloneRoomControllerView = React.createClass({
mixins: [
loop.store.StoreMixin("activeRoomStore")
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
isFirefox: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return this.getStoreState();
},
render: function() {
// If we don't know yet, don't display anything.
if (this.state.userAgentHandlesRoom === undefined) {
return null;
}
if (this.state.userAgentHandlesRoom) {
return (
<StandaloneHandleUserAgentView
dispatcher={this.props.dispatcher} />
);
}
return (
<StandaloneRoomView
activeRoomStore={this.getStore()}
dispatcher={this.props.dispatcher}
isFirefox={this.props.isFirefox} />
);
}
});
return {
StandaloneHandleUserAgentView: StandaloneHandleUserAgentView,
StandaloneRoomControllerView: StandaloneRoomControllerView,
StandaloneRoomFailureView: StandaloneRoomFailureView,
StandaloneRoomFooter: StandaloneRoomFooter,
StandaloneRoomHeader: StandaloneRoomHeader,
StandaloneRoomInfoArea: StandaloneRoomInfoArea,
StandaloneRoomView: StandaloneRoomView
StandaloneRoomView: StandaloneRoomView,
ToSView: ToSView
};
})(navigator.mozL10n);

View File

@ -153,7 +153,7 @@ loop.webapp = (function(_, OT, mozL10n) {
}
case "room": {
return (
React.createElement(loop.standaloneRoomViews.StandaloneRoomView, {
React.createElement(loop.standaloneRoomViews.StandaloneRoomControllerView, {
activeRoomStore: this.props.activeRoomStore,
dispatcher: this.props.dispatcher,
isFirefox: this.state.isFirefox})

View File

@ -153,7 +153,7 @@ loop.webapp = (function(_, OT, mozL10n) {
}
case "room": {
return (
<loop.standaloneRoomViews.StandaloneRoomView
<loop.standaloneRoomViews.StandaloneRoomControllerView
activeRoomStore={this.props.activeRoomStore}
dispatcher={this.props.dispatcher}
isFirefox={this.state.isFirefox} />

View File

@ -68,6 +68,7 @@ rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start
rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
rooms_room_joined_label=Someone has joined the conversation!
rooms_room_join_label=Join the conversation
rooms_room_joined_own_conversation_label=Enjoy your conversation
rooms_display_name_guest=Guest
rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid.
rooms_media_denied_message=We could not get access to your microphone or camera. Please reload the page to try again.

View File

@ -6,6 +6,7 @@ describe("loop.store.ActiveRoomStore", function () {
var expect = chai.expect;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var ROOM_STATES = loop.store.ROOM_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
@ -434,20 +435,20 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.assert.calledOnce(fakeMozLoop.rooms.get);
});
it("should dispatch an UpdateRoomInfo message with 'no data' failure if neither roomName nor context are supplied", function() {
it("should dispatch an UpdateRoomInfo message with failure if neither roomName nor context are supplied", function() {
fakeMozLoop.rooms.get.callsArgWith(1, null, {
roomUrl: "http://invalid"
});
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo({
roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA,
roomState: ROOM_STATES.READY,
roomUrl: "http://invalid"
}));
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo({
roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA,
roomState: ROOM_STATES.READY,
roomUrl: "http://invalid"
}));
});
});
describe("mozLoop.rooms.get returns roomName as a separate field (no context)", function() {
@ -459,13 +460,13 @@ describe("loop.store.ActiveRoomStore", function () {
fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails);
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomState: ROOM_STATES.READY
}, roomDetails)));
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomState: ROOM_STATES.READY
}, roomDetails)));
});
});
});
@ -491,25 +492,25 @@ describe("loop.store.ActiveRoomStore", function () {
it("should dispatch UpdateRoomInfo message with 'unsupported' failure if WebCrypto is unsupported", function() {
loop.crypto.isSupported.returns(false);
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED,
roomState: ROOM_STATES.READY
}, expectedDetails)));
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED,
roomState: ROOM_STATES.READY
}, expectedDetails)));
});
});
it("should dispatch UpdateRoomInfo message with 'no crypto key' failure if there is no crypto key", function() {
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.NO_CRYPTO_KEY,
roomState: ROOM_STATES.READY
}, expectedDetails)));
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.NO_CRYPTO_KEY,
roomState: ROOM_STATES.READY
}, expectedDetails)));
});
});
it("should dispatch UpdateRoomInfo message with 'decrypt failed' failure if decryption failed", function() {
@ -525,14 +526,14 @@ describe("loop.store.ActiveRoomStore", function () {
};
});
store.fetchServerData(fetchServerAction);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.DECRYPT_FAILED,
roomState: ROOM_STATES.READY
}, expectedDetails)));
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomInfoFailure: ROOM_INFO_FAILURES.DECRYPT_FAILED,
roomState: ROOM_STATES.READY
}, expectedDetails)));
});
});
it("should dispatch UpdateRoomInfo message with the context if decryption was successful", function() {
@ -558,18 +559,175 @@ describe("loop.store.ActiveRoomStore", function () {
};
});
store.fetchServerData(fetchServerAction);
return store.fetchServerData(fetchServerAction).then(function() {
var expectedData = _.extend({
roomContextUrls: roomContext.urls,
roomDescription: roomContext.description,
roomName: roomContext.roomName,
roomState: ROOM_STATES.READY
}, expectedDetails);
var expectedData = _.extend({
roomContextUrls: roomContext.urls,
roomDescription: roomContext.description,
roomName: roomContext.roomName,
roomState: ROOM_STATES.READY
}, expectedDetails);
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(expectedData));
});
});
});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(expectedData));
describe("User Agent Room Handling", function() {
var channelListener, roomDetails;
beforeEach(function() {
sandbox.stub(sharedUtils, "isFirefox").returns(true);
roomDetails = {
roomName: "fakeName",
roomUrl: "http://invalid"
};
fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails);
sandbox.stub(window, "addEventListener", function(eventName, listener) {
if (eventName === "WebChannelMessageToContent") {
channelListener = listener;
}
});
sandbox.stub(window, "removeEventListener", function(eventName, listener) {
if (eventName === "WebChannelMessageToContent" &&
listener === channelListener) {
channelListener = null;
}
});
});
it("should dispatch UserAgentHandlesRoom with false if the user agent is not Firefox", function() {
sharedUtils.isFirefox.returns(false);
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UserAgentHandlesRoom({
handlesRoom: false
}));
});
});
it("should dispatch with false after a timeout if there is no response from the channel", function() {
// When the dispatchEvent is called, we know the setup code has run, so
// advance the timer.
sandbox.stub(window, "dispatchEvent", function() {
sandbox.clock.tick(250);
});
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UserAgentHandlesRoom({
handlesRoom: false
}));
});
});
it("should not dispatch if a message is returned not for the link-clicker", function() {
// When the dispatchEvent is called, we know the setup code has run, so
// advance the timer.
sandbox.stub(window, "dispatchEvent", function() {
// We call the listener twice, but the first time with an invalid id.
// Hence we should only get the dispatch once.
channelListener({
detail: {
id: "invalid-id",
message: null
}
});
channelListener({
detail: {
id: "loop-link-clicker",
message: null
}
});
});
return store.fetchServerData(fetchServerAction).then(function() {
// Although this is only called once for the UserAgentHandlesRoom,
// it gets called twice due to the UpdateRoomInfo. Therefore,
// we test both results here.
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UserAgentHandlesRoom({
handlesRoom: false
}));
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(_.extend({
roomState: ROOM_STATES.READY
}, roomDetails)));
});
});
it("should dispatch with false if the user agent does not understand the message", function() {
// When the dispatchEvent is called, we know the setup code has run, so
// advance the timer.
sandbox.stub(window, "dispatchEvent", function() {
channelListener({
detail: {
id: "loop-link-clicker",
message: null
}
});
});
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UserAgentHandlesRoom({
handlesRoom: false
}));
});
});
it("should dispatch with false if the user agent cannot handle the message", function() {
// When the dispatchEvent is called, we know the setup code has run, so
// advance the timer.
sandbox.stub(window, "dispatchEvent", function() {
channelListener({
detail: {
id: "loop-link-clicker",
message: {
response: false
}
}
});
});
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UserAgentHandlesRoom({
handlesRoom: false
}));
});
});
it("should dispatch with true if the user agent can handle the message", function() {
// When the dispatchEvent is called, we know the setup code has run, so
// advance the timer.
sandbox.stub(window, "dispatchEvent", function() {
channelListener({
detail: {
id: "loop-link-clicker",
message: {
response: true
}
}
});
});
return store.fetchServerData(fetchServerAction).then(function() {
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UserAgentHandlesRoom({
handlesRoom: true
}));
});
});
});
});
@ -624,6 +782,20 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
describe("#userAgentHandlesRoom", function() {
it("should update the store state", function() {
store.setStoreState({
UserAgentHandlesRoom: false
});
store.userAgentHandlesRoom(new sharedActions.UserAgentHandlesRoom({
handlesRoom: true
}));
expect(store.getStoreState().userAgentHandlesRoom).eql(true);
});
});
describe("#updateSocialShareInfo", function() {
var fakeSocialShareInfo;
@ -659,32 +831,138 @@ describe("loop.store.ActiveRoomStore", function () {
expect(store.getStoreState().failureReason).eql(undefined);
});
it("should set the state to MEDIA_WAIT if media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, true);
describe("Standalone Handles Room", function() {
it("should dispatch a MetricsLogJoinRoom action", function() {
store.joinRoom();
store.joinRoom();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.MetricsLogJoinRoom({
userAgentHandledRoom: false
}));
});
expect(store.getStoreState().roomState).eql(ROOM_STATES.MEDIA_WAIT);
it("should set the state to MEDIA_WAIT if media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, true);
store.joinRoom();
expect(store.getStoreState().roomState).eql(ROOM_STATES.MEDIA_WAIT);
});
it("should not set the state to MEDIA_WAIT if no media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false);
store.joinRoom();
expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
});
it("should dispatch `ConnectionFailure` if no media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false);
store.joinRoom();
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.NO_MEDIA
}));
});
});
it("should not set the state to MEDIA_WAIT if no media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false);
describe("Firefox Handles Room", function() {
var channelListener;
store.joinRoom();
beforeEach(function() {
store.setStoreState({
userAgentHandlesRoom: true,
roomToken: "fakeToken",
standalone: true
});
expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
});
sandbox.stub(window, "addEventListener", function(eventName, listener) {
if (eventName === "WebChannelMessageToContent") {
channelListener = listener;
}
});
sandbox.stub(window, "removeEventListener", function(eventName, listener) {
if (eventName === "WebChannelMessageToContent" &&
listener === channelListener) {
channelListener = null;
}
});
it("should dispatch `ConnectionFailure` if no media devices are present", function() {
sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false);
sandbox.stub(console, "error");
});
store.joinRoom();
it("should dispatch a MetricsLogJoinRoom action", function() {
store.joinRoom();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.NO_MEDIA
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.MetricsLogJoinRoom({
userAgentHandledRoom: true,
ownRoom: true
}));
});
it("should dispatch an event to Firefox", function() {
sandbox.stub(window, "dispatchEvent");
store.joinRoom();
sinon.assert.calledOnce(window.dispatchEvent);
sinon.assert.calledWithExactly(window.dispatchEvent, new window.CustomEvent(
"WebChannelMessageToChrome", {
detail: {
id: "loop-link-clicker",
message: {
command: "openRoom",
roomToken: "fakeToken"
}
}
}));
});
it("should log an error if Firefox doesn't handle the room", function() {
// Start the join.
store.joinRoom();
// Pretend Firefox calls back.
channelListener({
detail: {
id: "loop-link-clicker",
message: null
}
});
sinon.assert.calledOnce(console.error);
});
it("should dispatch a JoinedRoom action if the room was successfully opened", function() {
// Start the join.
store.joinRoom();
// Pretend Firefox calls back.
channelListener({
detail: {
id: "loop-link-clicker",
message: {
response: true
}
}
});
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.JoinedRoom({
apiKey: "",
sessionToken: "",
sessionId: "",
expires: 0
}));
});
});
});
@ -762,6 +1040,17 @@ describe("loop.store.ActiveRoomStore", function () {
expect(store._storeState.roomState).eql(ROOM_STATES.JOINED);
});
it("should set the state to `JOINED` when Firefox handles the room", function() {
store.setStoreState({
userAgentHandlesRoom: true,
standalone: true
});
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
expect(store._storeState.roomState).eql(ROOM_STATES.JOINED);
});
it("should store the session and api values", function() {
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
@ -771,6 +1060,20 @@ describe("loop.store.ActiveRoomStore", function () {
expect(state.sessionId).eql(fakeJoinedData.sessionId);
});
it("should not store the session and api values when Firefox handles the room", function() {
store.setStoreState({
userAgentHandlesRoom: true,
standalone: true
});
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
var state = store.getStoreState();
expect(state.apiKey).eql(undefined);
expect(state.sessionToken).eql(undefined);
expect(state.sessionId).eql(undefined);
});
it("should start the session connection with the sdk", function() {
var actionData = new sharedActions.JoinedRoom(fakeJoinedData);

View File

@ -86,15 +86,6 @@ describe("loop.store.StandaloneMetricsStore", function() {
"Media granted");
});
it("should log an event on JoinRoom", function() {
store.joinRoom();
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Join the conversation");
});
it("should log an event on JoinedRoom", function() {
store.joinedRoom();
@ -150,6 +141,43 @@ describe("loop.store.StandaloneMetricsStore", function() {
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Retry failed room");
});
describe("MetricsLogJoinRoom", function() {
it("should log a 'Join the conversation' event if not joined by Firefox", function() {
store.metricsLogJoinRoom({
userAgentHandledRoom: false
});
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Join the conversation");
});
it("should log a 'Joined own room in Firefox' event if joining the own room in Firefox", function() {
store.metricsLogJoinRoom({
userAgentHandledRoom: true,
ownRoom: true
});
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Joined own room in Firefox");
});
it("should log a 'Joined in Firefox' event if joining a non-own room in Firefox", function() {
store.metricsLogJoinRoom({
userAgentHandledRoom: true,
ownRoom: false
});
sinon.assert.calledOnce(window.ga);
sinon.assert.calledWithExactly(window.ga,
"send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button,
"Joined in Firefox");
});
});
});
describe("Store Change Handlers", function() {

View File

@ -49,6 +49,8 @@ describe("loop.standaloneRoomViews", function() {
switch(key) {
case "standalone_title_with_room_name":
return args.roomName + " — " + args.clientShortname;
case "legal_text_and_links":
return args.terms_of_use_url + " " + args.privacy_notice_url;
default:
return key;
}
@ -66,6 +68,123 @@ describe("loop.standaloneRoomViews", function() {
view = null;
});
describe("TosView", function() {
var origConfig, node;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(
loop.standaloneRoomViews.ToSView, {
dispatcher: dispatcher
}));
}
beforeEach(function() {
origConfig = loop.config;
loop.config = {
legalWebsiteUrl: "http://fakelegal/",
privacyWebsiteUrl: "http://fakeprivacy/"
};
view = mountTestComponent();
node = view.getDOMNode();
});
afterEach(function() {
loop.config = origConfig;
});
it("should dispatch a link click action when the ToS link is clicked", function() {
// [0] is the first link, the legal one.
var link = node.querySelectorAll("a")[0];
TestUtils.Simulate.click(node, { target: link });
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: loop.config.legalWebsiteUrl
}));
});
it("should dispatch a link click action when the Privacy link is clicked", function() {
// [0] is the first link, the legal one.
var link = node.querySelectorAll("a")[1];
TestUtils.Simulate.click(node, { target: link });
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RecordClick({
linkInfo: loop.config.privacyWebsiteUrl
}));
});
it("should not dispatch an action when the text is clicked", function() {
TestUtils.Simulate.click(node, { target: node });
sinon.assert.notCalled(dispatcher.dispatch);
});
});
describe("StandaloneHandleUserAgentView", function() {
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(
loop.standaloneRoomViews.StandaloneHandleUserAgentView, {
dispatcher: dispatcher
}));
}
it("should display a join room button if the state is not ROOM_JOINED", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.READY
});
view = mountTestComponent();
var button = view.getDOMNode().querySelector(".info-panel > button");
expect(button.textContent).eql("rooms_room_join_label");
});
it("should dispatch a JoinRoom action when the join room button is clicked", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.READY
});
view = mountTestComponent();
var button = view.getDOMNode().querySelector(".info-panel > button");
TestUtils.Simulate.click(button);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.JoinRoom());
});
it("should display a enjoy your conversation button if the state is ROOM_JOINED", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.JOINED
});
view = mountTestComponent();
var button = view.getDOMNode().querySelector(".info-panel > button");
expect(button.textContent).eql("rooms_room_joined_own_conversation_label");
});
it("should disable the enjoy your conversation button if the state is ROOM_JOINED", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.JOINED
});
view = mountTestComponent();
var button = view.getDOMNode().querySelector(".info-panel > button");
expect(button.classList.contains("disabled")).eql(true);
});
});
describe("StandaloneRoomHeader", function() {
function mountTestComponent() {
return TestUtils.renderIntoDocument(
@ -804,4 +923,47 @@ describe("loop.standaloneRoomViews", function() {
});
});
});
describe("StandaloneRoomControllerView", function() {
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomControllerView, {
dispatcher: dispatcher,
isFirefox: true
}));
}
it("should not display anything if it is not known if Firefox can handle the room", function() {
activeRoomStore.setStoreState({
userAgentHandlesRoom: undefined
});
view = mountTestComponent();
expect(view.getDOMNode()).eql(null);
});
it("should render StandaloneHandleUserAgentView if Firefox can handle the room", function() {
activeRoomStore.setStoreState({
userAgentHandlesRoom: true
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.standaloneRoomViews.StandaloneHandleUserAgentView);
});
it("should render StandaloneRoomView if Firefox cannot handle the room", function() {
activeRoomStore.setStoreState({
userAgentHandlesRoom: false
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.standaloneRoomViews.StandaloneRoomView);
});
});
});

View File

@ -119,14 +119,14 @@ describe("loop.webapp", function() {
loop.webapp.UnsupportedBrowserView);
});
it("should display the StandaloneRoomView for `room` window type",
it("should display the StandaloneRoomControllerView for `room` window type",
function() {
standaloneAppStore.setStoreState({windowType: "room", isFirefox: true});
var webappRootView = mountTestComponent();
TestUtils.findRenderedComponentWithType(webappRootView,
loop.standaloneRoomViews.StandaloneRoomView);
loop.standaloneRoomViews.StandaloneRoomControllerView);
});
it("should display the HomeView for `home` window type", function() {

View File

@ -36,6 +36,7 @@
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView;
var StandaloneHandleUserAgentView = loop.standaloneRoomViews.StandaloneHandleUserAgentView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
@ -1515,6 +1516,21 @@
)
),
React.createElement(Section, {name: "StandaloneHandleUserAgentView"},
React.createElement(FramedExample, {
cssClass: "standalone",
dashed: true,
height: 483,
summary: "Standalone Room Handle Join in Firefox",
width: 644},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneHandleUserAgentView, {
activeRoomStore: readyRoomStore,
dispatcher: dispatcher})
)
)
),
React.createElement(Section, {name: "StandaloneRoomView"},
React.createElement(FramedExample, {cssClass: "standalone",
dashed: true,

View File

@ -36,6 +36,7 @@
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView;
var StandaloneHandleUserAgentView = loop.standaloneRoomViews.StandaloneHandleUserAgentView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
@ -1515,6 +1516,21 @@
</FramedExample>
</Section>
<Section name="StandaloneHandleUserAgentView">
<FramedExample
cssClass="standalone"
dashed={true}
height={483}
summary="Standalone Room Handle Join in Firefox"
width={644} >
<div className="standalone">
<StandaloneHandleUserAgentView
activeRoomStore={readyRoomStore}
dispatcher={dispatcher} />
</div>
</FramedExample>
</Section>
<Section name="StandaloneRoomView">
<FramedExample cssClass="standalone"
dashed={true}

View File

@ -352,7 +352,7 @@ nsGNOMEShellService::GetShouldSkipCheckDefaultBrowser(bool* aResult)
if (NS_FAILED(rv)) {
return rv;
}
if (defaultBrowserCheckCount < 3) {
if (defaultBrowserCheckCount < 4) {
*aResult = false;
return prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT,
defaultBrowserCheckCount + 1);

View File

@ -130,7 +130,7 @@ nsMacShellService::GetShouldSkipCheckDefaultBrowser(bool* aResult)
if (NS_FAILED(rv)) {
return rv;
}
if (defaultBrowserCheckCount < 3) {
if (defaultBrowserCheckCount < 4) {
*aResult = false;
return prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT,
defaultBrowserCheckCount + 1);

View File

@ -1011,7 +1011,7 @@ nsWindowsShellService::GetShouldSkipCheckDefaultBrowser(bool* aResult)
if (NS_FAILED(rv)) {
return rv;
}
if (defaultBrowserCheckCount < 3) {
if (defaultBrowserCheckCount < 4) {
*aResult = false;
return prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT,
defaultBrowserCheckCount + 1);

View File

@ -285,4 +285,22 @@
:root[devtoolstheme="dark"] #titlebar-close {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
}
/* ... and normal ones for the light theme on Windows 10 */
:root[devtoolstheme="light"] #titlebar-min {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize);
}
:root[devtoolstheme="light"] #titlebar-max {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize);
}
#main-window[devtoolstheme="light"][sizemode="maximized"] #titlebar-max {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore);
}
:root[devtoolstheme="light"] #titlebar-close {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close);
}
:root[devtoolstheme="light"] #titlebar-close:hover {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
}
}

View File

@ -350,6 +350,9 @@ pref("browser.link.open_newwindow", 3);
// 0=force all new windows to tabs, 1=don't force, 2=only force those with no features set
pref("browser.link.open_newwindow.restriction", 0);
// Image blocking policy
pref("browser.image_blocking.enabled", false);
// controls which bits of private data to clear. by default we clear them all.
pref("privacy.item.cache", true);
pref("privacy.item.cookies", true);

View File

@ -669,7 +669,7 @@ public class BrowserApp extends GeckoApp
return true;
case KeyEvent.KEYCODE_R:
tab.doReload();
tab.doReload(false);
return true;
case KeyEvent.KEYCODE_PERIOD:
@ -3410,7 +3410,7 @@ public class BrowserApp extends GeckoApp
if (itemId == R.id.reload) {
tab = Tabs.getInstance().getSelectedTab();
if (tab != null)
tab.doReload();
tab.doReload(false);
return true;
}
@ -3527,6 +3527,21 @@ public class BrowserApp extends GeckoApp
return super.onOptionsItemSelected(item);
}
@Override
public boolean onMenuItemLongClick(MenuItem item) {
if (item.getItemId() == R.id.reload) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
tab.doReload(true);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "reload_force");
}
return true;
}
return super.onMenuItemLongClick(item);
}
public void showGuestModeDialog(final GuestModeDialog type) {
final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
@Override

View File

@ -465,7 +465,7 @@ public class GeckoView extends LayerView
public void reload() {
Tab tab = Tabs.getInstance().getTab(mId);
if (tab != null) {
tab.doReload();
tab.doReload(true);
}
}

View File

@ -614,8 +614,8 @@ public class Tab {
return mEnteringReaderMode;
}
public void doReload() {
GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", "");
public void doReload(boolean bypassCache) {
GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", "{\"bypassCache\":" + String.valueOf(bypassCache) + "}");
GeckoAppShell.sendEventToGecko(e);
}

View File

@ -219,6 +219,17 @@ public class ImmutableViewportMetrics {
zoomFactor, isRTL);
}
public ImmutableViewportMetrics setPageRectFrom(ImmutableViewportMetrics aMetrics) {
if (aMetrics.cssPageRectLeft == cssPageRectLeft &&
aMetrics.cssPageRectTop == cssPageRectTop &&
aMetrics.cssPageRectRight == cssPageRectRight &&
aMetrics.cssPageRectBottom == cssPageRectBottom) {
return this;
}
RectF css = aMetrics.getCssPageRect();
return setPageRect(RectUtils.scale(css, zoomFactor), css);
}
public ImmutableViewportMetrics setIsRTL(boolean aIsRTL) {
if (isRTL == aIsRTL) {
return this;

View File

@ -950,14 +950,14 @@ class JavaPanZoomController
synchronized (mTarget.getLock()) {
float t = easeOut((float)mBounceDuration / BOUNCE_ANIMATION_DURATION);
ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t);
mTarget.setViewportMetrics(newMetrics);
mTarget.setViewportMetrics(newMetrics.setPageRectFrom(getMetrics()));
}
}
/* Concludes a bounce animation and snaps the viewport into place. */
private void finishBounce() {
synchronized (mTarget.getLock()) {
mTarget.setViewportMetrics(mBounceEndMetrics);
mTarget.setViewportMetrics(mBounceEndMetrics.setPageRectFrom(getMetrics()));
}
}
}

View File

@ -68,9 +68,12 @@ class SearchEngineRow extends AnimatedHeightLayout {
// Selected suggestion view
private int mSelectedView;
// Maximums for suggestions based on form factor
private static final int TABLET_MAX = 4;
private static final int PHONE_MAX = 2;
// Maximums for suggestions
private int mMaxSavedSuggestions;
private int mMaxSearchSuggestions;
// Remove this default limit value in Bug 1201325
private static final int SUGGESTIONS_MAX = 4;
public SearchEngineRow(Context context) {
this(context, null);
@ -132,6 +135,10 @@ class SearchEngineRow extends AnimatedHeightLayout {
mUserEnteredView.setOnClickListener(mClickListener);
mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text);
// Suggestion limits
mMaxSavedSuggestions = getResources().getInteger(R.integer.max_saved_suggestions);
mMaxSearchSuggestions = getResources().getInteger(R.integer.max_search_suggestions);
}
private void setDescriptionOnSuggestion(View v, String suggestion) {
@ -262,14 +269,14 @@ class SearchEngineRow extends AnimatedHeightLayout {
if (!AppConstants.NIGHTLY_BUILD) {
return null;
}
final ContentResolver cr = getContext().getContentResolver();
String[] columns = new String[] { SearchHistory.QUERY };
String actualQuery = SearchHistory.QUERY + " LIKE ?";
String[] queryArgs = new String[] { '%' + searchTerm + '%' };
final int limit = HardwareUtils.isTablet() ? TABLET_MAX : PHONE_MAX;
String sortOrderAndLimit = SearchHistory.DATE +" DESC LIMIT "+limit;
String sortOrderAndLimit = SearchHistory.DATE +" DESC LIMIT " + mMaxSavedSuggestions;
return cr.query(SearchHistory.CONTENT_URI, columns, actualQuery, queryArgs, sortOrderAndLimit);
}
@ -278,25 +285,26 @@ class SearchEngineRow extends AnimatedHeightLayout {
*
* @param animate whether or not to animate suggestions for visual polish
* @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls
* @param savedCount how many saved searches this searchTerm has
* @param savedSuggestionCount how many saved searches this searchTerm has
* @return the global count of how many suggestions have been bound/shown in the search engine row
*/
private int updateFromSearchEngine(boolean animate, int recycledSuggestionCount, int savedCount) {
private int updateFromSearchEngine(boolean animate, int recycledSuggestionCount, int savedSuggestionCount) {
// Remove this default limit value in Bug 1201325
int limit = TABLET_MAX;
int maxSuggestions = SUGGESTIONS_MAX;
if (AppConstants.NIGHTLY_BUILD) {
limit = HardwareUtils.isTablet() ? TABLET_MAX : PHONE_MAX;
maxSuggestions = mMaxSearchSuggestions;
// If there are less than max saved searches on phones, fill the space with more search engine suggestions
if (!HardwareUtils.isTablet() && savedCount < PHONE_MAX) {
limit += PHONE_MAX - savedCount;
if (!HardwareUtils.isTablet() && savedSuggestionCount < mMaxSavedSuggestions) {
maxSuggestions += mMaxSavedSuggestions - savedSuggestionCount;
}
}
int suggestionCounter = 0;
for (String suggestion : mSearchEngine.getSuggestions()) {
if (suggestionCounter == limit) {
if (suggestionCounter == maxSuggestions) {
break;
}
// Since the search engine suggestions are listed first, we can use suggestionCounter to get their relative positions for telemetry
String telemetryTag = "engine." + suggestionCounter;
bindSuggestionView(suggestion, animate, recycledSuggestionCount, suggestionCounter, false, telemetryTag);

View File

@ -217,6 +217,9 @@
<!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party">
<!ENTITY pref_cookies_disabled "Disabled">
<!ENTITY pref_tap_to_load_images_title "Tap-to-load images">
<!ENTITY pref_tap_to_load_images_summary "Load images only when you tap on them">
<!ENTITY pref_tracking_protection_title "Tracking protection">
<!ENTITY pref_tracking_protection_summary3 "Enabled in Private Browsing">
<!ENTITY pref_donottrack_title "Do not track">

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.menu;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
@ -256,8 +257,12 @@ public class GeckoMenu extends ListView
});
((MenuItemActionBar) actionView).setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return handleMenuItemLongClick(menuItem);
public boolean onLongClick(View view) {
if (handleMenuItemLongClick(menuItem)) {
GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec));
return true;
}
return false;
}
});
} else if (actionView instanceof MenuItemActionView) {
@ -270,7 +275,11 @@ public class GeckoMenu extends ListView
((MenuItemActionView) actionView).setMenuItemLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
return handleMenuItemLongClick(menuItem);
if (handleMenuItemLongClick(menuItem)) {
GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec));
return true;
}
return false;
}
});
}
@ -644,12 +653,15 @@ public class GeckoMenu extends ListView
}
boolean handleMenuItemLongClick(GeckoMenuItem item) {
if(!item.isEnabled()) {
if (!item.isEnabled()) {
return false;
}
if(mCallback != null) {
return mCallback.onMenuItemLongClick(item);
if (mCallback != null) {
if (mCallback.onMenuItemLongClick(item)) {
close();
return true;
}
}
return false;
}

View File

@ -133,6 +133,7 @@ OnSharedPreferenceChangeListener
private static final String PREFS_DEVTOOLS = NON_PREF_PREFIX + "devtools.enabled";
private static final String PREFS_DISPLAY = NON_PREF_PREFIX + "display.enabled";
private static final String PREFS_CUSTOMIZE_HOME = NON_PREF_PREFIX + "customize_home";
private static final String PREFS_CUSTOMIZE_IMAGE_BLOCKING = "browser.image_blocking.enabled";
private static final String PREFS_TRACKING_PROTECTION_PRIVATE_BROWSING = "privacy.trackingprotection.pbmode.enabled";
private static final String PREFS_TRACKING_PROTECTION_LEARN_MORE = NON_PREF_PREFIX + "trackingprotection.learn_more";
private static final String PREFS_CATEGORY_PRIVATE_DATA = NON_PREF_PREFIX + "category_private_data";
@ -886,6 +887,13 @@ OnSharedPreferenceChangeListener
i--;
continue;
}
} else if (PREFS_CUSTOMIZE_IMAGE_BLOCKING.equals(key)) {
// Only enable the ZoomedView / magnifying pref on Nightly.
if (!AppConstants.NIGHTLY_BUILD) {
preferences.removePreference(pref);
i--;
continue;
}
} else if (PREFS_HOMEPAGE.equals(key)) {
String setUrl = GeckoSharedPrefs.forProfile(getBaseContext()).getString(PREFS_HOMEPAGE, AboutPages.HOME);
setHomePageSummary(pref, setUrl);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 998 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -68,7 +68,6 @@
<org.mozilla.gecko.toolbar.ToolbarEditLayout android:id="@+id/edit_layout"
style="@style/UrlBar.Button"
android:paddingLeft="10dp"
android:paddingRight="12dp"
android:visibility="gone"
android:orientation="horizontal"

View File

@ -14,7 +14,9 @@
android:layout_gravity="center_vertical"/>
<!-- The site security icon is misaligned with the page title so
we add a bottom margin to align their bottoms. -->
we add a bottom margin to align their bottoms.
Site security icon must have exact position and size as search icon in
edit layout -->
<ImageButton android:id="@+id/site_security"
style="@style/UrlBar.ImageButton"
android:layout_width="@dimen/browser_toolbar_site_security_width"

View File

@ -6,11 +6,21 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto">
<!-- Overall, we want 12dp of padding from mic to the right edge of the toolbar.
However, setting a value of 12dp (using the padding from the parent container)
does not match drawablePadding=12dp. Part of this is the url_bar_entry drawable
overlaps the EditText, but I can't figure out the rest. Thus eyeballing for
paddingRight. -->
<!-- Search icon must have exact position and size as site security in
display layout -->
<ImageView android:id="@+id/search_icon"
android:layout_width="@dimen/browser_toolbar_site_security_width"
android:layout_height="@dimen/browser_toolbar_site_security_height"
android:layout_marginBottom="@dimen/browser_toolbar_site_security_margin_bottom"
android:layout_marginRight="@dimen/browser_toolbar_site_security_margin_right"
android:paddingBottom="@dimen/browser_toolbar_site_security_padding_vertical"
android:paddingLeft="@dimen/browser_toolbar_site_security_padding_horizontal"
android:paddingRight="@dimen/browser_toolbar_site_security_padding_horizontal"
android:paddingTop="@dimen/browser_toolbar_site_security_padding_vertical"
android:scaleType="fitCenter"
android:src="@drawable/search_icon_inactive"
android:visibility="gone"/>
<org.mozilla.gecko.toolbar.ToolbarEditText
android:id="@+id/url_edit_text"
style="@style/UrlBar.Title"
@ -21,7 +31,6 @@
android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen"
android:selectAllOnFocus="true"
android:contentDescription="@string/url_bar_default_text"
android:drawablePadding="12dp"
android:paddingRight="8dp"
gecko:autoUpdateTheme="false"/>

View File

@ -6,5 +6,7 @@
<resources>
<integer name="number_of_inline_share_devices">3</integer>
<integer name="max_search_suggestions">4</integer>
<integer name="max_saved_suggestions">4</integer>
</resources>

View File

@ -48,14 +48,11 @@
<item name="drawableTintList">@color/action_bar_menu_item_colors</item>
<item name="android:scaleType">center</item>
<!-- layout_width/height doesn't work here, likely because it's
an ImageButton, so we use padding instead.
<!-- layout_width/height doesn't work here, likely because it's only
added programmatically, so we use padding for the width instead.
layout_height is set to MATCH_PARENT programmatically in
org.mozilla.gecko.toolbar.BrowserToolbarTabletBase.addActionItem(View) -->
Notes:
* The bookmarks star is larger than the reload button
* The reload button contains whitespace at the top of the image to lower it -->
<item name="android:paddingTop">19dp</item>
<item name="android:paddingBottom">21dp</item>
<item name="android:paddingLeft">@dimen/tablet_browser_toolbar_menu_item_padding_horizontal</item>
<item name="android:paddingRight">@dimen/tablet_browser_toolbar_menu_item_padding_horizontal</item>
</style>

View File

@ -10,5 +10,7 @@
<integer name="max_icon_grid_columns">4</integer>
<integer name="panel_icon_grid_view_columns">3</integer>
<integer name="number_of_inline_share_devices">2</integer>
<integer name="max_search_suggestions">2</integer>
<integer name="max_saved_suggestions">2</integer>
</resources>

View File

@ -31,6 +31,11 @@
android:entryValues="@array/pref_restore_values"
android:persistent="true" />
<CheckBoxPreference android:key="browser.image_blocking.enabled"
android:title="@string/pref_tap_to_load_images_title"
android:summary="@string/pref_tap_to_load_images_summary"
android:defaultValue="false"/>
<CheckBoxPreference android:key="android.not_a_preference.tab_queue"
android:title="@string/pref_tab_queue_title"
android:summary="@string/pref_tab_queue_summary"

View File

@ -38,6 +38,11 @@
android:entryValues="@array/pref_restore_values"
android:persistent="true" />
<CheckBoxPreference android:key="browser.image_blocking.enabled"
android:title="@string/pref_tap_to_load_images_title"
android:summary="@string/pref_tap_to_load_images_summary"
android:defaultValue="false"/>
<CheckBoxPreference android:key="android.not_a_preference.tab_queue"
android:title="@string/pref_tab_queue_title"
android:summary="@string/pref_tab_queue_summary"

View File

@ -39,6 +39,11 @@
android:entryValues="@array/pref_restore_values"
android:persistent="true" />
<CheckBoxPreference android:key="browser.image_blocking.enabled"
android:title="@string/pref_tap_to_load_images_title"
android:summary="@string/pref_tap_to_load_images_summary"
android:defaultValue="false"/>
<CheckBoxPreference android:key="android.not_a_preference.tab_queue"
android:title="@string/pref_tab_queue_title"
android:summary="@string/pref_tab_queue_summary"

View File

@ -208,6 +208,9 @@
<string name="pref_cookies_not_accept_foreign">&pref_cookies_not_accept_foreign;</string>
<string name="pref_cookies_disabled">&pref_cookies_disabled;</string>
<string name="pref_tap_to_load_images_title">&pref_tap_to_load_images_title;</string>
<string name="pref_tap_to_load_images_summary">&pref_tap_to_load_images_summary;</string>
<string name="pref_tracking_protection_title">&pref_tracking_protection_title;</string>
<string name="pref_tracking_protection_summary">&pref_tracking_protection_summary3;</string>
<string name="pref_donottrack_title">&pref_donottrack_title;</string>

View File

@ -20,6 +20,7 @@ import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.LinearLayout;
@ -102,7 +103,7 @@ abstract class BrowserToolbarTabletBase extends BrowserToolbar {
@Override
public boolean addActionItem(final View actionItem) {
actionItemBar.addView(actionItem);
actionItemBar.addView(actionItem, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
return true;
}

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko.toolbar;
import android.app.Activity;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.speech.RecognizerIntent;
import android.widget.Button;
import android.widget.ImageButton;
@ -24,6 +25,8 @@ import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.util.ActivityResultHandler;
import org.mozilla.gecko.util.DrawableUtil;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.InputOptionsUtils;
import org.mozilla.gecko.widget.themed.ThemedLinearLayout;
@ -33,6 +36,7 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import java.util.List;
@ -44,6 +48,12 @@ import java.util.List;
*/
public class ToolbarEditLayout extends ThemedLinearLayout {
public interface OnSearchStateChangeListener {
public void onSearchStateChange(boolean isActive);
}
private final ImageView mSearchIcon;
private final ToolbarEditText mEditText;
private final ImageButton mVoiceInput;
@ -59,6 +69,8 @@ public class ToolbarEditLayout extends ThemedLinearLayout {
setOrientation(HORIZONTAL);
LayoutInflater.from(context).inflate(R.layout.toolbar_edit_layout, this);
mSearchIcon = (ImageView) findViewById(R.id.search_icon);
mEditText = (ToolbarEditText) findViewById(R.id.url_edit_text);
mVoiceInput = (ImageButton) findViewById(R.id.mic);
@ -67,6 +79,10 @@ public class ToolbarEditLayout extends ThemedLinearLayout {
@Override
public void onAttachedToWindow() {
if (HardwareUtils.isTablet()) {
mSearchIcon.setVisibility(View.VISIBLE);
}
mEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
@ -91,6 +107,13 @@ public class ToolbarEditLayout extends ThemedLinearLayout {
}
});
mEditText.setOnSearchStateChangeListener(new OnSearchStateChangeListener() {
@Override
public void onSearchStateChange(boolean isActive) {
updateSearchIcon(isActive);
}
});
mVoiceInput.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
@ -104,6 +127,37 @@ public class ToolbarEditLayout extends ThemedLinearLayout {
launchQRCodeReader();
}
});
// Set an inactive search icon on tablet devices when in editing mode
updateSearchIcon(false);
}
/**
* Update the search icon at the left of the edittext based
* on its state.
*
* @param isActive The state of the edittext. Active is when the initialized
* text has changed and is not empty.
*/
void updateSearchIcon(boolean isActive) {
if (!HardwareUtils.isTablet()) {
return;
}
// When on tablet show a magnifying glass in editing mode
final int searchDrawableId = R.drawable.search_icon_active;
final Drawable searchDrawable;
if (!isActive) {
searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.placeholder_grey);
} else {
if (isPrivateMode()) {
searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey);
} else {
searchDrawable = getResources().getDrawable(searchDrawableId);
}
}
mSearchIcon.setImageDrawable(searchDrawable);
}
@Override

View File

@ -13,13 +13,11 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.util.DrawableUtil;
import org.mozilla.gecko.toolbar.ToolbarEditLayout.OnSearchStateChangeListener;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.HardwareUtils;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.Rect;
import android.text.Editable;
import android.text.NoCopySpan;
@ -56,6 +54,7 @@ public class ToolbarEditText extends CustomEditText
private OnCommitListener mCommitListener;
private OnDismissListener mDismissListener;
private OnFilterListener mFilterListener;
private OnSearchStateChangeListener mSearchStateChangeListener;
private ToolbarPrefs mPrefs;
@ -87,14 +86,16 @@ public class ToolbarEditText extends CustomEditText
mFilterListener = listener;
}
void setOnSearchStateChangeListener(OnSearchStateChangeListener listener) {
mSearchStateChangeListener = listener;
}
@Override
public void onAttachedToWindow() {
setOnKeyListener(new KeyListener());
setOnKeyPreImeListener(new KeyPreImeListener());
setOnSelectionChangedListener(new SelectionChangeListener());
addTextChangedListener(new TextChangeListener());
// Set an inactive search icon on tablet devices when in editing mode
updateSearchIcon(false);
}
@Override
@ -104,7 +105,9 @@ public class ToolbarEditText extends CustomEditText
// Make search icon inactive when edit toolbar search term isn't a user entered
// search term
final boolean isActive = !TextUtils.isEmpty(getText());
updateSearchIcon(isActive);
if (mSearchStateChangeListener != null) {
mSearchStateChangeListener.onSearchStateChange(isActive);
}
if (gainFocus) {
resetAutocompleteState();
@ -160,33 +163,6 @@ public class ToolbarEditText extends CustomEditText
mPrefs = prefs;
}
/**
* Update the search icon at the left of the edittext based
* on its state.
*
* @param isActive The state of the edittext. Active is when the initialized
* text has changed and is not empty.
*/
void updateSearchIcon(boolean isActive) {
if (!HardwareUtils.isTablet()) {
return;
}
// When on tablet show a magnifying glass in editing mode
final int searchDrawableId = R.drawable.search_icon_active;
final Drawable searchDrawable;
if (!isActive) {
searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.placeholder_grey);
} else {
if (isPrivateMode()) {
searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey);
} else {
searchDrawable = getResources().getDrawable(searchDrawableId);
}
}
setCompoundDrawablesWithIntrinsicBounds(searchDrawable, null, null, null);
}
/**
* Mark the start of autocomplete changes so our text change
* listener does not react to changes in autocomplete text
@ -564,7 +540,9 @@ public class ToolbarEditText extends CustomEditText
}
// Update search icon with an active state since user is typing
updateSearchIcon(textLength > 0);
if (mSearchStateChangeListener != null) {
mSearchStateChangeListener.onSearchStateChange(textLength > 0);
}
if (mFilterListener != null) {
mFilterListener.onFilter(text, doAutocomplete ? ToolbarEditText.this : null);

View File

@ -970,6 +970,14 @@ var BrowserApp = {
filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
aTarget.ownerDocument, true, null);
});
NativeWindow.contextmenus.add(stringGetter("contextmenu.showImage"),
NativeWindow.contextmenus.imageBlockingPolicyContext,
function(target) {
UITelemetry.addEvent("action.1", "contextmenu", null, "web_show_image");
target.setAttribute("data-ctv-show", "true");
target.setAttribute("src", target.getAttribute("data-ctv-src"));
});
},
onAppUpdated: function() {
@ -1719,6 +1727,11 @@ var BrowserApp = {
// Check to see if this is a message to enable/disable mixed content blocking.
if (aData) {
let data = JSON.parse(aData);
if (data.bypassCache) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
}
if (data.contentType === "tracking") {
// Convert document URI into the format used by
// nsChannelClassifier::ShouldEnableTrackingProtection
@ -2520,6 +2533,23 @@ var NativeWindow = {
}
},
imageBlockingPolicyContext: {
matches: function imageBlockingPolicyContextMatches(aElement) {
if (!Services.prefs.getBoolPref("browser.image_blocking.enabled")) {
return false;
}
if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
// Only show the menuitem if we are blocking the image
if (aElement.getAttribute("data-ctv-show") == "true") {
return false;
}
return true;
}
return false;
}
},
mediaContext: function(aMode) {
return {
matches: function(aElt) {

View File

@ -0,0 +1,74 @@
/* 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 { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
/**
* Content policy for blocking images
*/
// SVG placeholder image for blocked image content
let PLACEHOLDER_IMG = "chrome://browser/skin/images/placeholder_image.svg";
function ImageBlockingPolicy() {}
ImageBlockingPolicy.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy]),
classDescription: "Click-To-Play Image",
classID: Components.ID("{f55f77f9-d33d-4759-82fc-60db3ee0bb91}"),
contractID: "@mozilla.org/browser/blockimages-policy;1",
xpcom_categories: [{category: "content-policy", service: true}],
// nsIContentPolicy interface implementation
shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
if (!getEnabled()) {
return Ci.nsIContentPolicy.ACCEPT;
}
if (contentType === Ci.nsIContentPolicy.TYPE_IMAGE || contentType === Ci.nsIContentPolicy.TYPE_IMAGESET) {
// Accept any non-http(s) image URLs
if (!contentLocation.schemeIs("http") && !contentLocation.schemeIs("https")) {
return Ci.nsIContentPolicy.ACCEPT;
}
if (node instanceof Ci.nsIDOMHTMLImageElement) {
// Accept if the user has asked to view the image
if (node.getAttribute("data-ctv-show") == "true") {
return Ci.nsIContentPolicy.ACCEPT;
}
setTimeout(() => {
// Cache the original image URL and swap in our placeholder
node.setAttribute("data-ctv-src", contentLocation.spec);
node.setAttribute("src", PLACEHOLDER_IMG);
// For imageset (img + srcset) the "srcset" is used even after we reset the "src" causing a loop.
// We are given the final image URL anyway, so it's OK to just remove the "srcset" value.
node.removeAttribute("srcset");
}, 0);
}
// Reject any image that is not associated with a DOM element
return Ci.nsIContentPolicy.REJECT;
}
// Accept all other content types
return Ci.nsIContentPolicy.ACCEPT;
},
shouldProcess: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) {
return Ci.nsIContentPolicy.ACCEPT;
},
};
function getEnabled() {
return Services.prefs.getBoolPref("browser.image_blocking.enabled");
}
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ImageBlockingPolicy]);

View File

@ -55,6 +55,11 @@ contract @mozilla.org/embedcomp/prompt-service;1 {9a61149b-2276-4a0a-b79c-be994a
category wakeup-request PromptService @mozilla.org/embedcomp/prompt-service;1,nsIPromptService,getService,Prompt:Call
#endif
# ImageBlockingPolicy.js
component {f55f77f9-d33d-4759-82fc-60db3ee0bb91} ImageBlockingPolicy.js
contract @mozilla.org/browser/blockimages-policy;1 {f55f77f9-d33d-4759-82fc-60db3ee0bb91}
category content-policy ImageBlockingPolicy @mozilla.org/browser/blockimages-policy;1
# XPIDialogService.js
component {c1242012-27d8-477e-a0f1-0b098ffc329b} XPIDialogService.js
contract @mozilla.org/addons/web-install-prompt;1 {c1242012-27d8-477e-a0f1-0b098ffc329b}

View File

@ -21,6 +21,7 @@ EXTRA_COMPONENTS += [
'DirectoryProvider.js',
'FilePicker.js',
'HelperAppDialog.js',
'ImageBlockingPolicy.js',
'LoginManagerPrompter.js',
'NSSDialogService.js',
'SiteSpecificUserAgent.js',

View File

@ -2,7 +2,7 @@
; 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/.
; Package file for the Fennec build.
; Package file for the Fennec build.
;
; File format:
;
@ -522,7 +522,7 @@
@BINPATH@/defaults/profile/prefs.js
; [Layout Engine Resources]
; Style Sheets, Graphics and other Resources used by the layout engine.
; Style Sheets, Graphics and other Resources used by the layout engine.
@BINPATH@/res/EditorOverride.css
@BINPATH@/res/contenteditable.css
@BINPATH@/res/designmode.css
@ -620,6 +620,7 @@ bin/libfreebl_32int64_3.so
@BINPATH@/components/ColorPicker.js
@BINPATH@/components/ContentDispatchChooser.js
@BINPATH@/components/ContentPermissionPrompt.js
@BINPATH@/components/ImageBlockingPolicy.js
@BINPATH@/components/DirectoryProvider.js
@BINPATH@/components/FilePicker.js
@BINPATH@/components/HelperAppDialog.js
@ -647,7 +648,7 @@ bin/libfreebl_32int64_3.so
@BINPATH@/components/browsercomps.xpt
#ifdef ENABLE_MARIONETTE
@BINPATH@/chrome/marionette@JAREXT@
@BINPATH@/chrome/marionette@JAREXT@
@BINPATH@/chrome/marionette.manifest
@BINPATH@/components/MarionetteComponents.manifest
@BINPATH@/components/marionettecomponent.js

View File

@ -248,6 +248,7 @@ contextmenu.shareImage=Share Image
# the text you have selected. %S is the name of the search engine. For example, "Google".
contextmenu.search=%S Search
contextmenu.saveImage=Save Image
contextmenu.showImage=Show Image
contextmenu.setImageAs=Set Image As
contextmenu.addSearchEngine2=Add as Search Engine
contextmenu.playMedia=Play

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 42 42" style="enable-background:new 0 0 42 42;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;filter:url(#Adobe_OpacityMaskFilter);}
.st1{fill:#010101;}
.st2{mask:url(#mask-cutout-blocked-sign_1_);}
.st3{fill:#F1F1F2;}
.st4{fill:#7F8081;}
.st5{fill:#4D4D4E;}
.st6{fill:#979899;}
.st7{fill:#010101;filter:url(#Adobe_OpacityMaskFilter_1_);}
.st8{fill:#FFFFFF;}
.st9{mask:url(#mask-cutout-frame_1_);fill:#656667;}
.st10{fill:#FFFFFF;filter:url(#Adobe_OpacityMaskFilter_2_);}
.st11{mask:url(#mask-cutout-blocked-sign-inner_1_);fill:#656667;}
.st12{fill:none;stroke:#656667;stroke-width:2;}
</style>
<g id="Layer_1">
<defs>
<filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="5" y="5" width="32" height="32">
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask maskUnits="userSpaceOnUse" x="5" y="5" width="32" height="32" id="mask-cutout-blocked-sign_1_">
<rect x="5" y="5" class="st0" width="32" height="32"/>
<circle class="st1" cx="30" cy="30" r="8"/>
</mask>
<g id="icon-frame" class="st2">
<path id="shape-background" class="st3" d="M10,9h22c1.7,0,3,1.3,3,3v18c0,1.7-1.3,3-3,3H10c-1.7,0-3-1.3-3-3V12 C7,10.3,8.3,9,10,9z"/>
<polygon class="st4" points="8,31 16,21 23,31 "/>
<polygon class="st5" points="16,31 28,15 36,25 36,31 "/>
<circle class="st6" cx="14" cy="16" r="3"/>
<defs>
<filter id="Adobe_OpacityMaskFilter_1_" filterUnits="userSpaceOnUse" x="5" y="5" width="32" height="32">
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask maskUnits="userSpaceOnUse" x="5" y="5" width="32" height="32" id="mask-cutout-frame_1_">
<rect x="5" y="5" class="st7" width="32" height="32"/>
<path class="st8" d="M10,9h22c1.7,0,3,1.3,3,3v18c0,1.7-1.3,3-3,3H10c-1.7,0-3-1.3-3-3V12C7,10.3,8.3,9,10,9z"/>
<path class="st1" d="M11,11h20c1.1,0,2,0.9,2,2v16c0,1.1-0.9,2-2,2H11c-1.1,0-2-0.9-2-2V13C9,11.9,9.9,11,11,11z"/>
</mask>
<rect x="5" y="5" class="st9" width="32" height="32"/>
</g>
<g id="icon-blocked-sign">
<defs>
<filter id="Adobe_OpacityMaskFilter_2_" filterUnits="userSpaceOnUse" x="24" y="24" width="12" height="12">
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask maskUnits="userSpaceOnUse" x="24" y="24" width="12" height="12" id="mask-cutout-blocked-sign-inner_1_">
<rect x="5" y="5" class="st10" width="32" height="32"/>
<circle class="st1" cx="30" cy="30" r="4"/>
</mask>
<circle class="st11" cx="30" cy="30" r="6"/>
<line class="st12" x1="26" y1="34" x2="34" y2="26"/>
</g>
</g>
<g id="Layer_2">
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -74,6 +74,7 @@ chrome.jar:
skin/images/certerror-warning.png (images/certerror-warning.png)
skin/images/throbber.png (images/throbber.png)
skin/images/search-clear-30.png (images/search-clear-30.png)
skin/images/placeholder_image.svg (images/placeholder_image.svg)
skin/images/play-hdpi.png (images/play-hdpi.png)
skin/images/pause-hdpi.png (images/pause-hdpi.png)
skin/images/cast-ready-hdpi.png (images/cast-ready-hdpi.png)

View File

@ -32,6 +32,9 @@ const PREF_LOG_LEVEL = "services.common.log.logger.tokenserverclient";
this.TokenServerClientError = function TokenServerClientError(message) {
this.name = "TokenServerClientError";
this.message = message || "Client error.";
// Without explicitly setting .stack, all stacks from these errors will point
// to the "new Error()" call a few lines down, which isn't helpful.
this.stack = Error().stack;
}
TokenServerClientError.prototype = new Error();
TokenServerClientError.prototype.constructor = TokenServerClientError;
@ -52,6 +55,7 @@ this.TokenServerClientNetworkError =
function TokenServerClientNetworkError(error) {
this.name = "TokenServerClientNetworkError";
this.error = error;
this.stack = Error().stack;
}
TokenServerClientNetworkError.prototype = new TokenServerClientError();
TokenServerClientNetworkError.prototype.constructor =
@ -96,6 +100,7 @@ this.TokenServerClientServerError =
this.name = "TokenServerClientServerError";
this.message = message || "Server error.";
this.cause = cause;
this.stack = Error().stack;
}
TokenServerClientServerError.prototype = new TokenServerClientError();
TokenServerClientServerError.prototype.constructor =

View File

@ -7,6 +7,7 @@
this.EXPORTED_SYMBOLS = [
"btoa", // It comes from a module import.
"encryptPayload",
"isConfiguredWithLegacyIdentity",
"ensureLegacyIdentityManager",
"setBasicCredentials",
"makeIdentityConfig",
@ -94,6 +95,18 @@ this.waitForZeroTimer = function waitForZeroTimer(callback) {
CommonUtils.namedTimer(wait, 150, {}, "timer");
}
/**
* Return true if Sync is configured with the "legacy" identity provider.
*/
this.isConfiguredWithLegacyIdentity = function() {
let ns = {};
Cu.import("resource://services-sync/service.js", ns);
// We can't use instanceof as BrowserIDManager (the "other" identity) inherits
// from IdentityManager so that would return true - so check the prototype.
return Object.getPrototypeOf(ns.Service.identity) === IdentityManager.prototype;
}
/**
* Ensure Sync is configured with the "legacy" identity provider.
*/

View File

@ -692,6 +692,10 @@ this.BrowserIDManager.prototype = {
_getAuthenticationHeader: function(httpObject, method) {
let cb = Async.makeSpinningCallback();
this._ensureValidToken().then(cb, cb);
// Note that in failure states we return null, causing the request to be
// made without authorization headers, thereby presumably causing a 401,
// which causes Sync to log out. If we throw, this may not happen as
// expected.
try {
cb.wait();
} catch (ex if !Async.isShutdownException(ex)) {
@ -730,8 +734,17 @@ this.BrowserIDManager.prototype = {
createClusterManager: function(service) {
return new BrowserIDClusterManager(service);
}
},
// Tell Sync what the login status should be if it saw a 401 fetching
// info/collections as part of login verification (typically immediately
// after login.)
// In our case, it almost certainly means a transient error fetching a token
// (and hitting this will cause us to logout, which will correctly handle an
// authoritative login issue.)
loginStatusFromVerification404() {
return LOGIN_FAILED_NETWORK_ERROR;
},
};
/* An implementation of the ClusterManager for this identity
@ -777,7 +790,7 @@ BrowserIDClusterManager.prototype = {
// it's likely a 401 was received using the existing token - in which
// case we just discard the existing token and fetch a new one.
if (this.service.clusterURL) {
log.debug("_findCluster found existing clusterURL, so discarding the current token");
log.debug("_findCluster has a pre-existing clusterURL, so discarding the current token");
this.identity._token = null;
}
return this.identity._ensureValidToken();

View File

@ -590,4 +590,13 @@ IdentityManager.prototype = {
// Do nothing for Sync 1.1.
return {accepted: true};
},
// Tell Sync what the login status should be if it saw a 401 fetching
// info/collections as part of login verification (typically immediately
// after login.)
// In our case it means an authoritative "password is incorrect".
loginStatusFromVerification404() {
return LOGIN_FAILED_LOGIN_REJECTED;
}
};

View File

@ -765,8 +765,12 @@ Sync11Service.prototype = {
return this.verifyLogin(false);
}
// We must have the right cluster, but the server doesn't expect us
this.status.login = LOGIN_FAILED_LOGIN_REJECTED;
// We must have the right cluster, but the server doesn't expect us.
// The implications of this depend on the identity being used - for
// the legacy identity, it's an authoritatively "incorrect password",
// (ie, LOGIN_FAILED_LOGIN_REJECTED) but for FxA it probably means
// "transient error fetching auth token".
this.status.login = this.identity.loginStatusFromVerification404();
return false;
default:
@ -990,6 +994,7 @@ Sync11Service.prototype = {
}
// Ask the identity manager to explicitly login now.
this._log.info("Logging in the user.");
let cb = Async.makeSpinningCallback();
this.identity.ensureLoggedIn().then(
() => cb(null),
@ -1005,9 +1010,9 @@ Sync11Service.prototype = {
&& (username || password || passphrase)) {
Svc.Obs.notify("weave:service:setup-complete");
}
this._log.info("Logging in the user.");
this._updateCachedURLs();
this._log.info("User logged in successfully - verifying login.");
if (!this.verifyLogin()) {
// verifyLogin sets the failure states here.
throw "Login failed: " + this.status.login;

View File

@ -184,7 +184,10 @@ add_identity_test(this, function test_401_logout() {
let errorCount = sumHistogram("WEAVE_STORAGE_AUTH_ERRORS", { key: "info/collections" });
do_check_eq(errorCount, 2);
do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED);
let expected = isConfiguredWithLegacyIdentity() ?
LOGIN_FAILED_LOGIN_REJECTED : LOGIN_FAILED_NETWORK_ERROR;
do_check_eq(Status.login, expected);
do_check_false(Service.isLoggedIn);
// Clean up.

View File

@ -956,11 +956,22 @@ add_identity_test(this, function test_loginError_fatal_clearsTriggers() {
Svc.Obs.add("weave:service:login:error", function onLoginError() {
Svc.Obs.remove("weave:service:login:error", onLoginError);
Utils.nextTick(function aLittleBitAfterLoginError() {
do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED);
do_check_eq(scheduler.nextSync, 0);
do_check_eq(scheduler.syncTimer, null);
if (isConfiguredWithLegacyIdentity()) {
// for the "legacy" identity, a 401 on info/collections means the
// password is wrong, so we enter a "login rejected" state.
do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED);
do_check_eq(scheduler.nextSync, 0);
do_check_eq(scheduler.syncTimer, null);
} else {
// For the FxA identity, a 401 on info/collections means a transient
// error, probably due to an inability to fetch a token.
do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
// syncs should still be scheduled.
do_check_true(scheduler.nextSync > Date.now());
do_check_true(scheduler.syncTimer.delay > 0);
}
cleanUpAndGo(server).then(deferred.resolve);
});
});

View File

@ -458,31 +458,5 @@ this.BrowserTestUtils = {
tab.ownerDocument.defaultView.gBrowser.removeTab(tab);
}
});
},
/**
* Version of EventUtils' `sendChar` function; it will synthesize a keypress
* event in a child process and returns a Promise that will result when the
* event was fired. Instead of a Window, a Browser object is required to be
* passed to this function.
*
* @param {String} char
* A character for the keypress event that is sent to the browser.
* @param {Browser} browser
* Browser element, must not be null.
*
* @returns {Promise}
* @resolves True if the keypress event was synthesized.
*/
sendChar(char, browser) {
return new Promise(resolve => {
let mm = browser.messageManager;
mm.addMessageListener("Test:SendCharDone", function charMsg(message) {
mm.removeMessageListener("Test:SendCharDone", charMsg);
resolve(message.data.sendCharResult);
});
mm.sendAsyncMessage("Test:SendChar", { char: char });
});
}
};

View File

@ -11,10 +11,6 @@ EventUtils.window = {};
EventUtils.parent = EventUtils.window;
EventUtils._EU_Ci = Components.interfaces;
EventUtils._EU_Cc = Components.classes;
// EventUtils' `sendChar` function relies on the navigator to synthetize events.
EventUtils.navigator = content.document.defaultView.navigator;
EventUtils.KeyboardEvent = content.document.defaultView.KeyboardEvent;
Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
addMessageListener("Test:SynthesizeMouse", (message) => {
@ -50,8 +46,3 @@ addMessageListener("Test:SynthesizeMouse", (message) => {
let result = EventUtils.synthesizeMouseAtPoint(left, top, data.event, content);
sendAsyncMessage("Test:SynthesizeMouseDone", { defaultPrevented: result });
});
addMessageListener("Test:SendChar", message => {
let result = EventUtils.sendChar(message.data.char, content);
sendAsyncMessage("Test:SendCharDone", { sendCharResult: result });
});

View File

@ -18,6 +18,7 @@ skip-if = e10s # Bug 1064580
[browser_f7_caret_browsing.js]
skip-if = e10s
[browser_findbar.js]
skip-if = e10s # Disabled for e10s: Bug ?????? - seems to be a timing issue with RemoteFinder.jsm messages coming later than the tests expect.
[browser_input_file_tooltips.js]
skip-if = e10s # Bug ?????? - test directly manipulates content (TypeError: doc.createElement is not a function)
[browser_isSynthetic.js]

View File

@ -2,8 +2,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
Components.utils.import("resource://gre/modules/Timer.jsm", this);
const TEST_PAGE_URI = "data:text/html;charset=utf-8,The letter s.";
/**
* Makes sure that the findbar hotkeys (' and /) event listeners
* are added to the system event group and do not get blocked
@ -13,7 +11,7 @@ add_task(function* test_hotkey_event_propagation() {
info("Ensure hotkeys are not affected by stopPropagation.");
// Opening new tab
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
let tab = yield promiseTestPageLoad();
let browser = gBrowser.getBrowserForTab(tab);
let findbar = gBrowser.getFindBar();
@ -25,29 +23,24 @@ add_task(function* test_hotkey_event_propagation() {
is(findbar.hidden, true, "Findbar is hidden now.");
gBrowser.selectedTab = tab;
yield promiseFocus();
yield BrowserTestUtils.sendChar(key, browser);
EventUtils.sendChar(key, browser.contentWindow);
is(findbar.hidden, false, "Findbar should not be hidden.");
yield closeFindbarAndWait(findbar);
}
// Stop propagation for all keyboard events.
let frameScript = () => {
const stopPropagation = e => e.stopImmediatePropagation();
let window = content.document.defaultView;
window.removeEventListener("keydown", stopPropagation);
window.removeEventListener("keypress", stopPropagation);
window.removeEventListener("keyup", stopPropagation);
};
let mm = browser.messageManager;
mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", false);
let window = browser.contentWindow;
let stopPropagation = function(e) { e.stopImmediatePropagation(); };
window.addEventListener("keydown", stopPropagation, true);
window.addEventListener("keypress", stopPropagation, true);
window.addEventListener("keyup", stopPropagation, true);
// Checking if findbar still appears when any hotkey is pressed.
for (let key of HOTKEYS) {
is(findbar.hidden, true, "Findbar is hidden now.");
gBrowser.selectedTab = tab;
yield promiseFocus();
yield BrowserTestUtils.sendChar(key, browser);
EventUtils.sendChar(key, browser.contentWindow);
is(findbar.hidden, false, "Findbar should not be hidden.");
yield closeFindbarAndWait(findbar);
}
@ -58,7 +51,7 @@ add_task(function* test_hotkey_event_propagation() {
add_task(function* test_not_found() {
info("Check correct 'Phrase not found' on new tab");
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
let tab = yield promiseTestPageLoad();
// Search for the first word.
yield promiseFindFinished("--- THIS SHOULD NEVER MATCH ---", false);
@ -70,7 +63,7 @@ add_task(function* test_not_found() {
});
add_task(function* test_found() {
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
let tab = yield promiseTestPageLoad();
// Search for a string that WILL be found, with 'Highlight All' on
yield promiseFindFinished("S", true);
@ -83,10 +76,10 @@ add_task(function* test_found() {
// Setting first findbar to case-sensitive mode should not affect
// new tab find bar.
add_task(function* test_tabwise_case_sensitive() {
let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
let tab1 = yield promiseTestPageLoad();
let findbar1 = gBrowser.getFindBar();
let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
let tab2 = yield promiseTestPageLoad();
let findbar2 = gBrowser.getFindBar();
// Toggle case sensitivity for first findbar
@ -109,33 +102,22 @@ add_task(function* test_tabwise_case_sensitive() {
gBrowser.removeTab(tab2);
});
/**
* Navigating from a web page (for example mozilla.org) to an internal page
* (like about:addons) might trigger a change of browser's remoteness.
* 'Remoteness change' means that rendering page content moves from child
* process into the parent process or the other way around.
* This test ensures that findbar properly handles such a change.
*/
add_task(function * test_reinitialization_at_remoteness_change() {
info("Ensure findbar re-initialization at remoteness change.");
function promiseTestPageLoad() {
let deferred = Promise.defer();
// Load a remote page and trigger findbar construction.
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI);
let browser = gBrowser.getBrowserForTab(tab);
let findbar = gBrowser.getFindBar();
let tab = gBrowser.selectedTab = gBrowser.addTab("data:text/html;charset=utf-8,The letter s.");
let browser = gBrowser.selectedBrowser;
browser.addEventListener("load", function listener() {
if (browser.currentURI.spec == "about:blank")
return;
info("Page loaded: " + browser.currentURI.spec);
browser.removeEventListener("load", listener, true);
// Findbar should operate normally.
yield promiseFindFinished("s", false);
ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty");
deferred.resolve(tab);
}, true);
gBrowser.updateBrowserRemoteness(browser, false);
// Findbar should keep operating normally.
yield promiseFindFinished("s", false);
ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty");
yield BrowserTestUtils.removeTab(tab);
});
return deferred.promise;
}
function promiseFindFinished(searchText, highlightOn) {
let deferred = Promise.defer();
@ -149,17 +131,8 @@ function promiseFindFinished(searchText, highlightOn) {
findbar._findField.value = searchText;
let resultListener;
// When highlighting is on the finder sends a second "FOUND" message after
// the search wraps. This causes timing problems with e10s. waitMore
// forces foundOrTimeout wait for the second "FOUND" message before
// resolving the promise.
let waitMore = highlightOn;
let findTimeout = setTimeout(() => foundOrTimedout(null), 2000);
let foundOrTimedout = function(aData) {
if (aData !== null && waitMore) {
waitMore = false;
return;
}
if (aData === null)
info("Result listener not called, timeout reached.");
clearTimeout(findTimeout);

View File

@ -373,28 +373,12 @@
// browser property
if (this.getAttribute("browserid"))
setTimeout(function(aSelf) { aSelf.browser = aSelf.browser; }, 0, this);
if (typeof gBrowser !== 'undefined')
gBrowser.tabContainer.addEventListener("TabRemotenessChange", this);
]]></constructor>
<destructor><![CDATA[
this.destroy();
]]></destructor>
<method name="handleEvent">
<parameter name="aEvent"/>
<body><![CDATA[
switch(aEvent.type) {
case "onRemotenessChange":
// Reinitializing browser to re-attach listeners.
this.browser._lastSearchString = this._findField.value;
this.browser = this.browser;
break;
}
]]></body>
</method>
<!-- This is necessary because the destructor isn't called when
we are removed from a document that is not destroyed. This
needs to be explicitly called in this case -->
@ -418,9 +402,6 @@
// Clear all timers that might still be running.
this._cancelTimers();
if (typeof gBrowser !== 'undefined')
gBrowser.tabContainer.removeEventListener("TabRemotenessChange", this);
]]></body>
</method>

View File

@ -228,7 +228,21 @@ nsUnknownContentTypeDialog.prototype = {
// because the original one is definitely gone (and nsIFilePicker doesn't like
// a null parent):
gDownloadLastDir = this._mDownloadDir;
parent = Services.wm.getMostRecentWindow("");
let windowsEnum = Services.wm.getEnumerator("");
while (windowsEnum.hasMoreElements()) {
let someWin = windowsEnum.getNext();
// We need to make sure we don't end up with this dialog, because otherwise
// that's going to go away when the user clicks "Save", and that breaks the
// windows file picker that's supposed to show up if we let the user choose
// where to save files...
if (someWin != this.mDialog) {
parent = someWin;
}
}
if (!parent) {
Cu.reportError("No candidate parent windows were found for the save filepicker." +
"This should never happen.");
}
}
Task.spawn(function() {