Merge m-c to inbound, a=merge
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
@ -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"/>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 |
@ -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$="-white"] { 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 |
@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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})
|
||||
|
@ -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} />
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 779 B |
Before Width: | Height: | Size: 824 B After Width: | Height: | Size: 998 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"/>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
74
mobile/android/components/ImageBlockingPolicy.js
Normal 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]);
|
@ -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}
|
||||
|
@ -21,6 +21,7 @@ EXTRA_COMPONENTS += [
|
||||
'DirectoryProvider.js',
|
||||
'FilePicker.js',
|
||||
'HelperAppDialog.js',
|
||||
'ImageBlockingPolicy.js',
|
||||
'LoginManagerPrompter.js',
|
||||
'NSSDialogService.js',
|
||||
'SiteSpecificUserAgent.js',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
62
mobile/android/themes/core/images/placeholder_image.svg
Normal 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 |
@ -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)
|
||||
|
@ -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 =
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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 });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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 });
|
||||
});
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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() {
|
||||
|