mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge m-c to inbound, a=merge CLOSED TREE
This commit is contained in:
commit
16a989a7b0
@ -537,6 +537,48 @@ SettingsListener.observe("theme.selected",
|
||||
setPAC();
|
||||
})();
|
||||
|
||||
#ifdef MOZ_B2G_RIL
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppsUtils",
|
||||
"resource://gre/modules/AppsUtils.jsm");
|
||||
|
||||
// ======================= Dogfooders FOTA ==========================
|
||||
SettingsListener.observe('debug.performance_data.dogfooding', false,
|
||||
isDogfooder => {
|
||||
if (!isDogfooder) {
|
||||
dump('AUS:Settings: Not a dogfooder!\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('mozTelephony' in navigator)) {
|
||||
dump('AUS:Settings: There is no mozTelephony!\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('mozMobileConnections' in navigator)) {
|
||||
dump('AUS:Settings: There is no mozMobileConnections!\n');
|
||||
return;
|
||||
}
|
||||
|
||||
let conn = navigator.mozMobileConnections[0];
|
||||
conn.addEventListener('radiostatechange', function onradiostatechange() {
|
||||
if (conn.radioState !== 'enabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
conn.removeEventListener('radiostatechange', onradiostatechange);
|
||||
navigator.mozTelephony.dial('*#06#').then(call => {
|
||||
return call.result.then(res => {
|
||||
if (res.success && res.statusMessage
|
||||
&& (res.serviceCode === 'scImei')) {
|
||||
Services.prefs.setCharPref("app.update.imei_hash",
|
||||
AppsUtils.computeHash(res.statusMessage, "SHA512"));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
#endif
|
||||
|
||||
// =================== Various simple mapping ======================
|
||||
var settingsToObserve = {
|
||||
'accessibility.screenreader_quicknav_modes': {
|
||||
|
@ -15,11 +15,11 @@
|
||||
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
|
||||
|
@ -15,11 +15,11 @@
|
||||
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
|
||||
|
@ -19,12 +19,12 @@
|
||||
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="067c08fb3e5744b42b68d1f861245f7d507109bc"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
|
||||
<!-- Stock Android things -->
|
||||
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
|
||||
|
@ -17,9 +17,9 @@
|
||||
</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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="a1f9532e4157df2dc8d3e9c8100120f80117dcd4"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
|
||||
|
@ -15,11 +15,11 @@
|
||||
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
|
||||
|
@ -15,11 +15,11 @@
|
||||
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
|
||||
|
@ -19,12 +19,12 @@
|
||||
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="067c08fb3e5744b42b68d1f861245f7d507109bc"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
|
||||
<!-- Stock Android things -->
|
||||
<project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
|
||||
|
@ -15,11 +15,11 @@
|
||||
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
|
||||
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"git": {
|
||||
"git_revision": "aede8622d780ec71f766a3ecccbff74c04aaba4e",
|
||||
"git_revision": "2082894c8e974b0c371e4dec298e0ad0f3ac56b1",
|
||||
"remote": "https://git.mozilla.org/releases/gaia.git",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "dbd3a4ea9042cae987147f2d05f41d2a7ebaccbc",
|
||||
"revision": "0e712c8d330e10908f99194a9638e62a07c5c483",
|
||||
"repo_path": "integration/gaia-central"
|
||||
}
|
||||
|
@ -17,9 +17,9 @@
|
||||
</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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="a1f9532e4157df2dc8d3e9c8100120f80117dcd4"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
|
||||
|
@ -15,11 +15,11 @@
|
||||
<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="aede8622d780ec71f766a3ecccbff74c04aaba4e"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2082894c8e974b0c371e4dec298e0ad0f3ac56b1"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="2782648aabf0af464dd9c4202b367b408898546d"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
<project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
|
||||
|
@ -6211,12 +6211,23 @@
|
||||
else
|
||||
aMenuitem.removeAttribute("selected");
|
||||
|
||||
function addEndImage() {
|
||||
let endImage = document.createElement("image");
|
||||
endImage.setAttribute("class", "alltabs-endimage");
|
||||
let endImageContainer = document.createElement("hbox");
|
||||
endImageContainer.setAttribute("align", "center");
|
||||
endImageContainer.setAttribute("pack", "center");
|
||||
endImageContainer.appendChild(endImage);
|
||||
aMenuitem.appendChild(endImageContainer);
|
||||
return endImage;
|
||||
}
|
||||
|
||||
if (aMenuitem.firstChild)
|
||||
aMenuitem.firstChild.remove();
|
||||
if (aTab.hasAttribute("muted"))
|
||||
aMenuitem.setAttribute("endimage", "chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio-muted");
|
||||
addEndImage().setAttribute("muted", "true");
|
||||
else if (aTab.hasAttribute("soundplaying"))
|
||||
aMenuitem.setAttribute("endimage", "chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio");
|
||||
else
|
||||
aMenuitem.removeAttribute("endimage");
|
||||
addEndImage().setAttribute("soundplaying", "true");
|
||||
]]></body>
|
||||
</method>
|
||||
</implementation>
|
||||
|
@ -10,8 +10,6 @@
|
||||
* ALL need to match an error in order for that error not to cause a test
|
||||
* failure. */
|
||||
const kWhitelist = [
|
||||
// Cleopatra is imported as-is, see bug 1004421.
|
||||
{sourceName: /cleopatra.*(tree|ui)\.css/i},
|
||||
// CodeMirror is imported as-is, see bug 1004423.
|
||||
{sourceName: /codemirror\.css/i},
|
||||
// PDFjs is futureproofing its pseudoselectors, and those rules are dropped.
|
||||
@ -24,6 +22,13 @@ const kWhitelist = [
|
||||
// Loop standalone client CSS uses placeholder cross browser pseudo-element
|
||||
{sourceName: /loop\/.*\.css/i,
|
||||
errorMessage: /Unknown pseudo-class.*placeholder/i},
|
||||
// Loop issues that crept in since this test was broken, see bug ...
|
||||
{sourceName: /loop\/.*shared\/css\/conversation.css/i,
|
||||
errorMessage: /Error in parsing value for 'display'/i},
|
||||
{sourceName: /loop\/.*shared\/css\/common.css/i,
|
||||
errorMessage: /Unknown property 'user-select'/i},
|
||||
{sourceName: /loop\/.*css\/panel.css/i,
|
||||
errorMessage: /Expected color but found 'none'/i},
|
||||
// Highlighter CSS uses chrome-only pseudo-class, see bug 985597.
|
||||
{sourceName: /highlighter\.css/i,
|
||||
errorMessage: /Unknown pseudo-class.*moz-native-anonymous/i},
|
||||
@ -65,6 +70,21 @@ function once(target, name) {
|
||||
});
|
||||
}
|
||||
|
||||
function messageIsCSSError(msg, innerWindowID, outerWindowID) {
|
||||
// Only care about CSS errors generated by our iframe:
|
||||
if ((msg instanceof Ci.nsIScriptError) &&
|
||||
msg.category.includes("CSS") &&
|
||||
msg.innerWindowID === innerWindowID && msg.outerWindowID === outerWindowID) {
|
||||
// Check if this error is whitelisted in kWhitelist
|
||||
if (!ignoredError(msg)) {
|
||||
ok(false, "Got error message for " + msg.sourceName + ": " + msg.errorMessage);
|
||||
return true;
|
||||
}
|
||||
info("Ignored error for " + msg.sourceName + " because of filter.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
add_task(function checkAllTheCSS() {
|
||||
let appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
|
||||
// This asynchronously produces a list of URLs (sadly, mostly sync on our
|
||||
@ -86,32 +106,20 @@ add_task(function checkAllTheCSS() {
|
||||
iframe.contentWindow.location = testFile;
|
||||
yield iframeLoaded;
|
||||
let doc = iframe.contentWindow.document;
|
||||
|
||||
// Listen for errors caused by the CSS:
|
||||
let errorListener = {
|
||||
observe: function(aMessage) {
|
||||
if (!aMessage || !(aMessage instanceof Ci.nsIScriptError)) {
|
||||
return;
|
||||
}
|
||||
// Only care about CSS errors generated by our iframe:
|
||||
if (aMessage.category.includes("CSS") && aMessage.innerWindowID === 0 && aMessage.outerWindowID === 0) {
|
||||
// Check if this error is whitelisted in kWhitelist
|
||||
if (!ignoredError(aMessage)) {
|
||||
ok(false, "Got error message for " + aMessage.sourceName + ": " + aMessage.errorMessage);
|
||||
errors++;
|
||||
} else {
|
||||
info("Ignored error for " + aMessage.sourceName + " because of filter.");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let windowUtils = iframe.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
let innerWindowID = windowUtils.currentInnerWindowID;
|
||||
let outerWindowID = windowUtils.outerWindowID;
|
||||
|
||||
// We build a list of promises that get resolved when their respective
|
||||
// files have loaded and produced no errors.
|
||||
let allPromises = [];
|
||||
let errors = 0;
|
||||
// Register the error listener to keep track of errors.
|
||||
Services.console.registerListener(errorListener);
|
||||
|
||||
// filter out either the devtools paths or the non-devtools paths:
|
||||
let isDevtools = SimpleTest.harnessParameters.subsuite == "devtools";
|
||||
let devtoolsPathBits = ["webide", "devtools"];
|
||||
uris = uris.filter(uri => isDevtools == devtoolsPathBits.some(path => uri.spec.includes(path)));
|
||||
|
||||
for (let uri of uris) {
|
||||
let linkEl = doc.createElement("link");
|
||||
linkEl.setAttribute("rel", "stylesheet");
|
||||
@ -122,7 +130,8 @@ add_task(function checkAllTheCSS() {
|
||||
linkEl.removeEventListener("error", onError);
|
||||
};
|
||||
let onError = (e) => {
|
||||
promiseForThisSpec.reject({error: e, href: linkEl.getAttribute("href")});
|
||||
ok(false, "Loading " + linkEl.getAttribute("href") + " threw an error!");
|
||||
promiseForThisSpec.resolve();
|
||||
linkEl.removeEventListener("load", onLoad);
|
||||
linkEl.removeEventListener("error", onError);
|
||||
};
|
||||
@ -136,12 +145,14 @@ add_task(function checkAllTheCSS() {
|
||||
|
||||
// Wait for all the files to have actually loaded:
|
||||
yield Promise.all(allPromises);
|
||||
|
||||
let messages = Services.console.getMessageArray();
|
||||
// Count errors (the test output will list actual issues for us, as well
|
||||
// as the ok(false) in the error listener)
|
||||
is(errors, 0, "All the styles (" + allPromises.length + ") loaded without errors.");
|
||||
// as the ok(false) in messageIsCSSError.
|
||||
let errors = messages.filter(m => messageIsCSSError(m, innerWindowID, outerWindowID));
|
||||
is(errors.length, 0, "All the styles (" + allPromises.length + ") loaded without errors.");
|
||||
|
||||
// Clean up to avoid leaks:
|
||||
Services.console.unregisterListener(errorListener);
|
||||
iframe.remove();
|
||||
doc.head.innerHTML = '';
|
||||
doc = null;
|
||||
|
@ -64,8 +64,12 @@ function iterateOverPath(path, extensions) {
|
||||
} else if (extensions.some((extension) => entry.name.endsWith(extension))) {
|
||||
let file = parentDir.clone();
|
||||
file.append(entry.name);
|
||||
let uriSpec = getURLForFile(file);
|
||||
files.push(Services.io.newURI(uriSpec, null, null));
|
||||
// the build system might leave dead symlinks hanging around, which are
|
||||
// returned as part of the directory iterator, but don't actually exist:
|
||||
if (file.exists()) {
|
||||
let uriSpec = getURLForFile(file);
|
||||
files.push(Services.io.newURI(uriSpec, null, null));
|
||||
}
|
||||
} else if (entry.name.endsWith(".ja") || entry.name.endsWith(".jar")) {
|
||||
let file = parentDir.clone();
|
||||
file.append(entry.name);
|
||||
|
@ -663,12 +663,6 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
case "userMadeSearchSuggestionsChoice":
|
||||
case "suggest.searches":
|
||||
this._cacheUserMadeSearchSuggestionsChoice();
|
||||
// Make sure the urlbar is focused. It won't be, for example,
|
||||
// if the user used an accesskey to make an opt-in choice.
|
||||
// mIgnoreFocus prevents the text from being selected.
|
||||
this.mIgnoreFocus = true;
|
||||
this.focus();
|
||||
this.mIgnoreFocus = false;
|
||||
if (this._userMadeSearchSuggestionsChoice) {
|
||||
this.popup.searchSuggestionsNotificationWasDismissed(
|
||||
this._prefs.getBoolPref("suggest.searches")
|
||||
@ -1189,6 +1183,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
<method name="dismissSearchSuggestionsNotification">
|
||||
<parameter name="enableSuggestions"/>
|
||||
<body><![CDATA[
|
||||
// Make sure the urlbar is focused. It won't be, for example, if the
|
||||
// user used an accesskey to make an opt-in choice. mIgnoreFocus
|
||||
// prevents the text from being selected.
|
||||
this.input.mIgnoreFocus = true;
|
||||
this.input.focus();
|
||||
this.input.mIgnoreFocus = false;
|
||||
|
||||
Services.prefs.setBoolPref(
|
||||
"browser.urlbar.suggest.searches", enableSuggestions
|
||||
);
|
||||
|
@ -19,7 +19,6 @@
|
||||
"chai": false,
|
||||
"console": false,
|
||||
"loop": true,
|
||||
"MozActivity": false,
|
||||
"mozRTCSessionDescription": false,
|
||||
"OT": false,
|
||||
"performance": false,
|
||||
|
@ -317,12 +317,9 @@ html[dir="rtl"] .contact-filter {
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
|
||||
.icon-contact-video-call:hover {
|
||||
background-color: #47b396;
|
||||
}
|
||||
|
||||
.icon-contact-video-call:hover,
|
||||
.icon-contact-video-call:active {
|
||||
background-color: #3aa689;
|
||||
background-color: #50E3C2;
|
||||
}
|
||||
|
||||
.icon-vertical-ellipsis {
|
||||
|
@ -134,9 +134,6 @@ loop.conversation = (function(mozL10n) {
|
||||
mozLoop: navigator.mozLoop
|
||||
});
|
||||
|
||||
// expose for functional tests
|
||||
loop.conversation._sdkDriver = sdkDriver;
|
||||
|
||||
// Create the stores.
|
||||
var conversationAppStore = new loop.store.ConversationAppStore({
|
||||
dispatcher: dispatcher,
|
||||
|
@ -134,9 +134,6 @@ loop.conversation = (function(mozL10n) {
|
||||
mozLoop: navigator.mozLoop
|
||||
});
|
||||
|
||||
// expose for functional tests
|
||||
loop.conversation._sdkDriver = sdkDriver;
|
||||
|
||||
// Create the stores.
|
||||
var conversationAppStore = new loop.store.ConversationAppStore({
|
||||
dispatcher: dispatcher,
|
||||
|
@ -7,363 +7,6 @@ loop.shared = loop.shared || {};
|
||||
loop.shared.models = (function(l10n) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Conversation model.
|
||||
*/
|
||||
var ConversationModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
connected: false, // Session connected flag
|
||||
ongoing: false, // Ongoing call flag
|
||||
callerId: undefined, // Loop caller id
|
||||
loopToken: undefined, // Loop conversation token
|
||||
sessionId: undefined, // OT session id
|
||||
sessionToken: undefined, // OT session token
|
||||
sessionType: undefined, // Hawk session type
|
||||
apiKey: undefined, // OT api key
|
||||
windowId: undefined, // The window id
|
||||
callId: undefined, // The callId on the server
|
||||
progressURL: undefined, // The websocket url to use for progress
|
||||
websocketToken: undefined, // The token to use for websocket auth, this is
|
||||
// stored as a hex string which is what the server
|
||||
// requires.
|
||||
callType: undefined, // The type of incoming call selected by
|
||||
// other peer ("audio" or "audio-video")
|
||||
selectedCallType: "audio-video", // The selected type for the call that was
|
||||
// initiated ("audio" or "audio-video")
|
||||
callToken: undefined, // Incoming call token.
|
||||
callUrl: undefined, // Incoming call url
|
||||
// Used for blocking a call url
|
||||
subscribedStream: false, // Used to indicate that a stream has been
|
||||
// subscribed to
|
||||
publishedStream: false // Used to indicate that a stream has been
|
||||
// published
|
||||
},
|
||||
|
||||
/**
|
||||
* SDK object.
|
||||
* @type {OT}
|
||||
*/
|
||||
sdk: undefined,
|
||||
|
||||
/**
|
||||
* SDK session object.
|
||||
* @type {XXX}
|
||||
*/
|
||||
session: undefined,
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Options:
|
||||
* - {OT} mozLoop: browser mozLoop service object.
|
||||
*
|
||||
* Required:
|
||||
* - {OT} sdk: OT SDK object.
|
||||
*
|
||||
* @param {Object} attributes Attributes object.
|
||||
* @param {Object} options Options object.
|
||||
*/
|
||||
initialize: function(attributes, options) {
|
||||
options = options || {};
|
||||
this.mozLoop = options.mozLoop;
|
||||
if (!options.sdk) {
|
||||
throw new Error("missing required sdk");
|
||||
}
|
||||
this.sdk = options.sdk;
|
||||
|
||||
// Set loop.debug.sdk to true in the browser, or standalone:
|
||||
// localStorage.setItem("debug.sdk", true);
|
||||
if (loop.shared.utils.getBoolPreference("debug.sdk")) {
|
||||
this.sdk.setLogLevel(this.sdk.DEBUG);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates an incoming conversation has been accepted.
|
||||
*/
|
||||
accepted: function() {
|
||||
this.trigger("call:accepted");
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to indicate that an outgoing call should start any necessary
|
||||
* set-up.
|
||||
*
|
||||
* @param {String} selectedCallType Call type ("audio" or "audio-video")
|
||||
*/
|
||||
setupOutgoingCall: function(selectedCallType) {
|
||||
if (selectedCallType) {
|
||||
this.set("selectedCallType", selectedCallType);
|
||||
}
|
||||
this.trigger("call:outgoing:get-media-privs");
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to indicate that media privileges have been accepted.
|
||||
*/
|
||||
gotMediaPrivs: function() {
|
||||
this.trigger("call:outgoing:setup");
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts an outgoing conversation.
|
||||
*
|
||||
* @param {Object} sessionData The session data received from the
|
||||
* server for the outgoing call.
|
||||
*/
|
||||
outgoing: function(sessionData) {
|
||||
this.setOutgoingSessionData(sessionData);
|
||||
this.trigger("call:outgoing");
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the session is ready.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isSessionReady: function() {
|
||||
return !!this.get("sessionId");
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets session information.
|
||||
* Session data received by creating an outgoing call.
|
||||
*
|
||||
* @param {Object} sessionData Conversation session information.
|
||||
*/
|
||||
setOutgoingSessionData: function(sessionData) {
|
||||
// Explicit property assignment to prevent later "surprises"
|
||||
this.set({
|
||||
sessionId: sessionData.sessionId,
|
||||
sessionToken: sessionData.sessionToken,
|
||||
apiKey: sessionData.apiKey,
|
||||
callId: sessionData.callId,
|
||||
progressURL: sessionData.progressURL,
|
||||
websocketToken: sessionData.websocketToken.toString(16)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets session information about the incoming call.
|
||||
*
|
||||
* @param {Object} sessionData Conversation session information.
|
||||
*/
|
||||
setIncomingSessionData: function(sessionData) {
|
||||
// Explicit property assignment to prevent later "surprises"
|
||||
this.set({
|
||||
sessionId: sessionData.sessionId,
|
||||
sessionToken: sessionData.sessionToken,
|
||||
sessionType: sessionData.sessionType,
|
||||
apiKey: sessionData.apiKey,
|
||||
callId: sessionData.callId,
|
||||
callerId: sessionData.callerId,
|
||||
urlCreationDate: sessionData.urlCreationDate,
|
||||
progressURL: sessionData.progressURL,
|
||||
websocketToken: sessionData.websocketToken.toString(16),
|
||||
callType: sessionData.callType || "audio-video",
|
||||
callToken: sessionData.callToken,
|
||||
callUrl: sessionData.callUrl
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts a SDK session and subscribe to call events.
|
||||
*/
|
||||
startSession: function() {
|
||||
if (!this.isSessionReady()) {
|
||||
throw new Error("Can't start session as it's not ready");
|
||||
}
|
||||
this.set({
|
||||
publishedStream: false,
|
||||
subscribedStream: false
|
||||
});
|
||||
|
||||
this.session = this.sdk.initSession(this.get("sessionId"));
|
||||
this.listenTo(this.session, "streamCreated", this._streamCreated);
|
||||
this.listenTo(this.session, "connectionDestroyed",
|
||||
this._connectionDestroyed);
|
||||
this.listenTo(this.session, "sessionDisconnected",
|
||||
this._sessionDisconnected);
|
||||
this.session.connect(this.get("apiKey"), this.get("sessionToken"),
|
||||
this._onConnectCompletion.bind(this));
|
||||
|
||||
// We store the call credentials for debugging purposes.
|
||||
if (this.mozLoop) {
|
||||
this.mozLoop.addConversationContext(this.get("windowId"),
|
||||
this.get("sessionId"),
|
||||
this.get("callId"));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Ends current session.
|
||||
*/
|
||||
endSession: function() {
|
||||
this.session.disconnect();
|
||||
this.set({
|
||||
publishedStream: false,
|
||||
subscribedStream: false,
|
||||
ongoing: false
|
||||
}).once("session:ended", this.stopListening, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to determine if video stream is available for the
|
||||
* incoming or outgoing call
|
||||
*
|
||||
* @param {string} callType Incoming or outgoing call
|
||||
*/
|
||||
hasVideoStream: function(callType) {
|
||||
if (callType === "incoming") {
|
||||
return this.get("callType") === "audio-video";
|
||||
}
|
||||
if (callType === "outgoing") {
|
||||
return this.get("selectedCallType") === "audio-video";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to remove the scheme from a url.
|
||||
*/
|
||||
_removeScheme: function(url) {
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
return url.replace(/^https?:\/\//, "");
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a conversation identifier for the incoming call view
|
||||
*/
|
||||
getCallIdentifier: function() {
|
||||
return this.get("callerId") || this._removeScheme(this.get("callUrl"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Publishes a local stream.
|
||||
*
|
||||
* @param {Publisher} publisher The publisher object to publish
|
||||
* to the session.
|
||||
*/
|
||||
publish: function(publisher) {
|
||||
this.session.publish(publisher);
|
||||
this.set("publishedStream", true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribes to a remote stream.
|
||||
*
|
||||
* @param {Stream} stream The remote stream to subscribe to.
|
||||
* @param {DOMElement} element The element to display the stream in.
|
||||
* @param {Object} config The display properties to set on the stream as
|
||||
* documented in:
|
||||
* https://tokbox.com/opentok/libraries/client/js/reference/Session.html#subscribe
|
||||
*/
|
||||
subscribe: function(stream, element, config) {
|
||||
this.session.subscribe(stream, element, config);
|
||||
this.set("subscribedStream", true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if a stream has been published and a stream has been
|
||||
* subscribed to.
|
||||
*/
|
||||
streamsConnected: function() {
|
||||
return this.get("publishedStream") && this.get("subscribedStream");
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a loop-server error, which has an optional `errno` property which
|
||||
* is server error identifier.
|
||||
*
|
||||
* Triggers the following events:
|
||||
*
|
||||
* - `session:expired` for expired call urls
|
||||
* - `session:error` for other generic errors
|
||||
*
|
||||
* @param {Error} err Error object.
|
||||
*/
|
||||
_handleServerError: function(err) {
|
||||
switch (err.errno) {
|
||||
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
|
||||
// missing OR expired; we treat this information as if the url is always
|
||||
// expired.
|
||||
case 105:
|
||||
this.trigger("session:expired", err);
|
||||
break;
|
||||
default:
|
||||
this.trigger("session:error", err);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Manages connection status
|
||||
* triggers apropriate event for connection error/success
|
||||
* http://tokbox.com/opentok/tutorials/connect-session/js/
|
||||
* http://tokbox.com/opentok/tutorials/hello-world/js/
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
|
||||
*
|
||||
* @param {error|null} error
|
||||
*/
|
||||
_onConnectCompletion: function(error) {
|
||||
if (error) {
|
||||
this.trigger("session:connection-error", error);
|
||||
this.endSession();
|
||||
} else {
|
||||
this.trigger("session:connected");
|
||||
this.set("connected", true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* New created streams are available.
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
|
||||
*
|
||||
* @param {StreamEvent} event
|
||||
*/
|
||||
_streamCreated: function(event) {
|
||||
this.set("ongoing", true)
|
||||
.trigger("session:stream-created", event);
|
||||
},
|
||||
|
||||
/**
|
||||
* Local user hung up.
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
|
||||
*
|
||||
* @param {SessionDisconnectEvent} event
|
||||
*/
|
||||
_sessionDisconnected: function(event) {
|
||||
if(event.reason === "networkDisconnected") {
|
||||
this._signalEnd("session:network-disconnected", event);
|
||||
} else {
|
||||
this._signalEnd("session:ended", event);
|
||||
}
|
||||
},
|
||||
|
||||
_signalEnd: function(eventName, event) {
|
||||
this.set("connected", false)
|
||||
.set("ongoing", false)
|
||||
.trigger(eventName, event);
|
||||
},
|
||||
|
||||
/**
|
||||
* Peer hung up. Disconnects local session.
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
|
||||
*
|
||||
* @param {ConnectionEvent} event
|
||||
*/
|
||||
_connectionDestroyed: function(event) {
|
||||
if (event.reason === "networkDisconnected") {
|
||||
this._signalEnd("session:network-disconnected", event);
|
||||
} else {
|
||||
this._signalEnd("session:peer-hungup", event);
|
||||
}
|
||||
this.endSession();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification model.
|
||||
*/
|
||||
@ -445,7 +88,6 @@ loop.shared.models = (function(l10n) {
|
||||
});
|
||||
|
||||
return {
|
||||
ConversationModel: ConversationModel,
|
||||
NotificationCollection: NotificationCollection,
|
||||
NotificationModel: NotificationModel
|
||||
};
|
||||
|
@ -485,232 +485,6 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation view.
|
||||
*/
|
||||
var ConversationView = React.createClass({displayName: "ConversationView",
|
||||
mixins: [
|
||||
Backbone.Events,
|
||||
sharedMixins.AudioMixin,
|
||||
sharedMixins.MediaSetupMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
audio: React.PropTypes.object,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
initiate: React.PropTypes.bool,
|
||||
isDesktop: React.PropTypes.bool,
|
||||
model: React.PropTypes.object.isRequired,
|
||||
mozLoop: React.PropTypes.object,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
video: React.PropTypes.object
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
initiate: true,
|
||||
isDesktop: false,
|
||||
video: {enabled: true, visible: true},
|
||||
audio: {enabled: true, visible: true}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
video: this.props.video,
|
||||
audio: this.props.audio
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.initiate) {
|
||||
/**
|
||||
* XXX This is a workaround for desktop machines that do not have a
|
||||
* camera installed. As we don't yet have device enumeration, when
|
||||
* we do, this can be removed (bug 1138851), and the sdk should handle it.
|
||||
*/
|
||||
if (this.props.isDesktop &&
|
||||
!window.MediaStreamTrack.getSources) {
|
||||
// If there's no getSources function, the sdk defines its own and caches
|
||||
// the result. So here we define the "normal" one which doesn't get cached, so
|
||||
// we can change it later.
|
||||
window.MediaStreamTrack.getSources = function(callback) {
|
||||
callback([{kind: "audio"}, {kind: "video"}]);
|
||||
};
|
||||
}
|
||||
|
||||
this.listenTo(this.props.sdk, "exception", this._handleSdkException);
|
||||
|
||||
this.listenTo(this.props.model, "session:connected",
|
||||
this._onSessionConnected);
|
||||
this.listenTo(this.props.model, "session:stream-created",
|
||||
this._streamCreated);
|
||||
this.listenTo(this.props.model, ["session:peer-hungup",
|
||||
"session:network-disconnected",
|
||||
"session:ended"].join(" "),
|
||||
this.stopPublishing);
|
||||
this.props.model.startSession();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// Unregister all local event listeners
|
||||
this.stopListening();
|
||||
this.hangup();
|
||||
},
|
||||
|
||||
hangup: function() {
|
||||
this.stopPublishing();
|
||||
this.props.model.endSession();
|
||||
},
|
||||
|
||||
_onSessionConnected: function(event) {
|
||||
this.startPublishing(event);
|
||||
this.play("connected");
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribes and attaches each created stream to a DOM element.
|
||||
*
|
||||
* XXX: for now we only support a single remote stream, hence a single DOM
|
||||
* element.
|
||||
*
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
|
||||
*
|
||||
* @param {StreamEvent} event
|
||||
*/
|
||||
_streamCreated: function(event) {
|
||||
var incoming = this.getDOMNode().querySelector(".remote");
|
||||
this.props.model.subscribe(event.stream, incoming,
|
||||
this.getDefaultPublisherConfig({
|
||||
publishVideo: this.props.video.enabled
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the SDK Exception event.
|
||||
*
|
||||
* https://tokbox.com/opentok/libraries/client/js/reference/ExceptionEvent.html
|
||||
*
|
||||
* @param {ExceptionEvent} event
|
||||
*/
|
||||
_handleSdkException: function(event) {
|
||||
/**
|
||||
* XXX This is a workaround for desktop machines that do not have a
|
||||
* camera installed. As we don't yet have device enumeration, when
|
||||
* we do, this can be removed (bug 1138851), and the sdk should handle it.
|
||||
*/
|
||||
if (this.publisher &&
|
||||
event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH &&
|
||||
event.message === "GetUserMedia" &&
|
||||
this.state.video.enabled) {
|
||||
this.state.video.enabled = false;
|
||||
|
||||
window.MediaStreamTrack.getSources = function(callback) {
|
||||
callback([{kind: "audio"}]);
|
||||
};
|
||||
|
||||
this.stopListening(this.publisher);
|
||||
this.publisher.destroy();
|
||||
this.startPublishing();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Publishes remote streams available once a session is connected.
|
||||
*
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
|
||||
*
|
||||
* @param {SessionConnectEvent} event
|
||||
*/
|
||||
startPublishing: function(event) {
|
||||
var outgoing = this.getDOMNode().querySelector(".local");
|
||||
|
||||
// XXX move this into its StreamingVideo component?
|
||||
this.publisher = this.props.sdk.initPublisher(
|
||||
outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
|
||||
|
||||
// Suppress OT GuM custom dialog, see bug 1018875
|
||||
this.listenTo(this.publisher, "accessDialogOpened accessDenied",
|
||||
function(ev) {
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
this.listenTo(this.publisher, "streamCreated", function(ev) {
|
||||
this.setState({
|
||||
audio: {enabled: ev.stream.hasAudio},
|
||||
video: {enabled: ev.stream.hasVideo}
|
||||
});
|
||||
});
|
||||
|
||||
this.listenTo(this.publisher, "streamDestroyed", function() {
|
||||
this.setState({
|
||||
audio: {enabled: false},
|
||||
video: {enabled: false}
|
||||
});
|
||||
});
|
||||
|
||||
this.props.model.publish(this.publisher);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles streaming status for a given stream type.
|
||||
*
|
||||
* @param {String} type Stream type ("audio" or "video").
|
||||
* @param {Boolean} enabled Enabled stream flag.
|
||||
*/
|
||||
publishStream: function(type, enabled) {
|
||||
if (type === "audio") {
|
||||
this.publisher.publishAudio(enabled);
|
||||
this.setState({audio: {enabled: enabled}});
|
||||
} else {
|
||||
this.publisher.publishVideo(enabled);
|
||||
this.setState({video: {enabled: enabled}});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unpublishes local stream.
|
||||
*/
|
||||
stopPublishing: function() {
|
||||
if (this.publisher) {
|
||||
// Unregister listeners for publisher events
|
||||
this.stopListening(this.publisher);
|
||||
|
||||
this.props.model.session.unpublish(this.publisher);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
local: true,
|
||||
"local-stream": true,
|
||||
"local-stream-audio": !this.state.video.enabled
|
||||
});
|
||||
return (
|
||||
React.createElement("div", {className: "video-layout-wrapper"},
|
||||
React.createElement("div", {className: "conversation in-call"},
|
||||
React.createElement("div", {className: "media nested"},
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: "video_inner remote focus-stream"},
|
||||
React.createElement(ConversationToolbar, {
|
||||
audio: this.state.audio,
|
||||
dispatcher: this.props.dispatcher,
|
||||
hangup: this.hangup,
|
||||
mozLoop: this.props.mozLoop,
|
||||
publishStream: this.publishStream,
|
||||
video: this.state.video})
|
||||
)
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses})
|
||||
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification view.
|
||||
*/
|
||||
@ -1322,7 +1096,6 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
ButtonGroup: ButtonGroup,
|
||||
Checkbox: Checkbox,
|
||||
ContextUrlView: ContextUrlView,
|
||||
ConversationView: ConversationView,
|
||||
ConversationToolbar: ConversationToolbar,
|
||||
MediaControlButton: MediaControlButton,
|
||||
MediaLayoutView: MediaLayoutView,
|
||||
|
@ -485,232 +485,6 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation view.
|
||||
*/
|
||||
var ConversationView = React.createClass({
|
||||
mixins: [
|
||||
Backbone.Events,
|
||||
sharedMixins.AudioMixin,
|
||||
sharedMixins.MediaSetupMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
audio: React.PropTypes.object,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
initiate: React.PropTypes.bool,
|
||||
isDesktop: React.PropTypes.bool,
|
||||
model: React.PropTypes.object.isRequired,
|
||||
mozLoop: React.PropTypes.object,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
video: React.PropTypes.object
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
initiate: true,
|
||||
isDesktop: false,
|
||||
video: {enabled: true, visible: true},
|
||||
audio: {enabled: true, visible: true}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
video: this.props.video,
|
||||
audio: this.props.audio
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if (this.props.initiate) {
|
||||
/**
|
||||
* XXX This is a workaround for desktop machines that do not have a
|
||||
* camera installed. As we don't yet have device enumeration, when
|
||||
* we do, this can be removed (bug 1138851), and the sdk should handle it.
|
||||
*/
|
||||
if (this.props.isDesktop &&
|
||||
!window.MediaStreamTrack.getSources) {
|
||||
// If there's no getSources function, the sdk defines its own and caches
|
||||
// the result. So here we define the "normal" one which doesn't get cached, so
|
||||
// we can change it later.
|
||||
window.MediaStreamTrack.getSources = function(callback) {
|
||||
callback([{kind: "audio"}, {kind: "video"}]);
|
||||
};
|
||||
}
|
||||
|
||||
this.listenTo(this.props.sdk, "exception", this._handleSdkException);
|
||||
|
||||
this.listenTo(this.props.model, "session:connected",
|
||||
this._onSessionConnected);
|
||||
this.listenTo(this.props.model, "session:stream-created",
|
||||
this._streamCreated);
|
||||
this.listenTo(this.props.model, ["session:peer-hungup",
|
||||
"session:network-disconnected",
|
||||
"session:ended"].join(" "),
|
||||
this.stopPublishing);
|
||||
this.props.model.startSession();
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
// Unregister all local event listeners
|
||||
this.stopListening();
|
||||
this.hangup();
|
||||
},
|
||||
|
||||
hangup: function() {
|
||||
this.stopPublishing();
|
||||
this.props.model.endSession();
|
||||
},
|
||||
|
||||
_onSessionConnected: function(event) {
|
||||
this.startPublishing(event);
|
||||
this.play("connected");
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribes and attaches each created stream to a DOM element.
|
||||
*
|
||||
* XXX: for now we only support a single remote stream, hence a single DOM
|
||||
* element.
|
||||
*
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
|
||||
*
|
||||
* @param {StreamEvent} event
|
||||
*/
|
||||
_streamCreated: function(event) {
|
||||
var incoming = this.getDOMNode().querySelector(".remote");
|
||||
this.props.model.subscribe(event.stream, incoming,
|
||||
this.getDefaultPublisherConfig({
|
||||
publishVideo: this.props.video.enabled
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the SDK Exception event.
|
||||
*
|
||||
* https://tokbox.com/opentok/libraries/client/js/reference/ExceptionEvent.html
|
||||
*
|
||||
* @param {ExceptionEvent} event
|
||||
*/
|
||||
_handleSdkException: function(event) {
|
||||
/**
|
||||
* XXX This is a workaround for desktop machines that do not have a
|
||||
* camera installed. As we don't yet have device enumeration, when
|
||||
* we do, this can be removed (bug 1138851), and the sdk should handle it.
|
||||
*/
|
||||
if (this.publisher &&
|
||||
event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH &&
|
||||
event.message === "GetUserMedia" &&
|
||||
this.state.video.enabled) {
|
||||
this.state.video.enabled = false;
|
||||
|
||||
window.MediaStreamTrack.getSources = function(callback) {
|
||||
callback([{kind: "audio"}]);
|
||||
};
|
||||
|
||||
this.stopListening(this.publisher);
|
||||
this.publisher.destroy();
|
||||
this.startPublishing();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Publishes remote streams available once a session is connected.
|
||||
*
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
|
||||
*
|
||||
* @param {SessionConnectEvent} event
|
||||
*/
|
||||
startPublishing: function(event) {
|
||||
var outgoing = this.getDOMNode().querySelector(".local");
|
||||
|
||||
// XXX move this into its StreamingVideo component?
|
||||
this.publisher = this.props.sdk.initPublisher(
|
||||
outgoing, this.getDefaultPublisherConfig({publishVideo: this.props.video.enabled}));
|
||||
|
||||
// Suppress OT GuM custom dialog, see bug 1018875
|
||||
this.listenTo(this.publisher, "accessDialogOpened accessDenied",
|
||||
function(ev) {
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
this.listenTo(this.publisher, "streamCreated", function(ev) {
|
||||
this.setState({
|
||||
audio: {enabled: ev.stream.hasAudio},
|
||||
video: {enabled: ev.stream.hasVideo}
|
||||
});
|
||||
});
|
||||
|
||||
this.listenTo(this.publisher, "streamDestroyed", function() {
|
||||
this.setState({
|
||||
audio: {enabled: false},
|
||||
video: {enabled: false}
|
||||
});
|
||||
});
|
||||
|
||||
this.props.model.publish(this.publisher);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles streaming status for a given stream type.
|
||||
*
|
||||
* @param {String} type Stream type ("audio" or "video").
|
||||
* @param {Boolean} enabled Enabled stream flag.
|
||||
*/
|
||||
publishStream: function(type, enabled) {
|
||||
if (type === "audio") {
|
||||
this.publisher.publishAudio(enabled);
|
||||
this.setState({audio: {enabled: enabled}});
|
||||
} else {
|
||||
this.publisher.publishVideo(enabled);
|
||||
this.setState({video: {enabled: enabled}});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Unpublishes local stream.
|
||||
*/
|
||||
stopPublishing: function() {
|
||||
if (this.publisher) {
|
||||
// Unregister listeners for publisher events
|
||||
this.stopListening(this.publisher);
|
||||
|
||||
this.props.model.session.unpublish(this.publisher);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
local: true,
|
||||
"local-stream": true,
|
||||
"local-stream-audio": !this.state.video.enabled
|
||||
});
|
||||
return (
|
||||
<div className="video-layout-wrapper">
|
||||
<div className="conversation in-call">
|
||||
<div className="media nested">
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className="video_inner remote focus-stream">
|
||||
<ConversationToolbar
|
||||
audio={this.state.audio}
|
||||
dispatcher={this.props.dispatcher}
|
||||
hangup={this.hangup}
|
||||
mozLoop={this.props.mozLoop}
|
||||
publishStream={this.publishStream}
|
||||
video={this.state.video} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={localStreamClasses}></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification view.
|
||||
*/
|
||||
@ -1322,7 +1096,6 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
ButtonGroup: ButtonGroup,
|
||||
Checkbox: Checkbox,
|
||||
ContextUrlView: ContextUrlView,
|
||||
ConversationView: ConversationView,
|
||||
ConversationToolbar: ConversationToolbar,
|
||||
MediaControlButton: MediaControlButton,
|
||||
MediaLayoutView: MediaLayoutView,
|
||||
|
@ -120,16 +120,7 @@
|
||||
// We don't use the SDK's CSS. This will prevent spurious 404 errors.
|
||||
window.OTProperties.cssURL = "about:blank";
|
||||
</script>
|
||||
<script type="text/javascript" src="js/multiplexGum.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/sdk.js"></script>
|
||||
<script>
|
||||
// multiplexGum needs evaluation before sdk.js, but TBPlugin is not
|
||||
// defined until after sdk.js has been evaluated. This updates the
|
||||
// navigator object to reference TBPlugin if it was defined by sdk.js.
|
||||
if (!navigator.originalGum) {
|
||||
navigator.originalGum = (window.TBPlugin && window.TBPlugin.getUserMedia);
|
||||
}
|
||||
</script>
|
||||
<script type="text/javascript" src="libs/l10n-gaia-02ca67948fe8.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/react-0.12.2.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/lodash-3.9.3.js"></script>
|
||||
@ -139,7 +130,6 @@
|
||||
<script type="text/javascript" src="config.js"></script>
|
||||
<script type="text/javascript" src="shared/js/utils.js"></script>
|
||||
<script type="text/javascript" src="shared/js/crypto.js"></script>
|
||||
<script type="text/javascript" src="shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="shared/js/mixins.js"></script>
|
||||
<script type="text/javascript" src="shared/js/actions.js"></script>
|
||||
<script type="text/javascript" src="shared/js/validate.js"></script>
|
||||
@ -154,7 +144,6 @@
|
||||
<script type="text/javascript" src="shared/js/urlRegExps.js"></script>
|
||||
<script type="text/javascript" src="shared/js/linkifiedTextView.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneClient.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
|
||||
<script type="text/javascript" src="js/standaloneMetricsStore.js"></script>
|
||||
|
@ -1,149 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
var loop = loop || {};
|
||||
|
||||
/**
|
||||
* Monkeypatch getUserMedia in a way that prevents additional camera and
|
||||
* microphone prompts, at the cost of ignoring all constraints other than
|
||||
* the first set passed in.
|
||||
*
|
||||
* The first call to navigator.getUserMedia (also now aliased to
|
||||
* multiplexGum.getPermsAndCacheMedia to allow for explicit calling code)
|
||||
* will cause the underlying gUM implementation to be called.
|
||||
*
|
||||
* While permission is pending, subsequent calls will result in the callbacks
|
||||
* being queued. Once the call succeeds or fails, all queued success or
|
||||
* failure callbacks will be invoked. Subsequent calls to either function will
|
||||
* cause the success or failure callback to be invoked immediately.
|
||||
*/
|
||||
loop.standaloneMedia = (function() {
|
||||
"use strict";
|
||||
|
||||
function patchSymbolIfExtant(objectName, propertyName, replacement) {
|
||||
var object;
|
||||
if (window[objectName]) {
|
||||
object = window[objectName];
|
||||
}
|
||||
if (object && object[propertyName]) {
|
||||
object[propertyName] = replacement;
|
||||
}
|
||||
}
|
||||
|
||||
// originalGum _must_ be on navigator; otherwise things blow up.
|
||||
// For TBPlugin users, navigator.originalGum is set after the TB SDK is loaded.
|
||||
navigator.originalGum = navigator.getUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.webkitGetUserMedia;
|
||||
|
||||
function _MultiplexGum() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
_MultiplexGum.prototype = {
|
||||
/**
|
||||
* @see The docs at the top of this file for overall semantics,
|
||||
* & http://developer.mozilla.org/en-US/docs/NavigatorUserMedia.getUserMedia
|
||||
* for params, since this is intended to be purely a passthrough to gUM.
|
||||
*/
|
||||
getPermsAndCacheMedia: function(constraints, onSuccess, onError) {
|
||||
function handleResult(callbacks, param) {
|
||||
// Operate on a copy of the array in case any of the callbacks
|
||||
// calls reset, which would cause an infinite-recursion.
|
||||
this.userMedia.successCallbacks = [];
|
||||
this.userMedia.errorCallbacks = [];
|
||||
callbacks.forEach(function(cb) {
|
||||
if (typeof cb == "function") {
|
||||
cb(param);
|
||||
}
|
||||
});
|
||||
}
|
||||
function handleSuccess(localStream) {
|
||||
this.userMedia.pending = false;
|
||||
this.userMedia.localStream = localStream;
|
||||
this.userMedia.error = null;
|
||||
handleResult.call(this, this.userMedia.successCallbacks.slice(0), localStream);
|
||||
}
|
||||
|
||||
function handleError(error) {
|
||||
this.userMedia.pending = false;
|
||||
this.userMedia.error = error;
|
||||
handleResult.call(this, this.userMedia.errorCallbacks.slice(0), error);
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
if (this.userMedia.localStream &&
|
||||
this.userMedia.localStream.ended) {
|
||||
this.userMedia.localStream = null;
|
||||
}
|
||||
|
||||
this.userMedia.errorCallbacks.push(onError);
|
||||
this.userMedia.successCallbacks.push(onSuccess);
|
||||
|
||||
if (this.userMedia.localStream) {
|
||||
handleSuccess.call(this, this.userMedia.localStream);
|
||||
return;
|
||||
} else if (this.userMedia.error) {
|
||||
handleError.call(this, this.userMedia.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.userMedia.pending) {
|
||||
return;
|
||||
}
|
||||
this.userMedia.pending = true;
|
||||
|
||||
navigator.originalGum(constraints, handleSuccess.bind(this),
|
||||
handleError.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the cached permissions, callbacks, and media to their default
|
||||
* state and call any error callbacks to let any waiting callers know
|
||||
* not to ever expect any more callbacks. We use "PERMISSION_DENIED",
|
||||
* for lack of a better, more specific gUM code that callers are likely
|
||||
* to be prepared to handle.
|
||||
*/
|
||||
reset: function() {
|
||||
// When called from the ctor, userMedia is not created yet.
|
||||
if (this.userMedia) {
|
||||
this.userMedia.errorCallbacks.forEach(function(cb) {
|
||||
if (typeof cb == "function") {
|
||||
cb("PERMISSION_DENIED");
|
||||
}
|
||||
});
|
||||
if (this.userMedia.localStream &&
|
||||
typeof this.userMedia.localStream.stop == "function") {
|
||||
this.userMedia.localStream.stop();
|
||||
}
|
||||
}
|
||||
this.userMedia = {
|
||||
error: null,
|
||||
localStream: null,
|
||||
pending: false,
|
||||
errorCallbacks: [],
|
||||
successCallbacks: []
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var singletonMultiplexGum = new _MultiplexGum();
|
||||
function myGetUserMedia() {
|
||||
// This function is needed to pull in the instance
|
||||
// of the singleton for tests to overwrite the used instance.
|
||||
singletonMultiplexGum.getPermsAndCacheMedia.apply(singletonMultiplexGum, arguments);
|
||||
}
|
||||
patchSymbolIfExtant("navigator", "mozGetUserMedia", myGetUserMedia);
|
||||
patchSymbolIfExtant("navigator", "webkitGetUserMedia", myGetUserMedia);
|
||||
patchSymbolIfExtant("navigator", "getUserMedia", myGetUserMedia);
|
||||
patchSymbolIfExtant("TBPlugin", "getUserMedia", myGetUserMedia);
|
||||
|
||||
return {
|
||||
multiplexGum: singletonMultiplexGum,
|
||||
_MultiplexGum: _MultiplexGum,
|
||||
setSingleton: function(singleton) {
|
||||
singletonMultiplexGum = singleton;
|
||||
}
|
||||
};
|
||||
})();
|
@ -6,7 +6,7 @@ var loop = loop || {};
|
||||
loop.store = loop.store || {};
|
||||
|
||||
/**
|
||||
* Manages the conversation window app controller view. Used to get
|
||||
* Manages the standalone app controller view. Used to get
|
||||
* the window data and store the window type.
|
||||
*/
|
||||
loop.store.StandaloneAppStore = (function() {
|
||||
@ -31,14 +31,10 @@ loop.store.StandaloneAppStore = (function() {
|
||||
if (!options.sdk) {
|
||||
throw new Error("Missing option sdk");
|
||||
}
|
||||
if (!options.conversation) {
|
||||
throw new Error("Missing option conversation");
|
||||
}
|
||||
|
||||
this._dispatcher = options.dispatcher;
|
||||
this._storeState = {};
|
||||
this._sdk = options.sdk;
|
||||
this._conversation = options.conversation;
|
||||
|
||||
this._dispatcher.register(this, [
|
||||
"extractTokenInfo"
|
||||
@ -132,10 +128,6 @@ loop.store.StandaloneAppStore = (function() {
|
||||
}
|
||||
// Else type is home.
|
||||
|
||||
if (token) {
|
||||
this._conversation.set({loopToken: token});
|
||||
}
|
||||
|
||||
this.setStoreState({
|
||||
windowType: windowType,
|
||||
isFirefox: sharedUtils.isFirefox(navigator.userAgent),
|
||||
|
@ -1,155 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.StandaloneClient = (function() {
|
||||
"use strict";
|
||||
|
||||
// The expected properties to be returned from the POST /calls request.
|
||||
var expectedCallsProperties = [ "sessionId", "sessionToken", "apiKey" ];
|
||||
|
||||
/**
|
||||
* Loop server standalone client.
|
||||
*
|
||||
* @param {Object} settings Settings object.
|
||||
*/
|
||||
function StandaloneClient(settings) {
|
||||
settings = settings || {};
|
||||
if (!settings.baseServerUrl) {
|
||||
throw new Error("missing required baseServerUrl");
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
StandaloneClient.prototype = {
|
||||
/**
|
||||
* Validates a data object to confirm it has the specified properties.
|
||||
*
|
||||
* @param {Object} data The data object to verify
|
||||
* @param {Array} properties The list of properties to verify within the object
|
||||
* @return This returns either the specific property if only one
|
||||
* property is specified, or it returns all properties
|
||||
*/
|
||||
_validate: function(data, properties) {
|
||||
if (typeof data !== "object") {
|
||||
throw new Error("Invalid data received from server");
|
||||
}
|
||||
|
||||
properties.forEach(function (property) {
|
||||
if (!data.hasOwnProperty(property)) {
|
||||
throw new Error("Invalid data received from server - missing " +
|
||||
property);
|
||||
}
|
||||
});
|
||||
|
||||
if (properties.length === 1) {
|
||||
return data[properties[0]];
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generic handler for XHR failures.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* @param xhrReq
|
||||
*/
|
||||
_failureHandler: function(cb, xhrReq) {
|
||||
var jsonErr = JSON.parse(xhrReq.responseText && xhrReq.responseText || "{}");
|
||||
var message = "HTTP " + xhrReq.status + " " + xhrReq.statusText;
|
||||
|
||||
// Logging the technical error to the console
|
||||
console.error("Server error", message, jsonErr);
|
||||
|
||||
// Create an error with server error `errno` code attached as a property
|
||||
var err = new Error(message);
|
||||
err.errno = jsonErr.errno;
|
||||
|
||||
cb(err);
|
||||
},
|
||||
|
||||
/**
|
||||
* Makes a request for url creation date for standalone UI
|
||||
*
|
||||
* @param {String} loopToken The loopToken representing the call
|
||||
* @param {Function} cb Callback(err, callUrlInfo)
|
||||
*
|
||||
**/
|
||||
requestCallUrlInfo: function(loopToken, cb) {
|
||||
if (!loopToken) {
|
||||
throw new Error("Missing required parameter loopToken");
|
||||
}
|
||||
if (!cb) {
|
||||
throw new Error("Missing required callback function");
|
||||
}
|
||||
|
||||
var url = this.settings.baseServerUrl + "/calls/" + loopToken;
|
||||
var xhrReq = new XMLHttpRequest();
|
||||
|
||||
xhrReq.open("GET", url, true);
|
||||
xhrReq.setRequestHeader("Content-type", "application/json");
|
||||
|
||||
xhrReq.onload = function() {
|
||||
var request = xhrReq;
|
||||
var responseJSON = JSON.parse(request.responseText || null);
|
||||
|
||||
if (request.readyState === 4 && request.status >= 200 && request.status < 300) {
|
||||
try {
|
||||
cb(null, responseJSON);
|
||||
} catch (err) {
|
||||
console.error("Error requesting call info", err.message);
|
||||
cb(err);
|
||||
}
|
||||
} else {
|
||||
this._failureHandler(cb, request);
|
||||
}
|
||||
}.bind(this, xhrReq);
|
||||
|
||||
xhrReq.send();
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts a call request to the server for a call represented by the
|
||||
* loopToken. Will return the session data for the call.
|
||||
*
|
||||
* @param {String} loopToken The loopToken representing the call
|
||||
* @param {String} callType The type of media in the call, e.g.
|
||||
* "audio" or "audio-video"
|
||||
* @param {Function} cb Callback(err, sessionData)
|
||||
*/
|
||||
requestCallInfo: function(loopToken, callType, cb) {
|
||||
if (!loopToken) {
|
||||
throw new Error("missing required parameter loopToken");
|
||||
}
|
||||
|
||||
var url = this.settings.baseServerUrl + "/calls/" + loopToken;
|
||||
var xhrReq = new XMLHttpRequest();
|
||||
|
||||
xhrReq.open("POST", url, true);
|
||||
xhrReq.setRequestHeader("Content-type", "application/json");
|
||||
|
||||
xhrReq.onload = function() {
|
||||
var request = xhrReq;
|
||||
var responseJSON = JSON.parse(request.responseText || null);
|
||||
|
||||
if (request.readyState === 4 && request.status >= 200 && request.status < 300) {
|
||||
try {
|
||||
cb(null, this._validate(responseJSON, expectedCallsProperties));
|
||||
} catch (err) {
|
||||
console.error("Error requesting call info", err.message);
|
||||
cb(err);
|
||||
}
|
||||
} else {
|
||||
this._failureHandler(cb, request);
|
||||
}
|
||||
}.bind(this, xhrReq);
|
||||
|
||||
xhrReq.send(JSON.stringify({callType: callType, channel: "standalone"}));
|
||||
}
|
||||
};
|
||||
|
||||
return StandaloneClient;
|
||||
})();
|
@ -16,14 +16,11 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
var sharedUtils = loop.shared.utils;
|
||||
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
|
||||
|
||||
var multiplexGum = loop.standaloneMedia.multiplexGum;
|
||||
|
||||
/**
|
||||
* Homepage view.
|
||||
*/
|
||||
var HomeView = React.createClass({displayName: "HomeView",
|
||||
render: function() {
|
||||
multiplexGum.reset();
|
||||
return (
|
||||
React.createElement("p", null, mozL10n.get("welcome", {clientShortname: mozL10n.get("clientShortname2")}))
|
||||
);
|
||||
@ -111,732 +108,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Expired call URL view.
|
||||
*/
|
||||
var CallUrlExpiredView = React.createClass({displayName: "CallUrlExpiredView",
|
||||
propTypes: {
|
||||
isFirefox: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("div", {className: "highlight-issue-box"},
|
||||
React.createElement("div", {className: "info-panel"},
|
||||
React.createElement("div", {className: "firefox-logo"}),
|
||||
React.createElement("h1", null, mozL10n.get("call_url_unavailable_notification_heading")),
|
||||
React.createElement("h4", null, mozL10n.get("call_url_unavailable_notification_message2"))
|
||||
),
|
||||
React.createElement(PromoteFirefoxView, {isFirefox: this.props.isFirefox})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationBranding = React.createClass({displayName: "ConversationBranding",
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("h1", {className: "standalone-header-title"},
|
||||
React.createElement("strong", null, mozL10n.get("clientShortname2"))
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationHeader = React.createClass({displayName: "ConversationHeader",
|
||||
propTypes: {
|
||||
urlCreationDateString: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
var conversationUrl = location.href;
|
||||
|
||||
var urlCreationDateClasses = cx({
|
||||
"light-color-font": true,
|
||||
"call-url-date": true, /* Used as a handler in the tests */
|
||||
// Hidden until date is available.
|
||||
"hide": !this.props.urlCreationDateString.length
|
||||
});
|
||||
|
||||
var callUrlCreationDateString = mozL10n.get("call_url_creation_date_label", {
|
||||
"call_url_creation_date": this.props.urlCreationDateString
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("header", {className: "standalone-header header-box container-box"},
|
||||
React.createElement(ConversationBranding, null),
|
||||
React.createElement("div", {className: "loop-logo",
|
||||
title: mozL10n.get("client_alttext",
|
||||
{clientShortname: mozL10n.get("clientShortname2")})}),
|
||||
React.createElement("h3", {className: "call-url"},
|
||||
conversationUrl
|
||||
),
|
||||
React.createElement("h4", {className: urlCreationDateClasses},
|
||||
callUrlCreationDateString
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationFooter = React.createClass({displayName: "ConversationFooter",
|
||||
render: function() {
|
||||
return (
|
||||
React.createElement("div", {className: "standalone-footer container-box"},
|
||||
React.createElement("div", {className: "footer-logo",
|
||||
title: mozL10n.get("vendor_alttext",
|
||||
{vendorShortname: mozL10n.get("vendorShortname")})}),
|
||||
React.createElement("div", {className: "footer-external-links"},
|
||||
React.createElement("a", {href: loop.config.generalSupportUrl, target: "_blank"},
|
||||
mozL10n.get("support_link")
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A view for when conversations are pending, displays any messages
|
||||
* and an option cancel button.
|
||||
*/
|
||||
var PendingConversationView = React.createClass({displayName: "PendingConversationView",
|
||||
propTypes: {
|
||||
callState: React.PropTypes.string.isRequired,
|
||||
// If not supplied, the cancel button is not displayed.
|
||||
cancelCallback: React.PropTypes.func
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cancelButtonClasses = React.addons.classSet({
|
||||
btn: true,
|
||||
"btn-large": true,
|
||||
"btn-cancel": true,
|
||||
hide: !this.props.cancelCallback
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "container"},
|
||||
React.createElement("div", {className: "container-box"},
|
||||
React.createElement("header", {className: "pending-header header-box"},
|
||||
React.createElement(ConversationBranding, null)
|
||||
),
|
||||
|
||||
React.createElement("div", {id: "cameraPreview"}),
|
||||
|
||||
React.createElement("div", {id: "messages"}),
|
||||
|
||||
React.createElement("p", {className: "standalone-btn-label"},
|
||||
this.props.callState
|
||||
),
|
||||
|
||||
React.createElement("div", {className: "btn-pending-cancel-group btn-group"},
|
||||
React.createElement("div", {className: "flex-padding-1"}),
|
||||
React.createElement("button", {className: cancelButtonClasses,
|
||||
onClick: this.props.cancelCallback},
|
||||
React.createElement("span", {className: "standalone-call-btn-text"},
|
||||
mozL10n.get("initiate_call_cancel_button")
|
||||
)
|
||||
),
|
||||
React.createElement("div", {className: "flex-padding-1"})
|
||||
)
|
||||
),
|
||||
React.createElement(ConversationFooter, null)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* View displayed whilst the get user media prompt is being displayed. Indicates
|
||||
* to the user to accept the prompt.
|
||||
*/
|
||||
var GumPromptConversationView = React.createClass({displayName: "GumPromptConversationView",
|
||||
render: function() {
|
||||
var callState = mozL10n.get("call_progress_getting_media_description", {
|
||||
clientShortname: mozL10n.get("clientShortname2")
|
||||
});
|
||||
document.title = mozL10n.get("standalone_title_with_status", {
|
||||
clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get("call_progress_getting_media_title")
|
||||
});
|
||||
|
||||
return React.createElement(PendingConversationView, {callState: callState});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* View displayed waiting for a call to be connected. Updates the display
|
||||
* once the websocket shows that the callee is being alerted.
|
||||
*/
|
||||
var WaitingConversationView = React.createClass({displayName: "WaitingConversationView",
|
||||
mixins: [sharedMixins.AudioMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
callState: "connecting"
|
||||
};
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
|
||||
.isRequired
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.play("connecting", {loop: true});
|
||||
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
|
||||
this._handleRingingProgress);
|
||||
},
|
||||
|
||||
_handleRingingProgress: function() {
|
||||
this.play("ringtone", {loop: true});
|
||||
this.setState({callState: "ringing"});
|
||||
},
|
||||
|
||||
_cancelOutgoingCall: function() {
|
||||
multiplexGum.reset();
|
||||
this.props.websocket.cancel();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var callStateStringEntityName = "call_progress_" + this.state.callState + "_description";
|
||||
var callState = mozL10n.get(callStateStringEntityName);
|
||||
document.title = mozL10n.get("standalone_title_with_status",
|
||||
{clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get(callStateStringEntityName)});
|
||||
|
||||
return (
|
||||
React.createElement(PendingConversationView, {
|
||||
callState: callState,
|
||||
cancelCallback: this._cancelOutgoingCall})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var InitiateCallButton = React.createClass({displayName: "InitiateCallButton",
|
||||
mixins: [sharedMixins.DropdownMenuMixin()],
|
||||
|
||||
propTypes: {
|
||||
caption: React.PropTypes.string.isRequired,
|
||||
disabled: React.PropTypes.bool,
|
||||
startCall: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {disabled: false};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var dropdownMenuClasses = React.addons.classSet({
|
||||
"native-dropdown-large-parent": true,
|
||||
"standalone-dropdown-menu": true,
|
||||
"visually-hidden": !this.state.showMenu
|
||||
});
|
||||
var chevronClasses = React.addons.classSet({
|
||||
"btn-chevron": true,
|
||||
"disabled": this.props.disabled
|
||||
});
|
||||
return (
|
||||
React.createElement("div", {className: "standalone-btn-chevron-menu-group"},
|
||||
React.createElement("div", {className: "btn-group-chevron"},
|
||||
React.createElement("div", {className: "btn-group"},
|
||||
React.createElement("button", {className: "btn btn-constrained btn-large btn-accept",
|
||||
disabled: this.props.disabled,
|
||||
onClick: this.props.startCall("audio-video"),
|
||||
title: mozL10n.get("initiate_audio_video_call_tooltip2")},
|
||||
React.createElement("span", {className: "standalone-call-btn-text"},
|
||||
this.props.caption
|
||||
),
|
||||
React.createElement("span", {className: "standalone-call-btn-video-icon"})
|
||||
),
|
||||
React.createElement("div", {className: chevronClasses,
|
||||
onClick: this.toggleDropdownMenu}
|
||||
)
|
||||
),
|
||||
React.createElement("ul", {className: dropdownMenuClasses},
|
||||
React.createElement("li", null,
|
||||
React.createElement("button", {className: "start-audio-only-call",
|
||||
disabled: this.props.disabled,
|
||||
onClick: this.props.startCall("audio")},
|
||||
mozL10n.get("initiate_audio_call_button2")
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Initiate conversation view.
|
||||
*/
|
||||
var InitiateConversationView = React.createClass({displayName: "InitiateConversationView",
|
||||
mixins: [Backbone.Events],
|
||||
|
||||
propTypes: {
|
||||
callButtonLabel: React.PropTypes.string.isRequired,
|
||||
client: React.PropTypes.object.isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
|
||||
// XXX Check more tightly here when we start injecting window.loop.*
|
||||
notifications: React.PropTypes.object.isRequired,
|
||||
title: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
urlCreationDateString: "",
|
||||
disableCallButton: false
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.listenTo(this.props.conversation,
|
||||
"session:error", this._onSessionError);
|
||||
this.props.client.requestCallUrlInfo(
|
||||
this.props.conversation.get("loopToken"),
|
||||
this._setConversationTimestamp);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.stopListening(this.props.conversation);
|
||||
localStorage.setItem("has-seen-tos", "true");
|
||||
},
|
||||
|
||||
_onSessionError: function(error, l10nProps) {
|
||||
var errorL10n = error || "unable_retrieve_call_info";
|
||||
this.props.notifications.errorL10n(errorL10n, l10nProps);
|
||||
console.error(errorL10n);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates the call.
|
||||
* Takes in a call type parameter "audio" or "audio-video" and returns
|
||||
* a function that initiates the call. React click handler requires a function
|
||||
* to be called when that event happenes.
|
||||
*
|
||||
* @param {string} User call type choice "audio" or "audio-video"
|
||||
*/
|
||||
startCall: function(callType) {
|
||||
return function() {
|
||||
this.props.conversation.setupOutgoingCall(callType);
|
||||
this.setState({disableCallButton: true});
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
_setConversationTimestamp: function(err, callUrlInfo) {
|
||||
if (err) {
|
||||
this.props.notifications.errorL10n("unable_retrieve_call_info");
|
||||
} else {
|
||||
this.setState({
|
||||
urlCreationDateString: sharedUtils.formatDate(callUrlInfo.urlCreationDate)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tosLinkName = mozL10n.get("terms_of_use_link_text");
|
||||
var privacyNoticeName = mozL10n.get("privacy_notice_link_text");
|
||||
|
||||
var tosHTML = mozL10n.get("legal_text_and_links", {
|
||||
"clientShortname": mozL10n.get("clientShortname2"),
|
||||
"terms_of_use_url": "<a target=_blank href='" +
|
||||
loop.config.legalWebsiteUrl + "'>" +
|
||||
tosLinkName + "</a>",
|
||||
"privacy_notice_url": "<a target=_blank href='" +
|
||||
loop.config.privacyWebsiteUrl + "'>" + privacyNoticeName + "</a>"
|
||||
});
|
||||
|
||||
var tosClasses = React.addons.classSet({
|
||||
"terms-service": true,
|
||||
hide: (localStorage.getItem("has-seen-tos") === "true")
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "container"},
|
||||
React.createElement("div", {className: "container-box"},
|
||||
|
||||
React.createElement(ConversationHeader, {
|
||||
urlCreationDateString: this.state.urlCreationDateString}),
|
||||
|
||||
React.createElement("p", {className: "standalone-btn-label"},
|
||||
this.props.title
|
||||
),
|
||||
|
||||
React.createElement("div", {id: "messages"}),
|
||||
|
||||
React.createElement("div", {className: "btn-group"},
|
||||
React.createElement("div", {className: "flex-padding-1"}),
|
||||
React.createElement(InitiateCallButton, {
|
||||
caption: this.props.callButtonLabel,
|
||||
disabled: this.state.disableCallButton,
|
||||
startCall: this.startCall}
|
||||
),
|
||||
React.createElement("div", {className: "flex-padding-1"})
|
||||
),
|
||||
|
||||
React.createElement("p", {className: tosClasses,
|
||||
dangerouslySetInnerHTML: {__html: tosHTML}})
|
||||
),
|
||||
|
||||
React.createElement(ConversationFooter, null)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Ended conversation view.
|
||||
*/
|
||||
var EndedConversationView = React.createClass({displayName: "EndedConversationView",
|
||||
propTypes: {
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
||||
.isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
onAfterFeedbackReceived: React.PropTypes.func.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
document.title = mozL10n.get("standalone_title_with_status",
|
||||
{clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get("status_conversation_ended")});
|
||||
return (
|
||||
React.createElement("div", {className: "ended-conversation"},
|
||||
React.createElement(sharedViews.ConversationView, {
|
||||
audio: {enabled: false, visible: false},
|
||||
dispatcher: this.props.dispatcher,
|
||||
initiate: false,
|
||||
model: this.props.conversation,
|
||||
sdk: this.props.sdk,
|
||||
video: {enabled: false, visible: false}})
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StartConversationView = React.createClass({displayName: "StartConversationView",
|
||||
render: function() {
|
||||
document.title = mozL10n.get("clientShortname2");
|
||||
return (
|
||||
React.createElement(InitiateConversationView, React.__spread({},
|
||||
this.props,
|
||||
{callButtonLabel: mozL10n.get("initiate_audio_video_call_button2"),
|
||||
title: mozL10n.get("initiate_call_button_label2")}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var FailedConversationView = React.createClass({displayName: "FailedConversationView",
|
||||
mixins: [sharedMixins.AudioMixin],
|
||||
|
||||
componentDidMount: function() {
|
||||
this.play("failure");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
document.title = mozL10n.get("standalone_title_with_status",
|
||||
{clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get("status_error")});
|
||||
return (
|
||||
React.createElement(InitiateConversationView, React.__spread({},
|
||||
this.props,
|
||||
{callButtonLabel: mozL10n.get("retry_call_button"),
|
||||
title: mozL10n.get("call_failed_title")}))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This view manages the outgoing conversation views - from
|
||||
* call initiation through to the actual conversation and call end.
|
||||
*
|
||||
* At the moment, it does more than that, these parts need refactoring out.
|
||||
*/
|
||||
var OutgoingConversationView = React.createClass({displayName: "OutgoingConversationView",
|
||||
propTypes: {
|
||||
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
isFirefox: React.PropTypes.bool.isRequired,
|
||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection).isRequired,
|
||||
sdk: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
callStatus: "start"
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.conversation.on("call:outgoing", this.startCall, this);
|
||||
this.props.conversation.on("call:outgoing:get-media-privs", this.getMediaPrivs, this);
|
||||
this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
|
||||
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
|
||||
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
|
||||
this.props.conversation.on("session:ended", this._endCall, this);
|
||||
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
|
||||
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
|
||||
this.props.conversation.on("session:connection-error", this._notifyError, this);
|
||||
},
|
||||
|
||||
componentDidUnmount: function() {
|
||||
this.props.conversation.off(null, null, this);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
// Only rerender if current state has actually changed
|
||||
return nextState.callStatus !== this.state.callStatus;
|
||||
},
|
||||
|
||||
resetCallStatus: function() {
|
||||
return function() {
|
||||
this.setState({callStatus: "start"});
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the conversation views.
|
||||
*/
|
||||
render: function() {
|
||||
switch (this.state.callStatus) {
|
||||
case "start": {
|
||||
return (
|
||||
React.createElement(StartConversationView, {
|
||||
client: this.props.client,
|
||||
conversation: this.props.conversation,
|
||||
notifications: this.props.notifications})
|
||||
);
|
||||
}
|
||||
case "failure": {
|
||||
return (
|
||||
React.createElement(FailedConversationView, {
|
||||
client: this.props.client,
|
||||
conversation: this.props.conversation,
|
||||
notifications: this.props.notifications})
|
||||
);
|
||||
}
|
||||
case "gumPrompt": {
|
||||
return React.createElement(GumPromptConversationView, null);
|
||||
}
|
||||
case "pending": {
|
||||
return React.createElement(WaitingConversationView, {websocket: this._websocket});
|
||||
}
|
||||
case "connected": {
|
||||
document.title = mozL10n.get("standalone_title_with_status",
|
||||
{clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get("status_in_conversation")});
|
||||
return (
|
||||
React.createElement(sharedViews.ConversationView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
initiate: true,
|
||||
model: this.props.conversation,
|
||||
sdk: this.props.sdk,
|
||||
video: {enabled: this.props.conversation.hasVideoStream("outgoing")}})
|
||||
);
|
||||
}
|
||||
case "end": {
|
||||
return (
|
||||
React.createElement(EndedConversationView, {
|
||||
conversation: this.props.conversation,
|
||||
dispatcher: this.props.dispatcher,
|
||||
onAfterFeedbackReceived: this.resetCallStatus(),
|
||||
sdk: this.props.sdk})
|
||||
);
|
||||
}
|
||||
case "expired": {
|
||||
return (
|
||||
React.createElement(CallUrlExpiredView, {isFirefox: this.props.isFirefox})
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return React.createElement(HomeView, null);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Notify the user that the connection was not possible
|
||||
* @param {{code: number, message: string}} error
|
||||
*/
|
||||
_notifyError: function(error) {
|
||||
console.error(error);
|
||||
this.props.notifications.errorL10n("connection_error_see_console_notification");
|
||||
this.setState({callStatus: "end"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Peer hung up. Notifies the user and ends the call.
|
||||
*
|
||||
* Event properties:
|
||||
* - {String} connectionId: OT session id
|
||||
*/
|
||||
_onPeerHungup: function() {
|
||||
this.props.notifications.warnL10n("peer_ended_conversation2");
|
||||
this.setState({callStatus: "end"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Network disconnected. Notifies the user and ends the call.
|
||||
*/
|
||||
_onNetworkDisconnected: function() {
|
||||
this.props.notifications.warnL10n("network_disconnected");
|
||||
this.setState({callStatus: "end"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the set up of a call, obtaining the required information from the
|
||||
* server.
|
||||
*/
|
||||
setupOutgoingCall: function() {
|
||||
var loopToken = this.props.conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this.props.notifications.errorL10n("missing_conversation_info");
|
||||
this.setState({callStatus: "failure"});
|
||||
} else {
|
||||
var callType = this.props.conversation.get("selectedCallType");
|
||||
|
||||
this.props.client.requestCallInfo(this.props.conversation.get("loopToken"),
|
||||
callType, function(err, sessionData) {
|
||||
if (err) {
|
||||
switch (err.errno) {
|
||||
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
|
||||
// missing OR expired; we treat this information as if the url is always
|
||||
// expired.
|
||||
case 105:
|
||||
this.setState({callStatus: "expired"});
|
||||
break;
|
||||
default:
|
||||
this.props.notifications.errorL10n("missing_conversation_info");
|
||||
this.setState({callStatus: "failure"});
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.props.conversation.outgoing(sessionData);
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks the user for the media privileges, handling the result appropriately.
|
||||
*/
|
||||
getMediaPrivs: function() {
|
||||
this.setState({callStatus: "gumPrompt"});
|
||||
multiplexGum.getPermsAndCacheMedia({audio: true, video: true},
|
||||
function(localStream) {
|
||||
this.props.conversation.gotMediaPrivs();
|
||||
}.bind(this),
|
||||
function(errorCode) {
|
||||
multiplexGum.reset();
|
||||
this.setState({callStatus: "failure"});
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Actually starts the call.
|
||||
*/
|
||||
startCall: function() {
|
||||
var loopToken = this.props.conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this.props.notifications.errorL10n("missing_conversation_info");
|
||||
this.setState({callStatus: "failure"});
|
||||
return;
|
||||
}
|
||||
|
||||
this._setupWebSocket();
|
||||
this.setState({callStatus: "pending"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to set up the web socket connection and navigate to the
|
||||
* call view if appropriate.
|
||||
*
|
||||
* @param {string} loopToken The session token to use.
|
||||
*/
|
||||
_setupWebSocket: function() {
|
||||
this._websocket = new loop.CallConnectionWebSocket({
|
||||
url: this.props.conversation.get("progressURL"),
|
||||
websocketToken: this.props.conversation.get("websocketToken"),
|
||||
callId: this.props.conversation.get("callId")
|
||||
});
|
||||
this._websocket.promiseConnect().then(function() {
|
||||
}.bind(this), function() {
|
||||
// XXX Not the ideal response, but bug 1047410 will be replacing
|
||||
// this by better "call failed" UI.
|
||||
this.props.notifications.errorL10n("cannot_start_call_session_not_ready");
|
||||
return;
|
||||
}.bind(this));
|
||||
|
||||
this._websocket.on("progress", this._handleWebSocketProgress, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the streams have been connected, and notifies the
|
||||
* websocket that the media is now connected.
|
||||
*/
|
||||
_checkConnected: function() {
|
||||
// Check we've had both local and remote streams connected before
|
||||
// sending the media up message.
|
||||
if (this.props.conversation.streamsConnected()) {
|
||||
this._websocket.mediaUp();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to receive websocket progress and to determine how to handle
|
||||
* it if appropraite.
|
||||
*/
|
||||
_handleWebSocketProgress: function(progressData) {
|
||||
switch(progressData.state) {
|
||||
case "connecting": {
|
||||
// We just go straight to the connected view as the media gets set up.
|
||||
this.setState({callStatus: "connected"});
|
||||
break;
|
||||
}
|
||||
case "terminated": {
|
||||
// At the moment, we show the same text regardless
|
||||
// of the terminated reason.
|
||||
this._handleCallTerminated(progressData.reason);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles call rejection.
|
||||
*
|
||||
* @param {String} reason The reason the call was terminated (reject, busy,
|
||||
* timeout, cancel, media-fail, user-unknown, closed)
|
||||
*/
|
||||
_handleCallTerminated: function(reason) {
|
||||
multiplexGum.reset();
|
||||
|
||||
if (reason === WEBSOCKET_REASONS.CANCEL) {
|
||||
this.setState({callStatus: "start"});
|
||||
return;
|
||||
}
|
||||
// XXX later, we'll want to display more meaningfull messages (needs UX)
|
||||
this.props.notifications.errorL10n("call_timeout_notification_text");
|
||||
this.setState({callStatus: "failure"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles ending a call by resetting the view to the start state.
|
||||
*/
|
||||
_endCall: function() {
|
||||
multiplexGum.reset();
|
||||
|
||||
if (this.state.callStatus !== "failure") {
|
||||
this.setState({callStatus: "end"});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Webapp Root View. This is the main, single, view that controls the display
|
||||
* of the webapp page.
|
||||
@ -849,12 +120,7 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
|
||||
propTypes: {
|
||||
activeRoomStore: React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
|
||||
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||
.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
standaloneAppStore: React.PropTypes.instanceOf(
|
||||
loop.store.StandaloneAppStore).isRequired
|
||||
},
|
||||
@ -885,17 +151,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
case "unsupportedBrowser": {
|
||||
return React.createElement(UnsupportedBrowserView, {isFirefox: this.state.isFirefox});
|
||||
}
|
||||
case "outgoing": {
|
||||
return (
|
||||
React.createElement(OutgoingConversationView, {
|
||||
client: this.props.client,
|
||||
conversation: this.props.conversation,
|
||||
dispatcher: this.props.dispatcher,
|
||||
isFirefox: this.state.isFirefox,
|
||||
notifications: this.props.notifications,
|
||||
sdk: this.props.sdk})
|
||||
);
|
||||
}
|
||||
case "room": {
|
||||
return (
|
||||
React.createElement(loop.standaloneRoomViews.StandaloneRoomView, {
|
||||
@ -924,14 +179,8 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
});
|
||||
|
||||
// Older non-flux based items.
|
||||
var notifications = new sharedModels.NotificationCollection();
|
||||
|
||||
// New flux items.
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var client = new loop.StandaloneClient({
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
});
|
||||
var sdkDriver = new loop.OTSdkDriver({
|
||||
// For the standalone, always request data channels. If they aren't
|
||||
// implemented on the client, there won't be a similar message to us, and
|
||||
@ -941,9 +190,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
sdk: OT
|
||||
});
|
||||
|
||||
var conversation = new sharedModels.ConversationModel({}, {
|
||||
sdk: OT
|
||||
});
|
||||
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
|
||||
mozLoop: standaloneMozLoop,
|
||||
sdkDriver: sdkDriver
|
||||
@ -951,7 +197,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
|
||||
// Stores
|
||||
var standaloneAppStore = new loop.store.StandaloneAppStore({
|
||||
conversation: conversation,
|
||||
dispatcher: dispatcher,
|
||||
sdk: OT
|
||||
});
|
||||
@ -976,11 +221,7 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
|
||||
React.render(React.createElement(WebappRootView, {
|
||||
activeRoomStore: activeRoomStore,
|
||||
client: client,
|
||||
conversation: conversation,
|
||||
dispatcher: dispatcher,
|
||||
notifications: notifications,
|
||||
sdk: OT,
|
||||
standaloneAppStore: standaloneAppStore}), document.querySelector("#main"));
|
||||
|
||||
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
|
||||
@ -997,14 +238,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
}
|
||||
|
||||
return {
|
||||
CallUrlExpiredView: CallUrlExpiredView,
|
||||
PendingConversationView: PendingConversationView,
|
||||
GumPromptConversationView: GumPromptConversationView,
|
||||
WaitingConversationView: WaitingConversationView,
|
||||
StartConversationView: StartConversationView,
|
||||
FailedConversationView: FailedConversationView,
|
||||
OutgoingConversationView: OutgoingConversationView,
|
||||
EndedConversationView: EndedConversationView,
|
||||
HomeView: HomeView,
|
||||
UnsupportedBrowserView: UnsupportedBrowserView,
|
||||
UnsupportedDeviceView: UnsupportedDeviceView,
|
||||
|
@ -16,14 +16,11 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
var sharedUtils = loop.shared.utils;
|
||||
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
|
||||
|
||||
var multiplexGum = loop.standaloneMedia.multiplexGum;
|
||||
|
||||
/**
|
||||
* Homepage view.
|
||||
*/
|
||||
var HomeView = React.createClass({
|
||||
render: function() {
|
||||
multiplexGum.reset();
|
||||
return (
|
||||
<p>{mozL10n.get("welcome", {clientShortname: mozL10n.get("clientShortname2")})}</p>
|
||||
);
|
||||
@ -111,732 +108,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Expired call URL view.
|
||||
*/
|
||||
var CallUrlExpiredView = React.createClass({
|
||||
propTypes: {
|
||||
isFirefox: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="highlight-issue-box">
|
||||
<div className="info-panel">
|
||||
<div className="firefox-logo" />
|
||||
<h1>{mozL10n.get("call_url_unavailable_notification_heading")}</h1>
|
||||
<h4>{mozL10n.get("call_url_unavailable_notification_message2")}</h4>
|
||||
</div>
|
||||
<PromoteFirefoxView isFirefox={this.props.isFirefox}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationBranding = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<h1 className="standalone-header-title">
|
||||
<strong>{mozL10n.get("clientShortname2")}</strong>
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationHeader = React.createClass({
|
||||
propTypes: {
|
||||
urlCreationDateString: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
var conversationUrl = location.href;
|
||||
|
||||
var urlCreationDateClasses = cx({
|
||||
"light-color-font": true,
|
||||
"call-url-date": true, /* Used as a handler in the tests */
|
||||
// Hidden until date is available.
|
||||
"hide": !this.props.urlCreationDateString.length
|
||||
});
|
||||
|
||||
var callUrlCreationDateString = mozL10n.get("call_url_creation_date_label", {
|
||||
"call_url_creation_date": this.props.urlCreationDateString
|
||||
});
|
||||
|
||||
return (
|
||||
<header className="standalone-header header-box container-box">
|
||||
<ConversationBranding />
|
||||
<div className="loop-logo"
|
||||
title={mozL10n.get("client_alttext",
|
||||
{clientShortname: mozL10n.get("clientShortname2")})}></div>
|
||||
<h3 className="call-url">
|
||||
{conversationUrl}
|
||||
</h3>
|
||||
<h4 className={urlCreationDateClasses}>
|
||||
{callUrlCreationDateString}
|
||||
</h4>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationFooter = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div className="standalone-footer container-box">
|
||||
<div className="footer-logo"
|
||||
title={mozL10n.get("vendor_alttext",
|
||||
{vendorShortname: mozL10n.get("vendorShortname")})} />
|
||||
<div className="footer-external-links">
|
||||
<a href={loop.config.generalSupportUrl} target="_blank">
|
||||
{mozL10n.get("support_link")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A view for when conversations are pending, displays any messages
|
||||
* and an option cancel button.
|
||||
*/
|
||||
var PendingConversationView = React.createClass({
|
||||
propTypes: {
|
||||
callState: React.PropTypes.string.isRequired,
|
||||
// If not supplied, the cancel button is not displayed.
|
||||
cancelCallback: React.PropTypes.func
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var cancelButtonClasses = React.addons.classSet({
|
||||
btn: true,
|
||||
"btn-large": true,
|
||||
"btn-cancel": true,
|
||||
hide: !this.props.cancelCallback
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="container-box">
|
||||
<header className="pending-header header-box">
|
||||
<ConversationBranding />
|
||||
</header>
|
||||
|
||||
<div id="cameraPreview" />
|
||||
|
||||
<div id="messages" />
|
||||
|
||||
<p className="standalone-btn-label">
|
||||
{this.props.callState}
|
||||
</p>
|
||||
|
||||
<div className="btn-pending-cancel-group btn-group">
|
||||
<div className="flex-padding-1" />
|
||||
<button className={cancelButtonClasses}
|
||||
onClick={this.props.cancelCallback} >
|
||||
<span className="standalone-call-btn-text">
|
||||
{mozL10n.get("initiate_call_cancel_button")}
|
||||
</span>
|
||||
</button>
|
||||
<div className="flex-padding-1" />
|
||||
</div>
|
||||
</div>
|
||||
<ConversationFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* View displayed whilst the get user media prompt is being displayed. Indicates
|
||||
* to the user to accept the prompt.
|
||||
*/
|
||||
var GumPromptConversationView = React.createClass({
|
||||
render: function() {
|
||||
var callState = mozL10n.get("call_progress_getting_media_description", {
|
||||
clientShortname: mozL10n.get("clientShortname2")
|
||||
});
|
||||
document.title = mozL10n.get("standalone_title_with_status", {
|
||||
clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get("call_progress_getting_media_title")
|
||||
});
|
||||
|
||||
return <PendingConversationView callState={callState}/>;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* View displayed waiting for a call to be connected. Updates the display
|
||||
* once the websocket shows that the callee is being alerted.
|
||||
*/
|
||||
var WaitingConversationView = React.createClass({
|
||||
mixins: [sharedMixins.AudioMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
callState: "connecting"
|
||||
};
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
|
||||
.isRequired
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.play("connecting", {loop: true});
|
||||
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
|
||||
this._handleRingingProgress);
|
||||
},
|
||||
|
||||
_handleRingingProgress: function() {
|
||||
this.play("ringtone", {loop: true});
|
||||
this.setState({callState: "ringing"});
|
||||
},
|
||||
|
||||
_cancelOutgoingCall: function() {
|
||||
multiplexGum.reset();
|
||||
this.props.websocket.cancel();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var callStateStringEntityName = "call_progress_" + this.state.callState + "_description";
|
||||
var callState = mozL10n.get(callStateStringEntityName);
|
||||
document.title = mozL10n.get("standalone_title_with_status",
|
||||
{clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get(callStateStringEntityName)});
|
||||
|
||||
return (
|
||||
<PendingConversationView
|
||||
callState={callState}
|
||||
cancelCallback={this._cancelOutgoingCall} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var InitiateCallButton = React.createClass({
|
||||
mixins: [sharedMixins.DropdownMenuMixin()],
|
||||
|
||||
propTypes: {
|
||||
caption: React.PropTypes.string.isRequired,
|
||||
disabled: React.PropTypes.bool,
|
||||
startCall: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {disabled: false};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var dropdownMenuClasses = React.addons.classSet({
|
||||
"native-dropdown-large-parent": true,
|
||||
"standalone-dropdown-menu": true,
|
||||
"visually-hidden": !this.state.showMenu
|
||||
});
|
||||
var chevronClasses = React.addons.classSet({
|
||||
"btn-chevron": true,
|
||||
"disabled": this.props.disabled
|
||||
});
|
||||
return (
|
||||
<div className="standalone-btn-chevron-menu-group">
|
||||
<div className="btn-group-chevron">
|
||||
<div className="btn-group">
|
||||
<button className="btn btn-constrained btn-large btn-accept"
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.props.startCall("audio-video")}
|
||||
title={mozL10n.get("initiate_audio_video_call_tooltip2")}>
|
||||
<span className="standalone-call-btn-text">
|
||||
{this.props.caption}
|
||||
</span>
|
||||
<span className="standalone-call-btn-video-icon" />
|
||||
</button>
|
||||
<div className={chevronClasses}
|
||||
onClick={this.toggleDropdownMenu}>
|
||||
</div>
|
||||
</div>
|
||||
<ul className={dropdownMenuClasses}>
|
||||
<li>
|
||||
<button className="start-audio-only-call"
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.props.startCall("audio")}>
|
||||
{mozL10n.get("initiate_audio_call_button2")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Initiate conversation view.
|
||||
*/
|
||||
var InitiateConversationView = React.createClass({
|
||||
mixins: [Backbone.Events],
|
||||
|
||||
propTypes: {
|
||||
callButtonLabel: React.PropTypes.string.isRequired,
|
||||
client: React.PropTypes.object.isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
|
||||
// XXX Check more tightly here when we start injecting window.loop.*
|
||||
notifications: React.PropTypes.object.isRequired,
|
||||
title: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
urlCreationDateString: "",
|
||||
disableCallButton: false
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.listenTo(this.props.conversation,
|
||||
"session:error", this._onSessionError);
|
||||
this.props.client.requestCallUrlInfo(
|
||||
this.props.conversation.get("loopToken"),
|
||||
this._setConversationTimestamp);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.stopListening(this.props.conversation);
|
||||
localStorage.setItem("has-seen-tos", "true");
|
||||
},
|
||||
|
||||
_onSessionError: function(error, l10nProps) {
|
||||
var errorL10n = error || "unable_retrieve_call_info";
|
||||
this.props.notifications.errorL10n(errorL10n, l10nProps);
|
||||
console.error(errorL10n);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates the call.
|
||||
* Takes in a call type parameter "audio" or "audio-video" and returns
|
||||
* a function that initiates the call. React click handler requires a function
|
||||
* to be called when that event happenes.
|
||||
*
|
||||
* @param {string} User call type choice "audio" or "audio-video"
|
||||
*/
|
||||
startCall: function(callType) {
|
||||
return function() {
|
||||
this.props.conversation.setupOutgoingCall(callType);
|
||||
this.setState({disableCallButton: true});
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
_setConversationTimestamp: function(err, callUrlInfo) {
|
||||
if (err) {
|
||||
this.props.notifications.errorL10n("unable_retrieve_call_info");
|
||||
} else {
|
||||
this.setState({
|
||||
urlCreationDateString: sharedUtils.formatDate(callUrlInfo.urlCreationDate)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var tosLinkName = mozL10n.get("terms_of_use_link_text");
|
||||
var privacyNoticeName = mozL10n.get("privacy_notice_link_text");
|
||||
|
||||
var tosHTML = mozL10n.get("legal_text_and_links", {
|
||||
"clientShortname": mozL10n.get("clientShortname2"),
|
||||
"terms_of_use_url": "<a target=_blank href='" +
|
||||
loop.config.legalWebsiteUrl + "'>" +
|
||||
tosLinkName + "</a>",
|
||||
"privacy_notice_url": "<a target=_blank href='" +
|
||||
loop.config.privacyWebsiteUrl + "'>" + privacyNoticeName + "</a>"
|
||||
});
|
||||
|
||||
var tosClasses = React.addons.classSet({
|
||||
"terms-service": true,
|
||||
hide: (localStorage.getItem("has-seen-tos") === "true")
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="container-box">
|
||||
|
||||
<ConversationHeader
|
||||
urlCreationDateString={this.state.urlCreationDateString} />
|
||||
|
||||
<p className="standalone-btn-label">
|
||||
{this.props.title}
|
||||
</p>
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<div className="btn-group">
|
||||
<div className="flex-padding-1" />
|
||||
<InitiateCallButton
|
||||
caption={this.props.callButtonLabel}
|
||||
disabled={this.state.disableCallButton}
|
||||
startCall={this.startCall}
|
||||
/>
|
||||
<div className="flex-padding-1" />
|
||||
</div>
|
||||
|
||||
<p className={tosClasses}
|
||||
dangerouslySetInnerHTML={{__html: tosHTML}}></p>
|
||||
</div>
|
||||
|
||||
<ConversationFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Ended conversation view.
|
||||
*/
|
||||
var EndedConversationView = React.createClass({
|
||||
propTypes: {
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
||||
.isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
onAfterFeedbackReceived: React.PropTypes.func.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
render: function() {
|
||||
document.title = mozL10n.get("standalone_title_with_status",
|
||||
{clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get("status_conversation_ended")});
|
||||
return (
|
||||
<div className="ended-conversation">
|
||||
<sharedViews.ConversationView
|
||||
audio={{enabled: false, visible: false}}
|
||||
dispatcher={this.props.dispatcher}
|
||||
initiate={false}
|
||||
model={this.props.conversation}
|
||||
sdk={this.props.sdk}
|
||||
video={{enabled: false, visible: false}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StartConversationView = React.createClass({
|
||||
render: function() {
|
||||
document.title = mozL10n.get("clientShortname2");
|
||||
return (
|
||||
<InitiateConversationView
|
||||
{...this.props}
|
||||
callButtonLabel={mozL10n.get("initiate_audio_video_call_button2")}
|
||||
title={mozL10n.get("initiate_call_button_label2")} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var FailedConversationView = React.createClass({
|
||||
mixins: [sharedMixins.AudioMixin],
|
||||
|
||||
componentDidMount: function() {
|
||||
this.play("failure");
|
||||
},
|
||||
|
||||
render: function() {
|
||||
document.title = mozL10n.get("standalone_title_with_status",
|
||||
{clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get("status_error")});
|
||||
return (
|
||||
<InitiateConversationView
|
||||
{...this.props}
|
||||
callButtonLabel={mozL10n.get("retry_call_button")}
|
||||
title={mozL10n.get("call_failed_title")} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* This view manages the outgoing conversation views - from
|
||||
* call initiation through to the actual conversation and call end.
|
||||
*
|
||||
* At the moment, it does more than that, these parts need refactoring out.
|
||||
*/
|
||||
var OutgoingConversationView = React.createClass({
|
||||
propTypes: {
|
||||
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
isFirefox: React.PropTypes.bool.isRequired,
|
||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection).isRequired,
|
||||
sdk: React.PropTypes.object.isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
callStatus: "start"
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.conversation.on("call:outgoing", this.startCall, this);
|
||||
this.props.conversation.on("call:outgoing:get-media-privs", this.getMediaPrivs, this);
|
||||
this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
|
||||
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
|
||||
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
|
||||
this.props.conversation.on("session:ended", this._endCall, this);
|
||||
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
|
||||
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
|
||||
this.props.conversation.on("session:connection-error", this._notifyError, this);
|
||||
},
|
||||
|
||||
componentDidUnmount: function() {
|
||||
this.props.conversation.off(null, null, this);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
// Only rerender if current state has actually changed
|
||||
return nextState.callStatus !== this.state.callStatus;
|
||||
},
|
||||
|
||||
resetCallStatus: function() {
|
||||
return function() {
|
||||
this.setState({callStatus: "start"});
|
||||
}.bind(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the conversation views.
|
||||
*/
|
||||
render: function() {
|
||||
switch (this.state.callStatus) {
|
||||
case "start": {
|
||||
return (
|
||||
<StartConversationView
|
||||
client={this.props.client}
|
||||
conversation={this.props.conversation}
|
||||
notifications={this.props.notifications} />
|
||||
);
|
||||
}
|
||||
case "failure": {
|
||||
return (
|
||||
<FailedConversationView
|
||||
client={this.props.client}
|
||||
conversation={this.props.conversation}
|
||||
notifications={this.props.notifications} />
|
||||
);
|
||||
}
|
||||
case "gumPrompt": {
|
||||
return <GumPromptConversationView />;
|
||||
}
|
||||
case "pending": {
|
||||
return <WaitingConversationView websocket={this._websocket} />;
|
||||
}
|
||||
case "connected": {
|
||||
document.title = mozL10n.get("standalone_title_with_status",
|
||||
{clientShortname: mozL10n.get("clientShortname2"),
|
||||
currentStatus: mozL10n.get("status_in_conversation")});
|
||||
return (
|
||||
<sharedViews.ConversationView
|
||||
dispatcher={this.props.dispatcher}
|
||||
initiate={true}
|
||||
model={this.props.conversation}
|
||||
sdk={this.props.sdk}
|
||||
video={{enabled: this.props.conversation.hasVideoStream("outgoing")}} />
|
||||
);
|
||||
}
|
||||
case "end": {
|
||||
return (
|
||||
<EndedConversationView
|
||||
conversation={this.props.conversation}
|
||||
dispatcher={this.props.dispatcher}
|
||||
onAfterFeedbackReceived={this.resetCallStatus()}
|
||||
sdk={this.props.sdk} />
|
||||
);
|
||||
}
|
||||
case "expired": {
|
||||
return (
|
||||
<CallUrlExpiredView isFirefox={this.props.isFirefox}/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <HomeView />;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Notify the user that the connection was not possible
|
||||
* @param {{code: number, message: string}} error
|
||||
*/
|
||||
_notifyError: function(error) {
|
||||
console.error(error);
|
||||
this.props.notifications.errorL10n("connection_error_see_console_notification");
|
||||
this.setState({callStatus: "end"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Peer hung up. Notifies the user and ends the call.
|
||||
*
|
||||
* Event properties:
|
||||
* - {String} connectionId: OT session id
|
||||
*/
|
||||
_onPeerHungup: function() {
|
||||
this.props.notifications.warnL10n("peer_ended_conversation2");
|
||||
this.setState({callStatus: "end"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Network disconnected. Notifies the user and ends the call.
|
||||
*/
|
||||
_onNetworkDisconnected: function() {
|
||||
this.props.notifications.warnL10n("network_disconnected");
|
||||
this.setState({callStatus: "end"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the set up of a call, obtaining the required information from the
|
||||
* server.
|
||||
*/
|
||||
setupOutgoingCall: function() {
|
||||
var loopToken = this.props.conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this.props.notifications.errorL10n("missing_conversation_info");
|
||||
this.setState({callStatus: "failure"});
|
||||
} else {
|
||||
var callType = this.props.conversation.get("selectedCallType");
|
||||
|
||||
this.props.client.requestCallInfo(this.props.conversation.get("loopToken"),
|
||||
callType, function(err, sessionData) {
|
||||
if (err) {
|
||||
switch (err.errno) {
|
||||
// loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
|
||||
// missing OR expired; we treat this information as if the url is always
|
||||
// expired.
|
||||
case 105:
|
||||
this.setState({callStatus: "expired"});
|
||||
break;
|
||||
default:
|
||||
this.props.notifications.errorL10n("missing_conversation_info");
|
||||
this.setState({callStatus: "failure"});
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.props.conversation.outgoing(sessionData);
|
||||
}.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks the user for the media privileges, handling the result appropriately.
|
||||
*/
|
||||
getMediaPrivs: function() {
|
||||
this.setState({callStatus: "gumPrompt"});
|
||||
multiplexGum.getPermsAndCacheMedia({audio: true, video: true},
|
||||
function(localStream) {
|
||||
this.props.conversation.gotMediaPrivs();
|
||||
}.bind(this),
|
||||
function(errorCode) {
|
||||
multiplexGum.reset();
|
||||
this.setState({callStatus: "failure"});
|
||||
}.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Actually starts the call.
|
||||
*/
|
||||
startCall: function() {
|
||||
var loopToken = this.props.conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this.props.notifications.errorL10n("missing_conversation_info");
|
||||
this.setState({callStatus: "failure"});
|
||||
return;
|
||||
}
|
||||
|
||||
this._setupWebSocket();
|
||||
this.setState({callStatus: "pending"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to set up the web socket connection and navigate to the
|
||||
* call view if appropriate.
|
||||
*
|
||||
* @param {string} loopToken The session token to use.
|
||||
*/
|
||||
_setupWebSocket: function() {
|
||||
this._websocket = new loop.CallConnectionWebSocket({
|
||||
url: this.props.conversation.get("progressURL"),
|
||||
websocketToken: this.props.conversation.get("websocketToken"),
|
||||
callId: this.props.conversation.get("callId")
|
||||
});
|
||||
this._websocket.promiseConnect().then(function() {
|
||||
}.bind(this), function() {
|
||||
// XXX Not the ideal response, but bug 1047410 will be replacing
|
||||
// this by better "call failed" UI.
|
||||
this.props.notifications.errorL10n("cannot_start_call_session_not_ready");
|
||||
return;
|
||||
}.bind(this));
|
||||
|
||||
this._websocket.on("progress", this._handleWebSocketProgress, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the streams have been connected, and notifies the
|
||||
* websocket that the media is now connected.
|
||||
*/
|
||||
_checkConnected: function() {
|
||||
// Check we've had both local and remote streams connected before
|
||||
// sending the media up message.
|
||||
if (this.props.conversation.streamsConnected()) {
|
||||
this._websocket.mediaUp();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to receive websocket progress and to determine how to handle
|
||||
* it if appropraite.
|
||||
*/
|
||||
_handleWebSocketProgress: function(progressData) {
|
||||
switch(progressData.state) {
|
||||
case "connecting": {
|
||||
// We just go straight to the connected view as the media gets set up.
|
||||
this.setState({callStatus: "connected"});
|
||||
break;
|
||||
}
|
||||
case "terminated": {
|
||||
// At the moment, we show the same text regardless
|
||||
// of the terminated reason.
|
||||
this._handleCallTerminated(progressData.reason);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles call rejection.
|
||||
*
|
||||
* @param {String} reason The reason the call was terminated (reject, busy,
|
||||
* timeout, cancel, media-fail, user-unknown, closed)
|
||||
*/
|
||||
_handleCallTerminated: function(reason) {
|
||||
multiplexGum.reset();
|
||||
|
||||
if (reason === WEBSOCKET_REASONS.CANCEL) {
|
||||
this.setState({callStatus: "start"});
|
||||
return;
|
||||
}
|
||||
// XXX later, we'll want to display more meaningfull messages (needs UX)
|
||||
this.props.notifications.errorL10n("call_timeout_notification_text");
|
||||
this.setState({callStatus: "failure"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles ending a call by resetting the view to the start state.
|
||||
*/
|
||||
_endCall: function() {
|
||||
multiplexGum.reset();
|
||||
|
||||
if (this.state.callStatus !== "failure") {
|
||||
this.setState({callStatus: "end"});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Webapp Root View. This is the main, single, view that controls the display
|
||||
* of the webapp page.
|
||||
@ -849,12 +120,7 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
|
||||
propTypes: {
|
||||
activeRoomStore: React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
|
||||
client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
|
||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||
.isRequired,
|
||||
sdk: React.PropTypes.object.isRequired,
|
||||
standaloneAppStore: React.PropTypes.instanceOf(
|
||||
loop.store.StandaloneAppStore).isRequired
|
||||
},
|
||||
@ -885,17 +151,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
case "unsupportedBrowser": {
|
||||
return <UnsupportedBrowserView isFirefox={this.state.isFirefox}/>;
|
||||
}
|
||||
case "outgoing": {
|
||||
return (
|
||||
<OutgoingConversationView
|
||||
client={this.props.client}
|
||||
conversation={this.props.conversation}
|
||||
dispatcher={this.props.dispatcher}
|
||||
isFirefox={this.state.isFirefox}
|
||||
notifications={this.props.notifications}
|
||||
sdk={this.props.sdk} />
|
||||
);
|
||||
}
|
||||
case "room": {
|
||||
return (
|
||||
<loop.standaloneRoomViews.StandaloneRoomView
|
||||
@ -924,14 +179,8 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
});
|
||||
|
||||
// Older non-flux based items.
|
||||
var notifications = new sharedModels.NotificationCollection();
|
||||
|
||||
// New flux items.
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
var client = new loop.StandaloneClient({
|
||||
baseServerUrl: loop.config.serverUrl
|
||||
});
|
||||
var sdkDriver = new loop.OTSdkDriver({
|
||||
// For the standalone, always request data channels. If they aren't
|
||||
// implemented on the client, there won't be a similar message to us, and
|
||||
@ -941,9 +190,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
sdk: OT
|
||||
});
|
||||
|
||||
var conversation = new sharedModels.ConversationModel({}, {
|
||||
sdk: OT
|
||||
});
|
||||
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
|
||||
mozLoop: standaloneMozLoop,
|
||||
sdkDriver: sdkDriver
|
||||
@ -951,7 +197,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
|
||||
// Stores
|
||||
var standaloneAppStore = new loop.store.StandaloneAppStore({
|
||||
conversation: conversation,
|
||||
dispatcher: dispatcher,
|
||||
sdk: OT
|
||||
});
|
||||
@ -976,11 +221,7 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
|
||||
React.render(<WebappRootView
|
||||
activeRoomStore={activeRoomStore}
|
||||
client={client}
|
||||
conversation={conversation}
|
||||
dispatcher={dispatcher}
|
||||
notifications={notifications}
|
||||
sdk={OT}
|
||||
standaloneAppStore={standaloneAppStore} />, document.querySelector("#main"));
|
||||
|
||||
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
|
||||
@ -997,14 +238,6 @@ loop.webapp = (function(_, OT, mozL10n) {
|
||||
}
|
||||
|
||||
return {
|
||||
CallUrlExpiredView: CallUrlExpiredView,
|
||||
PendingConversationView: PendingConversationView,
|
||||
GumPromptConversationView: GumPromptConversationView,
|
||||
WaitingConversationView: WaitingConversationView,
|
||||
StartConversationView: StartConversationView,
|
||||
FailedConversationView: FailedConversationView,
|
||||
OutgoingConversationView: OutgoingConversationView,
|
||||
EndedConversationView: EndedConversationView,
|
||||
HomeView: HomeView,
|
||||
UnsupportedBrowserView: UnsupportedBrowserView,
|
||||
UnsupportedDeviceView: UnsupportedDeviceView,
|
||||
|
@ -1,11 +1,5 @@
|
||||
## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
|
||||
restart_call=Rejoin
|
||||
conversation_has_ended=Your conversation has ended.
|
||||
call_timeout_notification_text=Your call did not go through.
|
||||
missing_conversation_info=Missing conversation information.
|
||||
network_disconnected=The network connection terminated abruptly.
|
||||
peer_ended_conversation2=The person you were calling has ended the conversation.
|
||||
call_failed_title=Call failed.
|
||||
generic_failure_message=We're having technical difficulties…
|
||||
generic_failure_with_reason2=You can try again or email a link to be reached at later.
|
||||
generic_failure_no_reason2=Would you like to try again?
|
||||
@ -20,33 +14,21 @@ unmute_local_video_button_title2=Enable video
|
||||
active_screenshare_button_title=Stop sharing
|
||||
inactive_screenshare_button_title=Share your screen
|
||||
|
||||
outgoing_call_title=Start conversation?
|
||||
call_with_contact_title=Conversation with {{incomingCallIdentity}}
|
||||
welcome=Welcome to the {{clientShortname}} web client.
|
||||
incompatible_browser_heading=Oops!
|
||||
incompatible_browser_message=Firefox Hello only works in browsers that support WebRTC
|
||||
powered_by_webrtc=The audio and video components of {{clientShortname}} are powered by WebRTC.
|
||||
use_latest_firefox=Please try this link in a WebRTC-enabled browser, such as {{firefoxBrandNameLink}}.
|
||||
unsupported_platform_heading=Sorry!
|
||||
unsupported_platform_message={{platform}} does not currently support {{clientShortname}}
|
||||
unsupported_platform_ios=iOS
|
||||
unsupported_platform_windows_phone=Windows Phone
|
||||
unsupported_platform_blackberry=Blackberry
|
||||
unsupported_platform_learn_more_link=Learn more about why your platform doesn't support {{clientShortname}}
|
||||
connection_error_see_console_notification=Call failed; see console for details.
|
||||
call_url_unavailable_notification_heading=Oops!
|
||||
call_url_unavailable_notification_message2=Sorry, this URL is not available. It may be expired or entered incorrectly.
|
||||
promote_firefox_hello_heading=Download {{brandShortname}} to make free audio and video calls!
|
||||
get_firefox_button=Get {{brandShortname}}
|
||||
initiate_call_button_label2=Ready to start your conversation?
|
||||
initiate_audio_video_call_button2=Start
|
||||
initiate_audio_video_call_tooltip2=Start a video conversation
|
||||
initiate_audio_call_button2=Voice conversation
|
||||
initiate_call_cancel_button=Cancel
|
||||
legal_text_and_links=By using {{clientShortname}} you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}
|
||||
terms_of_use_link_text=Terms of use
|
||||
privacy_notice_link_text=Privacy notice
|
||||
invite_header_text=Invite someone to join you.
|
||||
self_view_hidden_message=Self-view hidden but still being sent; resize window \
|
||||
to show
|
||||
|
||||
@ -60,33 +42,11 @@ clientShortname2=Firefox Hello
|
||||
## should remain "Mozilla" for all locales.
|
||||
vendorShortname=Mozilla
|
||||
|
||||
## LOCALIZATION NOTE(client_alttext): {{clientShortname}} will be replaced with the
|
||||
## value of the clientShortname2 string above.
|
||||
client_alttext={{clientShortname}} logo
|
||||
vendor_alttext={{vendorShortname}} logo
|
||||
|
||||
## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
|
||||
call_url_creation_date_label=(from {{call_url_creation_date}})
|
||||
call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
|
||||
call_progress_getting_media_title=Waiting for media…
|
||||
call_progress_connecting_description=Connecting…
|
||||
call_progress_ringing_description=Ringing…
|
||||
|
||||
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
|
||||
## a signed-in to signed-in user call.
|
||||
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
|
||||
feedback_rejoin_button=Rejoin
|
||||
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
|
||||
## an abusive user.
|
||||
feedback_report_user_button=Report User
|
||||
|
||||
## LOCALIZATION_NOTE(first_time_experience.title): clientShortname will be
|
||||
## replaced by the brand name
|
||||
first_time_experience_title={{clientShortname}} — Join the conversation
|
||||
first_time_experience_button_label=Get Started
|
||||
|
||||
help_label=Help
|
||||
tour_label=Tour
|
||||
|
||||
rooms_default_room_name_template=Conversation {{conversationLabel}}
|
||||
## LOCALIZATION_NOTE(rooms_welcome_title): {{conversationName}} will be replaced
|
||||
@ -115,18 +75,11 @@ room_information_failure_unsupported_browser=Your browser cannot access any info
|
||||
# localized content.
|
||||
rooms_read_while_wait_offer=Want something to read while you wait?
|
||||
|
||||
## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be
|
||||
## replaced by the brand name and {{currentStatus}} will be replaced
|
||||
## by the current call status (Connecting, Ringing, etc.)
|
||||
standalone_title_with_status={{clientShortname}} — {{currentStatus}}
|
||||
## LOCALIZATION_NOTE(standalone_title_with_room_name): {{roomName}} will be replaced
|
||||
## by the name of the conversation and {{clientShortname}} will be
|
||||
## replaced by the brand name.
|
||||
standalone_title_with_room_name={{roomName}} — {{clientShortname}}
|
||||
status_in_conversation=In conversation
|
||||
status_conversation_ended=Conversation ended
|
||||
status_error=Something went wrong
|
||||
support_link=Get Help
|
||||
|
||||
# Text chat strings
|
||||
|
||||
|
@ -87,9 +87,6 @@ describe("loop.conversation", function() {
|
||||
sandbox.stub(React, "render");
|
||||
sandbox.stub(document.mozL10n, "initialize");
|
||||
|
||||
sandbox.stub(loop.shared.models.ConversationModel.prototype,
|
||||
"initialize");
|
||||
|
||||
sandbox.stub(loop.Dispatcher.prototype, "dispatch");
|
||||
|
||||
sandbox.stub(loop.shared.utils,
|
||||
|
@ -34,9 +34,7 @@ module.exports = function(config) {
|
||||
"content/shared/js/textChatView.js",
|
||||
"content/shared/js/urlRegExps.js",
|
||||
"content/shared/js/linkifiedTextView.js",
|
||||
"standalone/content/js/multiplexGum.js",
|
||||
"standalone/content/js/standaloneAppStore.js",
|
||||
"standalone/content/js/standaloneClient.js",
|
||||
"standalone/content/js/standaloneMozLoop.js",
|
||||
"standalone/content/js/standaloneRoomViews.js",
|
||||
"standalone/content/js/standaloneMetricsStore.js",
|
||||
|
@ -6,442 +6,18 @@ describe("loop.shared.models", function() {
|
||||
"use strict";
|
||||
|
||||
var expect = chai.expect;
|
||||
var l10n = navigator.mozL10n || document.mozL10n;
|
||||
var sharedModels = loop.shared.models, sandbox, fakeXHR,
|
||||
requests = [], fakeSDK, fakeMozLoop, fakeSession, fakeSessionData;
|
||||
var l10n = navigator.mozL10n;
|
||||
var sharedModels = loop.shared.models;
|
||||
var sandbox;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox.useFakeTimers();
|
||||
fakeXHR = sandbox.useFakeXMLHttpRequest();
|
||||
requests = [];
|
||||
// https://github.com/cjohansen/Sinon.JS/issues/393
|
||||
fakeXHR.xhr.onCreate = function(xhr) {
|
||||
requests.push(xhr);
|
||||
};
|
||||
fakeSessionData = {
|
||||
sessionId: "sessionId",
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey",
|
||||
callType: "callType",
|
||||
websocketToken: 123,
|
||||
callToken: "callToken",
|
||||
callUrl: "http://invalid/callToken",
|
||||
callerId: "mrssmith"
|
||||
};
|
||||
fakeSession = _.extend({
|
||||
connect: function () {},
|
||||
endSession: sandbox.stub(),
|
||||
set: sandbox.stub(),
|
||||
disconnect: sandbox.spy(),
|
||||
unpublish: sandbox.spy()
|
||||
}, Backbone.Events);
|
||||
fakeSDK = {
|
||||
initPublisher: sandbox.spy(),
|
||||
initSession: sandbox.stub().returns(fakeSession)
|
||||
};
|
||||
fakeMozLoop = {
|
||||
addConversationContext: sinon.spy()
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("ConversationModel", function() {
|
||||
describe("#initialize", function() {
|
||||
it("should require a sdk option", function() {
|
||||
expect(function() {
|
||||
new sharedModels.ConversationModel({}, {});
|
||||
}).to.Throw(Error, /missing required sdk/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
var conversation;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {
|
||||
sdk: fakeSDK,
|
||||
mozLoop: fakeMozLoop
|
||||
});
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
});
|
||||
|
||||
describe("#accepted", function() {
|
||||
it("should trigger a `call:accepted` event", function(done) {
|
||||
conversation.once("call:accepted", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
conversation.accepted();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setupOutgoingCall", function() {
|
||||
it("should set the a custom selected call type", function() {
|
||||
conversation.setupOutgoingCall("audio");
|
||||
|
||||
expect(conversation.get("selectedCallType")).eql("audio");
|
||||
});
|
||||
|
||||
it("should respect the default selected call type when none is passed",
|
||||
function() {
|
||||
conversation.setupOutgoingCall();
|
||||
|
||||
expect(conversation.get("selectedCallType")).eql("audio-video");
|
||||
});
|
||||
|
||||
it("should trigger a `call:outgoing:get-media-privs` event", function(done) {
|
||||
conversation.once("call:outgoing:get-media-privs", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
conversation.setupOutgoingCall();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#gotMediaPrivs", function() {
|
||||
it("should trigger a `call:outgoing:setup` event", function(done) {
|
||||
conversation.once("call:outgoing:setup", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
conversation.gotMediaPrivs();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#outgoing", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(conversation, "endSession");
|
||||
sandbox.stub(conversation, "setOutgoingSessionData");
|
||||
sandbox.stub(conversation, "setIncomingSessionData");
|
||||
});
|
||||
|
||||
it("should save the outgoing sessionData", function() {
|
||||
conversation.outgoing(fakeSessionData);
|
||||
|
||||
sinon.assert.calledOnce(conversation.setOutgoingSessionData);
|
||||
});
|
||||
|
||||
it("should trigger a `call:outgoing` event", function(done) {
|
||||
conversation.once("call:outgoing", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
conversation.outgoing();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setSessionData", function() {
|
||||
it("should update outgoing conversation session information",
|
||||
function() {
|
||||
conversation.setOutgoingSessionData(fakeSessionData);
|
||||
|
||||
expect(conversation.get("sessionId")).eql("sessionId");
|
||||
expect(conversation.get("sessionToken")).eql("sessionToken");
|
||||
expect(conversation.get("apiKey")).eql("apiKey");
|
||||
});
|
||||
|
||||
it("should update incoming conversation session information",
|
||||
function() {
|
||||
conversation.setIncomingSessionData(fakeSessionData);
|
||||
|
||||
expect(conversation.get("sessionId")).eql("sessionId");
|
||||
expect(conversation.get("sessionToken")).eql("sessionToken");
|
||||
expect(conversation.get("apiKey")).eql("apiKey");
|
||||
expect(conversation.get("callType")).eql("callType");
|
||||
expect(conversation.get("callToken")).eql("callToken");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#startSession", function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK,
|
||||
mozLoop: fakeMozLoop
|
||||
});
|
||||
model.set({
|
||||
publishedStream: true,
|
||||
subscribedStream: true
|
||||
});
|
||||
model.startSession();
|
||||
});
|
||||
|
||||
it("should start a session", function() {
|
||||
sinon.assert.calledOnce(fakeSDK.initSession);
|
||||
});
|
||||
|
||||
it("should reset the stream flags", function() {
|
||||
expect(model.get("publishedStream")).eql(false);
|
||||
expect(model.get("subscribedStream")).eql(false);
|
||||
});
|
||||
|
||||
it("should call addConversationContext", function() {
|
||||
fakeMozLoop.addConversationContext = sandbox.stub();
|
||||
|
||||
model.set({
|
||||
windowId: "28",
|
||||
sessionId: "321456",
|
||||
callId: "142536"
|
||||
});
|
||||
model.startSession();
|
||||
|
||||
sinon.assert.calledOnce(fakeMozLoop.addConversationContext);
|
||||
sinon.assert.calledWithExactly(fakeMozLoop.addConversationContext,
|
||||
"28", "321456", "142536");
|
||||
});
|
||||
|
||||
it("should call connect", function() {
|
||||
fakeSession.connect = sandbox.stub();
|
||||
|
||||
model.startSession();
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.connect);
|
||||
sinon.assert.calledWithExactly(fakeSession.connect,
|
||||
sinon.match.string, sinon.match.string,
|
||||
sinon.match.func);
|
||||
});
|
||||
|
||||
it("should set connected to true when no error is called back",
|
||||
function() {
|
||||
fakeSession.connect = function(key, token, cb) {
|
||||
cb(null);
|
||||
};
|
||||
sandbox.stub(model, "set");
|
||||
|
||||
model.startSession();
|
||||
|
||||
sinon.assert.calledWith(model.set, "connected", true);
|
||||
});
|
||||
|
||||
it("should trigger session:connected when no error is called back",
|
||||
function() {
|
||||
fakeSession.connect = function(key, token, cb) {
|
||||
cb(null);
|
||||
};
|
||||
sandbox.stub(model, "trigger");
|
||||
|
||||
model.startSession();
|
||||
|
||||
sinon.assert.calledWithExactly(model.trigger, "session:connected");
|
||||
});
|
||||
|
||||
describe("Session events", function() {
|
||||
|
||||
it("should trigger a fail event when an error is called back",
|
||||
function() {
|
||||
fakeSession.connect = function(key, token, cb) {
|
||||
cb({
|
||||
error: true
|
||||
});
|
||||
};
|
||||
sandbox.stub(model, "endSession");
|
||||
|
||||
model.startSession();
|
||||
|
||||
sinon.assert.calledOnce(model.endSession);
|
||||
sinon.assert.calledWithExactly(model.endSession);
|
||||
});
|
||||
|
||||
it("should trigger session:connection-error event when an error is" +
|
||||
" called back", function() {
|
||||
fakeSession.connect = function(key, token, cb) {
|
||||
cb({
|
||||
error: true
|
||||
});
|
||||
};
|
||||
sandbox.stub(model, "trigger");
|
||||
|
||||
model.startSession();
|
||||
|
||||
sinon.assert.called(model.trigger);
|
||||
sinon.assert.calledWithExactly(model.trigger,
|
||||
"session:connection-error", sinon.match.object);
|
||||
});
|
||||
|
||||
it("should set the connected attr to true on connection completed",
|
||||
function() {
|
||||
fakeSession.connect = function(key, token, cb) {
|
||||
cb();
|
||||
};
|
||||
|
||||
model.startSession();
|
||||
|
||||
expect(model.get("connected")).eql(true);
|
||||
});
|
||||
|
||||
it("should trigger a session:ended event on sessionDisconnected",
|
||||
function(done) {
|
||||
model.once("session:ended", function(){ done(); });
|
||||
|
||||
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
|
||||
});
|
||||
|
||||
it("should trigger network-disconnected on networkDisconnect reason",
|
||||
function(done) {
|
||||
model.once("session:network-disconnected", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
var fakeEvent = {
|
||||
connectionId: 42,
|
||||
reason: "networkDisconnected"
|
||||
};
|
||||
|
||||
fakeSession.trigger("sessionDisconnected", fakeEvent);
|
||||
});
|
||||
|
||||
it("should set the connected attribute to false on sessionDisconnected",
|
||||
function() {
|
||||
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
|
||||
|
||||
expect(model.get("connected")).eql(false);
|
||||
});
|
||||
|
||||
it("should set the ongoing attribute to false on sessionDisconnected",
|
||||
function() {
|
||||
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
|
||||
|
||||
expect(model.get("ongoing")).eql(false);
|
||||
});
|
||||
|
||||
describe("connectionDestroyed event received", function() {
|
||||
var fakeEvent = {reason: "ko", connection: {connectionId: 42}};
|
||||
|
||||
it("should trigger a session:peer-hungup model event",
|
||||
function(done) {
|
||||
model.once("session:peer-hungup", function(event) {
|
||||
expect(event.connection.connectionId).eql(42);
|
||||
done();
|
||||
});
|
||||
|
||||
fakeSession.trigger("connectionDestroyed", fakeEvent);
|
||||
});
|
||||
|
||||
it("should terminate the session", function() {
|
||||
sandbox.stub(model, "endSession");
|
||||
|
||||
fakeSession.trigger("connectionDestroyed", fakeEvent);
|
||||
|
||||
sinon.assert.calledOnce(model.endSession);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#endSession", function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK
|
||||
});
|
||||
model.set("ongoing", true);
|
||||
model.startSession();
|
||||
});
|
||||
|
||||
it("should disconnect current session", function() {
|
||||
model.endSession();
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.disconnect);
|
||||
});
|
||||
|
||||
it("should set the connected attribute to false", function() {
|
||||
model.endSession();
|
||||
|
||||
expect(model.get("connected")).eql(false);
|
||||
});
|
||||
|
||||
it("should set the ongoing attribute to false", function() {
|
||||
model.endSession();
|
||||
|
||||
expect(model.get("ongoing")).eql(false);
|
||||
});
|
||||
|
||||
it("should set the streams to unpublished", function() {
|
||||
model.set({
|
||||
publishedStream: true,
|
||||
subscribedStream: true
|
||||
});
|
||||
|
||||
model.endSession();
|
||||
|
||||
expect(model.get("publishedStream")).eql(false);
|
||||
expect(model.get("subscribedStream")).eql(false);
|
||||
});
|
||||
|
||||
it("should stop listening to session events once the session is " +
|
||||
"actually disconnected", function() {
|
||||
sandbox.stub(model, "stopListening");
|
||||
|
||||
model.endSession();
|
||||
model.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(model.stopListening);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#hasVideoStream", function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK
|
||||
});
|
||||
model.startSession();
|
||||
});
|
||||
|
||||
it("should return true for incoming callType", function() {
|
||||
model.set("callType", "audio-video");
|
||||
|
||||
expect(model.hasVideoStream("incoming")).to.eql(true);
|
||||
});
|
||||
|
||||
it("should return true for outgoing callType", function() {
|
||||
model.set("selectedCallType", "audio-video");
|
||||
|
||||
expect(model.hasVideoStream("outgoing")).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getCallIdentifier", function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK
|
||||
});
|
||||
model.startSession();
|
||||
});
|
||||
|
||||
it("should return the callerId", function() {
|
||||
expect(model.getCallIdentifier()).eql("mrssmith");
|
||||
});
|
||||
|
||||
it("should return the shorted callUrl if the callerId does not exist",
|
||||
function() {
|
||||
model.set({callerId: ""});
|
||||
|
||||
expect(model.getCallIdentifier()).eql("invalid/callToken");
|
||||
});
|
||||
|
||||
it("should return an empty string if neither callerId nor callUrl exist",
|
||||
function() {
|
||||
model.set({
|
||||
callerId: undefined,
|
||||
callUrl: undefined
|
||||
});
|
||||
|
||||
expect(model.getCallIdentifier()).eql("");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationCollection", function() {
|
||||
var collection, notifData, testNotif;
|
||||
|
||||
@ -502,6 +78,5 @@ describe("loop.shared.models", function() {
|
||||
expect(collection.at(0).get("message")).eql("translated:fakeId:fakeProp");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -574,341 +574,6 @@ describe("loop.shared.views", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConversationView", function() {
|
||||
var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model, fakeAudio;
|
||||
|
||||
function mountTestComponent(props) {
|
||||
props = _.extend({
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: {}
|
||||
}, props || {});
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(sharedViews.ConversationView, props));
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
fakeAudio = {
|
||||
play: sinon.spy(),
|
||||
pause: sinon.spy(),
|
||||
removeAttribute: sinon.spy()
|
||||
};
|
||||
sandbox.stub(window, "Audio").returns(fakeAudio);
|
||||
|
||||
fakeSessionData = {
|
||||
sessionId: "sessionId",
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey"
|
||||
};
|
||||
fakeSession = _.extend({
|
||||
connection: {connectionId: 42},
|
||||
connect: sandbox.spy(),
|
||||
disconnect: sandbox.spy(),
|
||||
publish: sandbox.spy(),
|
||||
unpublish: sandbox.spy(),
|
||||
subscribe: sandbox.spy()
|
||||
}, Backbone.Events);
|
||||
fakePublisher = _.extend({
|
||||
publishAudio: sandbox.spy(),
|
||||
publishVideo: sandbox.spy()
|
||||
}, Backbone.Events);
|
||||
fakeSDK = {
|
||||
initPublisher: sandbox.stub().returns(fakePublisher),
|
||||
initSession: sandbox.stub().returns(fakeSession),
|
||||
on: sandbox.stub()
|
||||
};
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK
|
||||
});
|
||||
});
|
||||
|
||||
describe("#componentDidMount", function() {
|
||||
it("should start a session by default", function() {
|
||||
sandbox.stub(model, "startSession");
|
||||
|
||||
mountTestComponent({
|
||||
sdk: fakeSDK,
|
||||
model: model,
|
||||
video: {enabled: true}
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(model.startSession);
|
||||
});
|
||||
|
||||
// Test loop.shared.utils.findParentNode.
|
||||
// Added here to take advantage of having markup.
|
||||
it("should find '.video-layout-wrapper'", function() {
|
||||
var view = mountTestComponent({
|
||||
initiate: false,
|
||||
sdk: fakeSDK,
|
||||
model: model,
|
||||
video: {enabled: true}
|
||||
});
|
||||
var menu = view.getDOMNode().querySelector(".btn-hangup-entry");
|
||||
|
||||
var result = loop.shared.utils.findParentNode(menu,
|
||||
"video-layout-wrapper");
|
||||
|
||||
expect(result.classList.contains("video-layout-wrapper")).to.eql(true);
|
||||
});
|
||||
|
||||
it("shouldn't start a session if initiate is false", function() {
|
||||
sandbox.stub(model, "startSession");
|
||||
|
||||
mountTestComponent({
|
||||
initiate: false,
|
||||
sdk: fakeSDK,
|
||||
model: model,
|
||||
video: {enabled: true}
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(model.startSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
var comp;
|
||||
|
||||
beforeEach(function() {
|
||||
comp = mountTestComponent({
|
||||
sdk: fakeSDK,
|
||||
model: model,
|
||||
video: {enabled: false}
|
||||
});
|
||||
});
|
||||
|
||||
describe("#hangup", function() {
|
||||
beforeEach(function() {
|
||||
comp.startPublishing();
|
||||
});
|
||||
|
||||
it("should disconnect the session", function() {
|
||||
sandbox.stub(model, "endSession");
|
||||
|
||||
comp.hangup();
|
||||
|
||||
sinon.assert.calledOnce(model.endSession);
|
||||
});
|
||||
|
||||
it("should stop publishing local streams", function() {
|
||||
comp.hangup();
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.unpublish);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#startPublishing", function() {
|
||||
it("should publish local stream", function() {
|
||||
comp.startPublishing();
|
||||
|
||||
sinon.assert.calledOnce(fakeSDK.initPublisher);
|
||||
sinon.assert.calledOnce(fakeSession.publish);
|
||||
});
|
||||
|
||||
// XXX This test would need reworking, but the code should be going
|
||||
// away after the obsolences of call urls (currently bug 1170150).
|
||||
it("should start listening to OT publisher accessDialogOpened and " +
|
||||
" accessDenied events");
|
||||
});
|
||||
|
||||
describe("#stopPublishing", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(fakePublisher, "off");
|
||||
comp.startPublishing();
|
||||
});
|
||||
|
||||
it("should stop publish local stream", function() {
|
||||
comp.stopPublishing();
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.unpublish);
|
||||
});
|
||||
|
||||
it("should unsubscribe from publisher events",
|
||||
function() {
|
||||
comp.stopPublishing();
|
||||
|
||||
// Note: Backbone.Events#stopListening calls off() on passed object.
|
||||
sinon.assert.calledOnce(fakePublisher.off);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#publishStream", function() {
|
||||
var component;
|
||||
|
||||
beforeEach(function() {
|
||||
component = mountTestComponent({
|
||||
sdk: fakeSDK,
|
||||
model: model,
|
||||
video: {enabled: false}
|
||||
});
|
||||
component.startPublishing();
|
||||
});
|
||||
|
||||
it("should start streaming local audio", function() {
|
||||
component.publishStream("audio", true);
|
||||
|
||||
sinon.assert.calledOnce(fakePublisher.publishAudio);
|
||||
sinon.assert.calledWithExactly(fakePublisher.publishAudio, true);
|
||||
});
|
||||
|
||||
it("should stop streaming local audio", function() {
|
||||
component.publishStream("audio", false);
|
||||
|
||||
sinon.assert.calledOnce(fakePublisher.publishAudio);
|
||||
sinon.assert.calledWithExactly(fakePublisher.publishAudio, false);
|
||||
});
|
||||
|
||||
it("should start streaming local video", function() {
|
||||
component.publishStream("video", true);
|
||||
|
||||
sinon.assert.calledOnce(fakePublisher.publishVideo);
|
||||
sinon.assert.calledWithExactly(fakePublisher.publishVideo, true);
|
||||
});
|
||||
|
||||
it("should stop streaming local video", function() {
|
||||
component.publishStream("video", false);
|
||||
|
||||
sinon.assert.calledOnce(fakePublisher.publishVideo);
|
||||
sinon.assert.calledWithExactly(fakePublisher.publishVideo, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Model events", function() {
|
||||
|
||||
describe("for standalone", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
// In standalone, navigator.mozLoop does not exists
|
||||
if (navigator.hasOwnProperty("mozLoop")) {
|
||||
sandbox.stub(navigator, "mozLoop", undefined);
|
||||
}
|
||||
});
|
||||
|
||||
it("should play a connected sound, once, on session:connected",
|
||||
function() {
|
||||
var url = "shared/sounds/connected.ogg";
|
||||
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
|
||||
model.trigger("session:connected");
|
||||
|
||||
fakeAudioXHR.onload();
|
||||
|
||||
sinon.assert.called(fakeAudioXHR.open);
|
||||
sinon.assert.calledWithExactly(fakeAudioXHR.open, "GET", url, true);
|
||||
|
||||
sinon.assert.calledOnce(fakeAudio.play);
|
||||
expect(fakeAudio.loop).to.not.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for desktop", function() {
|
||||
var origMozLoop;
|
||||
|
||||
beforeEach(function() {
|
||||
origMozLoop = navigator.mozLoop;
|
||||
navigator.mozLoop = {
|
||||
getAudioBlob: sinon.spy(function(name, callback) {
|
||||
var data = new ArrayBuffer(10);
|
||||
callback(null, new Blob([data], {type: "audio/ogg"}));
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
navigator.mozLoop = origMozLoop;
|
||||
});
|
||||
|
||||
it("should play a connected sound, once, on session:connected",
|
||||
function() {
|
||||
var url = "chrome://browser/content/loop/shared/sounds/connected.ogg";
|
||||
model.trigger("session:connected");
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
|
||||
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
|
||||
"connected", sinon.match.func);
|
||||
sinon.assert.calledOnce(fakeAudio.play);
|
||||
expect(fakeAudio.loop).to.not.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("for both (standalone and desktop)", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
|
||||
});
|
||||
|
||||
it("should start streaming on session:connected", function() {
|
||||
model.trigger("session:connected");
|
||||
|
||||
sinon.assert.calledOnce(fakeSDK.initPublisher);
|
||||
});
|
||||
|
||||
it("should publish remote stream on session:stream-created",
|
||||
function() {
|
||||
var s1 = {connection: {connectionId: 42}};
|
||||
|
||||
model.trigger("session:stream-created", {stream: s1});
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.subscribe);
|
||||
sinon.assert.calledWith(fakeSession.subscribe, s1);
|
||||
});
|
||||
|
||||
it("should unpublish local stream on session:ended", function() {
|
||||
comp.startPublishing();
|
||||
|
||||
model.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.unpublish);
|
||||
});
|
||||
|
||||
it("should unpublish local stream on session:peer-hungup", function() {
|
||||
comp.startPublishing();
|
||||
|
||||
model.trigger("session:peer-hungup");
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.unpublish);
|
||||
});
|
||||
|
||||
it("should unpublish local stream on session:network-disconnected",
|
||||
function() {
|
||||
comp.startPublishing();
|
||||
|
||||
model.trigger("session:network-disconnected");
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.unpublish);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Publisher events", function() {
|
||||
beforeEach(function() {
|
||||
comp.startPublishing();
|
||||
});
|
||||
|
||||
it("should set audio state on streamCreated", function() {
|
||||
fakePublisher.trigger("streamCreated", {stream: {hasAudio: true}});
|
||||
expect(comp.state.audio.enabled).eql(true);
|
||||
|
||||
fakePublisher.trigger("streamCreated", {stream: {hasAudio: false}});
|
||||
expect(comp.state.audio.enabled).eql(false);
|
||||
});
|
||||
|
||||
it("should set video state on streamCreated", function() {
|
||||
fakePublisher.trigger("streamCreated", {stream: {hasVideo: true}});
|
||||
expect(comp.state.video.enabled).eql(true);
|
||||
|
||||
fakePublisher.trigger("streamCreated", {stream: {hasVideo: false}});
|
||||
expect(comp.state.video.enabled).eql(false);
|
||||
});
|
||||
|
||||
it("should set media state on streamDestroyed", function() {
|
||||
fakePublisher.trigger("streamDestroyed");
|
||||
|
||||
expect(comp.state.audio.enabled).eql(false);
|
||||
expect(comp.state.video.enabled).eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationListView", function() {
|
||||
var coll, view, testNotif;
|
||||
|
||||
|
@ -45,7 +45,6 @@
|
||||
</script>
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/utils.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/mixins.js"></script>
|
||||
<script src="../../content/shared/js/websocket.js"></script>
|
||||
<script src="../../content/shared/js/actions.js"></script>
|
||||
@ -58,21 +57,17 @@
|
||||
<script src="../../content/shared/js/textChatStore.js"></script>
|
||||
<script src="../../content/shared/js/textChatView.js"></script>
|
||||
<script src="../../content/shared/js/otSdkDriver.js"></script>
|
||||
<script src="../../standalone/content/js/multiplexGum.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneClient.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneMozLoop.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
|
||||
<script src="../../standalone/content/js/standaloneMetricsStore.js"></script>
|
||||
<script src="../../standalone/content/js/webapp.js"></script>
|
||||
<!-- Test scripts -->
|
||||
<script src="standalone_client_test.js"></script>
|
||||
<script src="standaloneAppStore_test.js"></script>
|
||||
<script src="standaloneMozLoop_test.js"></script>
|
||||
<script src="standaloneRoomViews_test.js"></script>
|
||||
<script src="standaloneMetricsStore_test.js"></script>
|
||||
<script src="webapp_test.js"></script>
|
||||
<script src="multiplexGum_test.js"></script>
|
||||
<script>
|
||||
describe("Uncaught Error Check", function() {
|
||||
it("should load the tests without errors", function() {
|
||||
|
@ -1,363 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
describe("loop.standaloneMedia._MultiplexGum", function() {
|
||||
"use strict";
|
||||
|
||||
var expect = chai.expect;
|
||||
var defaultGum =
|
||||
navigator.getUserMedia ||
|
||||
navigator.mozGetUserMedia ||
|
||||
navigator.webkitGetUserMedia ||
|
||||
(window.TBPlugin && window.TBPlugin.getUserMedia);
|
||||
|
||||
var sandbox;
|
||||
var multiplexGum;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
multiplexGum = new loop.standaloneMedia._MultiplexGum();
|
||||
loop.standaloneMedia.setSingleton(multiplexGum);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("#constructor", function() {
|
||||
it("pending should default to false", function() {
|
||||
expect(multiplexGum.userMedia.pending).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("default getUserMedia", function() {
|
||||
it("should call getPermsAndCacheMedia", function() {
|
||||
var fakeOptions = {audio: true, video: true};
|
||||
var successCB = function() {};
|
||||
var errorCB = function() {};
|
||||
sandbox.stub(navigator, "originalGum");
|
||||
sandbox.stub(loop.standaloneMedia._MultiplexGum.prototype,
|
||||
"getPermsAndCacheMedia");
|
||||
multiplexGum = new loop.standaloneMedia._MultiplexGum();
|
||||
|
||||
defaultGum(fakeOptions, successCB, errorCB);
|
||||
|
||||
sinon.assert.calledOnce(multiplexGum.getPermsAndCacheMedia);
|
||||
sinon.assert.calledWithExactly(multiplexGum.getPermsAndCacheMedia,
|
||||
fakeOptions, successCB, errorCB);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getPermsAndCacheMedia", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(navigator, "originalGum");
|
||||
});
|
||||
|
||||
it("should change pending to true", function() {
|
||||
multiplexGum.getPermsAndCacheMedia();
|
||||
|
||||
expect(multiplexGum.userMedia.pending).to.equal(true);
|
||||
});
|
||||
|
||||
it("should call originalGum", function() {
|
||||
multiplexGum.getPermsAndCacheMedia();
|
||||
|
||||
sinon.assert.calledOnce(navigator.originalGum);
|
||||
});
|
||||
|
||||
it("should reset the pending state when the error callback is called",
|
||||
function(done) {
|
||||
var fakeError = new Error();
|
||||
navigator.originalGum.callsArgWith(2, fakeError);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
|
||||
expect(multiplexGum.userMedia.pending).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should reset the pending state when the success callback is called",
|
||||
function(done) {
|
||||
var fakeLocalStream = {};
|
||||
navigator.originalGum.callsArgWith(1, fakeLocalStream);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null,
|
||||
function onSuccess(localStream) {
|
||||
expect(multiplexGum.userMedia.pending).to.equal(false);
|
||||
done();
|
||||
}, null);
|
||||
});
|
||||
|
||||
it("should call the error callback when originalGum calls back an error",
|
||||
function(done) {
|
||||
var fakeError = new Error();
|
||||
navigator.originalGum.callsArgWith(2, fakeError);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
|
||||
expect(error).to.eql(fakeError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should propagate the success callback when originalGum succeeds",
|
||||
function(done) {
|
||||
var fakeLocalStream = {};
|
||||
navigator.originalGum.callsArgWith(1, fakeLocalStream);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null,
|
||||
function onSuccess(localStream) {
|
||||
expect(localStream).to.eql(fakeLocalStream);
|
||||
done();
|
||||
}, null);
|
||||
});
|
||||
|
||||
it("should call the success callback when the stream is cached",
|
||||
function(done) {
|
||||
var fakeLocalStream = {};
|
||||
multiplexGum.userMedia.localStream = fakeLocalStream;
|
||||
sinon.assert.notCalled(navigator.originalGum);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null,
|
||||
function onSuccess(localStream) {
|
||||
expect(localStream).to.eql(fakeLocalStream);
|
||||
done();
|
||||
}, null);
|
||||
});
|
||||
|
||||
it("should call the error callback when an error is cached",
|
||||
function(done) {
|
||||
var fakeError = new Error();
|
||||
multiplexGum.userMedia.error = fakeError;
|
||||
sinon.assert.notCalled(navigator.originalGum);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
|
||||
expect(error).to.eql(fakeError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear the error when success is called back", function(done) {
|
||||
var fakeError = new Error();
|
||||
var fakeLocalStream = {};
|
||||
multiplexGum.userMedia.localStream = fakeLocalStream;
|
||||
multiplexGum.userMedia.error = fakeError;
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, function onSuccess(localStream) {
|
||||
expect(multiplexGum.userMedia.error).to.not.eql(fakeError);
|
||||
expect(localStream).to.eql(fakeLocalStream);
|
||||
done();
|
||||
}, null);
|
||||
});
|
||||
|
||||
it("should call all success callbacks when success is achieved",
|
||||
function(done) {
|
||||
var fakeLocalStream = {};
|
||||
var calls = 0;
|
||||
// Async is needed so that the callbacks can be queued up.
|
||||
navigator.originalGum.callsArgWithAsync(1, fakeLocalStream);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, function onSuccess(localStream) {
|
||||
calls += 1;
|
||||
expect(localStream).to.eql(fakeLocalStream);
|
||||
}, null);
|
||||
|
||||
expect(multiplexGum.userMedia).to.have.property("pending", true);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, function onSuccess(localStream) {
|
||||
calls += 10;
|
||||
expect(localStream).to.eql(fakeLocalStream);
|
||||
expect(calls).to.equal(11);
|
||||
done();
|
||||
}, null);
|
||||
});
|
||||
|
||||
it("should call all error callbacks when error is encountered",
|
||||
function(done) {
|
||||
var fakeError = new Error();
|
||||
var calls = 0;
|
||||
// Async is needed so that the callbacks can be queued up.
|
||||
navigator.originalGum.callsArgWithAsync(2, fakeError);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
|
||||
calls += 1;
|
||||
expect(error).to.eql(fakeError);
|
||||
});
|
||||
|
||||
expect(multiplexGum.userMedia).to.have.property("pending", true);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, null, function onError(error) {
|
||||
calls += 10;
|
||||
expect(error).to.eql(fakeError);
|
||||
expect(calls).to.eql(11);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call a getPermsAndCacheMedia success callback at the time" +
|
||||
" of gUM success callback fires",
|
||||
function() {
|
||||
var fakeLocalStream = {};
|
||||
multiplexGum.userMedia.localStream = fakeLocalStream;
|
||||
navigator.originalGum.callsArgWith(1, fakeLocalStream);
|
||||
var calledOnce = false;
|
||||
var promiseCalledOnce = new Promise(function(resolve, reject) {
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null,
|
||||
function gPACMSuccess(localStream) {
|
||||
expect(localStream).to.eql(fakeLocalStream);
|
||||
expect(multiplexGum.userMedia).to.have.property("pending", false);
|
||||
expect(multiplexGum.userMedia.successCallbacks.length).to.equal(0);
|
||||
if (calledOnce) {
|
||||
sinon.assert.fail("original callback was called twice");
|
||||
}
|
||||
calledOnce = true;
|
||||
resolve();
|
||||
}, function() {
|
||||
sinon.assert.fail("error callback should not have fired");
|
||||
reject();
|
||||
});
|
||||
});
|
||||
|
||||
return promiseCalledOnce.then(function() {
|
||||
defaultGum(null, function gUMSuccess(localStream2) {
|
||||
expect(localStream2).to.eql(fakeLocalStream);
|
||||
expect(multiplexGum.userMedia).to.have.property("pending", false);
|
||||
expect(multiplexGum.userMedia.successCallbacks.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call a getPermsAndCacheMedia error callback when the " +
|
||||
" gUM error callback fires",
|
||||
function() {
|
||||
var fakeError = "monkeys ate the stream";
|
||||
multiplexGum.userMedia.error = fakeError;
|
||||
navigator.originalGum.callsArgWith(2, fakeError);
|
||||
var calledOnce = false;
|
||||
var promiseCalledOnce = new Promise(function(resolve, reject) {
|
||||
multiplexGum.getPermsAndCacheMedia(null, function() {
|
||||
sinon.assert.fail("success callback should not have fired");
|
||||
reject();
|
||||
}, function gPACMError(errString) {
|
||||
expect(errString).to.eql(fakeError);
|
||||
expect(multiplexGum.userMedia).to.have.property("pending", false);
|
||||
if (calledOnce) {
|
||||
sinon.assert.fail("original error callback was called twice");
|
||||
}
|
||||
calledOnce = true;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return promiseCalledOnce.then(function() {
|
||||
defaultGum(null, function() {},
|
||||
function gUMError(errString) {
|
||||
expect(errString).to.eql(fakeError);
|
||||
expect(multiplexGum.userMedia).to.have.property("pending", false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should call the success callback with a new stream, " +
|
||||
" when a new stream is available",
|
||||
function(done) {
|
||||
var endedStream = {ended: true};
|
||||
var newStream = {};
|
||||
multiplexGum.userMedia.localStream = endedStream;
|
||||
navigator.originalGum.callsArgWith(1, newStream);
|
||||
|
||||
multiplexGum.getPermsAndCacheMedia(null, function onSuccess(localStream) {
|
||||
expect(localStream).to.eql(newStream);
|
||||
done();
|
||||
}, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#reset", function () {
|
||||
it("should reset all userMedia state to default", function() {
|
||||
// If userMedia is defined, then it needs to have all of
|
||||
// the properties that multipleGum will depend on. It is
|
||||
// easier to simply delete the object than to setup a fake
|
||||
// state of the object.
|
||||
delete multiplexGum.userMedia;
|
||||
|
||||
multiplexGum.reset();
|
||||
|
||||
expect(multiplexGum.userMedia).to.deep.equal({
|
||||
error: null,
|
||||
localStream: null,
|
||||
pending: false,
|
||||
errorCallbacks: [],
|
||||
successCallbacks: []
|
||||
});
|
||||
});
|
||||
|
||||
it("should call all queued error callbacks with 'PERMISSION_DENIED'",
|
||||
function(done) {
|
||||
sandbox.stub(navigator, "originalGum");
|
||||
multiplexGum.getPermsAndCacheMedia(null, function(localStream) {
|
||||
sinon.assert.fail(
|
||||
"The success callback shouldn't be called due to reset");
|
||||
}, function(error) {
|
||||
expect(error).to.equal("PERMISSION_DENIED");
|
||||
done();
|
||||
});
|
||||
multiplexGum.reset();
|
||||
});
|
||||
|
||||
it("should call MST.stop() on the stream tracks", function() {
|
||||
var stopStub = sandbox.stub();
|
||||
multiplexGum.userMedia.localStream = {stop: stopStub};
|
||||
|
||||
multiplexGum.reset();
|
||||
|
||||
sinon.assert.calledOnce(stopStub);
|
||||
});
|
||||
|
||||
it("should not call MST.stop() on the stream tracks if .stop() doesn't exist",
|
||||
function() {
|
||||
multiplexGum.userMedia.localStream = {};
|
||||
|
||||
try {
|
||||
multiplexGum.reset();
|
||||
} catch (ex) {
|
||||
sinon.assert.fail(
|
||||
"reset shouldn't throw when a stream doesn't implement stop(): "
|
||||
+ ex);
|
||||
}
|
||||
});
|
||||
|
||||
it("should not get stuck in recursion if the error callback calls 'reset'",
|
||||
function() {
|
||||
sandbox.stub(navigator, "originalGum");
|
||||
navigator.originalGum.callsArgWith(2, "PERMISSION_DENIED");
|
||||
|
||||
var calledOnce = false;
|
||||
multiplexGum.getPermsAndCacheMedia(null, null, function() {
|
||||
if (calledOnce) {
|
||||
sinon.assert.fail("reset should only be called once");
|
||||
}
|
||||
calledOnce = true;
|
||||
multiplexGum.reset.bind(multiplexGum)();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not get stuck in recursion if the success callback calls 'reset'",
|
||||
function() {
|
||||
sandbox.stub(navigator, "originalGum");
|
||||
navigator.originalGum.callsArgWith(1, {});
|
||||
|
||||
var calledOnce = false;
|
||||
multiplexGum.getPermsAndCacheMedia(null, function() {
|
||||
calledOnce = true;
|
||||
multiplexGum.reset.bind(multiplexGum)();
|
||||
}, function() {
|
||||
if (calledOnce) {
|
||||
sinon.assert.fail("reset should only be called once");
|
||||
}
|
||||
calledOnce = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -22,9 +22,8 @@ describe("loop.store.StandaloneAppStore", function () {
|
||||
it("should throw an error if the dispatcher is missing", function() {
|
||||
expect(function() {
|
||||
new loop.store.StandaloneAppStore({
|
||||
sdk: {},
|
||||
helper: {},
|
||||
conversation: {}
|
||||
sdk: {}
|
||||
});
|
||||
}).to.Throw(/dispatcher/);
|
||||
});
|
||||
@ -33,25 +32,14 @@ describe("loop.store.StandaloneAppStore", function () {
|
||||
expect(function() {
|
||||
new loop.store.StandaloneAppStore({
|
||||
dispatcher: dispatcher,
|
||||
helper: {},
|
||||
conversation: {}
|
||||
});
|
||||
}).to.Throw(/sdk/);
|
||||
});
|
||||
|
||||
it("should throw an error if conversation is missing", function() {
|
||||
expect(function() {
|
||||
new loop.store.StandaloneAppStore({
|
||||
dispatcher: dispatcher,
|
||||
sdk: {},
|
||||
helper: {}
|
||||
});
|
||||
}).to.Throw(/conversation/);
|
||||
}).to.Throw(/sdk/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#extractTokenInfo", function() {
|
||||
var store, fakeGetWindowData, fakeSdk, fakeConversation, helper;
|
||||
var store, fakeGetWindowData, fakeSdk, helper;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeGetWindowData = {
|
||||
@ -66,17 +54,12 @@ describe("loop.store.StandaloneAppStore", function () {
|
||||
checkSystemRequirements: sinon.stub().returns(true)
|
||||
};
|
||||
|
||||
fakeConversation = {
|
||||
set: sinon.spy()
|
||||
};
|
||||
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
store = new loop.store.StandaloneAppStore({
|
||||
dispatcher: dispatcher,
|
||||
sdk: fakeSdk,
|
||||
helper: helper,
|
||||
conversation: fakeConversation
|
||||
sdk: fakeSdk
|
||||
});
|
||||
});
|
||||
|
||||
@ -153,32 +136,6 @@ describe("loop.store.StandaloneAppStore", function () {
|
||||
expect(store.getStoreState().windowType).eql("home");
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for call paths",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "/c/fakecalltoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
sinon.assert.calledOnce(fakeConversation.set);
|
||||
sinon.assert.calledWithExactly(fakeConversation.set, {
|
||||
loopToken: "fakecalltoken"
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the loopToken on the conversation for room paths",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "/c/fakeroomtoken";
|
||||
|
||||
store.extractTokenInfo(
|
||||
new sharedActions.ExtractTokenInfo(fakeGetWindowData));
|
||||
|
||||
sinon.assert.calledOnce(fakeConversation.set);
|
||||
sinon.assert.calledWithExactly(fakeConversation.set, {
|
||||
loopToken: "fakeroomtoken"
|
||||
});
|
||||
});
|
||||
|
||||
it("should dispatch a FetchServerData action for call paths",
|
||||
function() {
|
||||
fakeGetWindowData.windowPath = "/c/fakecalltoken";
|
||||
|
@ -1,179 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
describe("loop.StandaloneClient", function() {
|
||||
"use strict";
|
||||
|
||||
var expect = chai.expect;
|
||||
var sandbox,
|
||||
fakeXHR,
|
||||
requests = [],
|
||||
callback,
|
||||
fakeToken;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
fakeXHR = sandbox.useFakeXMLHttpRequest();
|
||||
requests = [];
|
||||
// https://github.com/cjohansen/Sinon.JS/issues/393
|
||||
fakeXHR.xhr.onCreate = function (xhr) {
|
||||
requests.push(xhr);
|
||||
};
|
||||
callback = sinon.spy();
|
||||
fakeToken = "fakeTokenText";
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.StandaloneClient", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a baseServerUrl setting", function() {
|
||||
expect(function() {
|
||||
new loop.StandaloneClient();
|
||||
}).to.Throw(Error, /required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallUrlInfo", function() {
|
||||
var client, fakeServerErrorDescription;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.StandaloneClient(
|
||||
{baseServerUrl: "http://fake.api"}
|
||||
);
|
||||
});
|
||||
|
||||
describe("should make the requests to the server", function() {
|
||||
|
||||
it("should throw if loopToken is missing", function() {
|
||||
expect(client.requestCallUrlInfo).to
|
||||
.throw(/Missing required parameter loopToken/);
|
||||
});
|
||||
|
||||
it("should make a GET request for the call url creation date", function() {
|
||||
client.requestCallUrlInfo("fakeCallUrlToken", function() {});
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url)
|
||||
.to.eql("http://fake.api/calls/fakeCallUrlToken");
|
||||
expect(requests[0].method).to.eql("GET");
|
||||
});
|
||||
|
||||
it("should call the callback with (null, serverResponse)", function() {
|
||||
var successCallback = sandbox.spy(function() {});
|
||||
var serverResponse = {
|
||||
calleeFriendlyName: "Andrei",
|
||||
urlCreationDate: 0
|
||||
};
|
||||
|
||||
client.requestCallUrlInfo("fakeCallUrlToken", successCallback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(serverResponse));
|
||||
|
||||
sinon.assert.calledWithExactly(successCallback,
|
||||
null,
|
||||
serverResponse);
|
||||
});
|
||||
|
||||
it("should log the error if the requests fails", function() {
|
||||
sinon.stub(console, "error");
|
||||
var serverResponse = {error: true};
|
||||
var error = JSON.stringify(serverResponse);
|
||||
|
||||
client.requestCallUrlInfo("fakeCallUrlToken", sandbox.stub());
|
||||
requests[0].respond(404, {"Content-Type": "application/json"},
|
||||
error);
|
||||
|
||||
sinon.assert.calledOnce(console.error);
|
||||
sinon.assert.calledWithExactly(console.error, "Server error",
|
||||
"HTTP 404 Not Found", serverResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe("requestCallInfo", function() {
|
||||
var client, fakeServerErrorDescription;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.StandaloneClient(
|
||||
{baseServerUrl: "http://fake.api"}
|
||||
);
|
||||
fakeServerErrorDescription = {
|
||||
code: 401,
|
||||
errno: 101,
|
||||
error: "error",
|
||||
message: "invalid token",
|
||||
info: "error info"
|
||||
};
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when token is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallInfo();
|
||||
}).to.Throw(Error, /missing.*[Tt]oken/);
|
||||
});
|
||||
|
||||
it("should post data for the given call", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls/fake");
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
expect(requests[0].requestBody).to.be.equal('{"callType":"audio","channel":"standalone"}');
|
||||
});
|
||||
|
||||
it("should receive call data for the given call", function() {
|
||||
client.requestCallInfo("fake", "audio-video", callback);
|
||||
|
||||
var sessionData = {
|
||||
sessionId: "one",
|
||||
sessionToken: "two",
|
||||
apiKey: "three"
|
||||
};
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(sessionData));
|
||||
sinon.assert.calledWithExactly(callback, null, sessionData);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
requests[0].respond(401, {"Content-Type": "application/json"},
|
||||
JSON.stringify(fakeServerErrorDescription));
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /HTTP 401 Unauthorized/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should attach the server error description object to the error " +
|
||||
"passed to the callback",
|
||||
function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
requests[0].respond(401, {"Content-Type": "application/json"},
|
||||
JSON.stringify(fakeServerErrorDescription));
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return err.errno === fakeServerErrorDescription.errno;
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallInfo("fake", "audio", callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": "one"}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -29,7 +29,6 @@
|
||||
<div id="results"></div>
|
||||
<script src="fake-mozLoop.js"></script>
|
||||
<script src="fake-l10n.js"></script>
|
||||
<script src="../content/js/multiplexGum.js"></script>
|
||||
<script src="../content/shared/libs/react-0.12.2.js"></script>
|
||||
<script src="../content/shared/libs/lodash-3.9.3.js"></script>
|
||||
<script src="../content/shared/libs/backbone-1.2.1.js"></script>
|
||||
@ -53,7 +52,6 @@
|
||||
<script src="../content/js/roomViews.js"></script>
|
||||
<script src="../content/js/conversationViews.js"></script>
|
||||
<script src="../content/js/client.js"></script>
|
||||
<script src="../standalone/content/js/multiplexGum.js"></script>
|
||||
<script src="../standalone/content/js/webapp.js"></script>
|
||||
<script src="../standalone/content/js/standaloneRoomViews.js"></script>
|
||||
<script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>
|
||||
|
@ -331,7 +331,7 @@ menuitem.bookmark-item {
|
||||
}
|
||||
|
||||
/* Stock icons for the menu bar items */
|
||||
menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip):not([endimage]) {
|
||||
menuitem:not([type]):not(.menuitem-tooltip):not(.menuitem-iconic-tooltip) {
|
||||
-moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic");
|
||||
}
|
||||
|
||||
@ -1636,6 +1636,14 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
|
||||
box-shadow: inset -5px 0 ThreeDShadow;
|
||||
}
|
||||
|
||||
.alltabs-endimage[muted] {
|
||||
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio-muted);
|
||||
}
|
||||
|
||||
.alltabs-endimage[soundplaying] {
|
||||
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
#sidebar-throbber[loading="true"] {
|
||||
list-style-image: url("chrome://global/skin/icons/loading_16.png");
|
||||
|
@ -2958,6 +2958,14 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
|
||||
box-shadow: inset -5px 0 ThreeDShadow;
|
||||
}
|
||||
|
||||
.alltabs-endimage[muted] {
|
||||
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio-muted);
|
||||
}
|
||||
|
||||
.alltabs-endimage[soundplaying] {
|
||||
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio);
|
||||
}
|
||||
|
||||
/* Bookmarks toolbar */
|
||||
#PlacesToolbarDropIndicator {
|
||||
list-style-image: url(chrome://browser/skin/places/toolbarDropMarker.png);
|
||||
|
@ -3,7 +3,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/. */
|
||||
|
||||
@import url("floating-scrollbars.css");
|
||||
@import url("chrome://browser/skin/devtools/floating-scrollbars.css");
|
||||
|
||||
scrollbar thumb {
|
||||
background-color: rgba(170,170,170,0.2) !important;
|
||||
|
@ -732,7 +732,7 @@ toolbar[brighttext] .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
|
||||
padding: var(--toolbarbutton-vertical-inner-padding) 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 1px;
|
||||
transition-property: background-color, border-color;
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
@ -773,22 +773,6 @@ toolbaritem[cui-areatype="toolbar"] > :-moz-any(@nestedButtons@) > .toolbarbutto
|
||||
|
||||
--toolbarbutton-checkedhover-backgroundcolor: rgba(90%,90%,90%,.4);
|
||||
}
|
||||
.findbar-button > .toolbarbutton-text,
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-text,
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-badge-stack,
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
|
||||
background-color: hsla(210,32%,93%,0);
|
||||
background-origin: padding-box;
|
||||
border-radius: 2px;
|
||||
border-color: hsla(210,54%,20%,0) hsla(210,54%,20%,0) hsla(210,54%,20%,0);
|
||||
box-shadow: 0 1px hsla(0,0%,100%,0) inset,
|
||||
0 1px hsla(210,54%,20%,0),
|
||||
0 0 2px hsla(210,54%,20%,0);
|
||||
transition-property: background-color, border-color, box-shadow;
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon:-moz-locale-dir(ltr),
|
||||
@ -893,26 +877,6 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
||||
box-shadow: var(--toolbarbutton-hover-boxshadow);
|
||||
}
|
||||
|
||||
@media (-moz-os-version: windows-xp),
|
||||
(-moz-os-version: windows-vista),
|
||||
(-moz-os-version: windows-win7) {
|
||||
/* < Win8 */
|
||||
#nav-bar .toolbarbutton-1:not(:hover):not(:active):not([open]) > .toolbarbutton-menubutton-dropmarker::before,
|
||||
#nav-bar .toolbaritem-combined-buttons > .toolbarbutton-1:-moz-any(:not(:hover):not([open]),[disabled]) + .toolbarbutton-1:-moz-any(:not(:hover):not([open]),[disabled])::before {
|
||||
height: 18px;
|
||||
background-size: 1px 18px;
|
||||
}
|
||||
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button:not([disabled]):not([open]):not(:active):hover > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1:not([buttonover]):not([open]):not(:active):hover > .toolbarbutton-menubutton-dropmarker:not([disabled]) > .dropmarker-icon,
|
||||
@conditionalForwardWithUrlbar@ > #forward-button:not([open]):not(:active):not([disabled]):hover > .toolbarbutton-icon {
|
||||
border-color: hsla(210,54%,20%,.3) hsla(210,54%,20%,.35) hsla(210,54%,20%,.4);
|
||||
background-color: hsla(210,48%,96%,.75);
|
||||
box-shadow: 0 0 1px hsla(210,54%,20%,.03),
|
||||
0 0 2px hsla(210,54%,20%,.1);
|
||||
}
|
||||
}
|
||||
|
||||
.findbar-button:not([disabled=true]):-moz-any([checked="true"],:hover:active) > .toolbarbutton-text,
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button:not([disabled=true]):-moz-any(:hover:active, [open]) > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1[open] > .toolbarbutton-menubutton-dropmarker:not([disabled=true]) > .dropmarker-icon,
|
||||
@ -925,28 +889,9 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
||||
transition-duration: 10ms;
|
||||
}
|
||||
|
||||
@media (-moz-os-version: windows-xp),
|
||||
(-moz-os-version: windows-vista),
|
||||
(-moz-os-version: windows-win7) {
|
||||
/* < Win8 */
|
||||
.findbar-button:not([disabled=true]):-moz-any([checked="true"],:hover:active) > .toolbarbutton-text,
|
||||
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button:not([disabled=true]):-moz-any(:hover:active, [open]) > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1[open] > .toolbarbutton-menubutton-dropmarker:not([disabled]) > .dropmarker-icon,
|
||||
#nav-bar .toolbarbutton-1:not([disabled]):-moz-any([open],[checked],:hover:active) > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1:not([disabled]):-moz-any([open],[checked],:hover:active) > .toolbarbutton-text,
|
||||
#nav-bar .toolbarbutton-1:not([disabled]):-moz-any([open],[checked],:hover:active) > .toolbarbutton-badge-stack {
|
||||
text-shadow: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
#nav-bar .toolbarbutton-1:-moz-any(:hover,[open]) > .toolbarbutton-menubutton-dropmarker:not([disabled]) > .dropmarker-icon {
|
||||
-moz-border-start-color: hsla(210,54%,20%,.35);
|
||||
}
|
||||
|
||||
#nav-bar .toolbarbutton-1[checked]:not(:active):hover > .toolbarbutton-icon {
|
||||
background-color: var(--toolbarbutton-checkedhover-backgroundcolor);
|
||||
transition: background-color .4s;
|
||||
}
|
||||
#nav-bar .toolbarbutton-1[checked]:not(:active):hover > .toolbarbutton-icon {
|
||||
background-color: var(--toolbarbutton-checkedhover-backgroundcolor);
|
||||
transition: background-color .4s;
|
||||
}
|
||||
|
||||
#TabsToolbar .toolbarbutton-1,
|
||||
@ -2245,6 +2190,14 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
|
||||
box-shadow: inset -5px 0 ThreeDShadow;
|
||||
}
|
||||
|
||||
.alltabs-endimage[muted] {
|
||||
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio-muted);
|
||||
}
|
||||
|
||||
.alltabs-endimage[soundplaying] {
|
||||
list-style-image: url(chrome://browser/skin/tabbrowser/tab-audio.svg#tab-audio);
|
||||
}
|
||||
|
||||
toolbarbutton.chevron {
|
||||
list-style-image: url("chrome://global/skin/toolbar/chevron.gif") !important;
|
||||
}
|
||||
|
@ -8717,6 +8717,10 @@ AC_SUBST(MOZ_CHILD_PROCESS_BUNDLE)
|
||||
# "Profile" field, which controls profile location.
|
||||
# - MOZ_APP_ID: When set, used for application.ini's "ID" field, and
|
||||
# crash reporter server url.
|
||||
# - MOZ_APP_ANDROID_VERSION_CODE: On android, "android:versionCode" for
|
||||
# the main application is set to the value of this variable. If not
|
||||
# set, it falls back to a Mozilla-specific value derived from the
|
||||
# build ID.
|
||||
# - MOZ_PROFILE_MIGRATOR: When set, enables profile migrator.
|
||||
|
||||
if test -z "$MOZ_APP_NAME"; then
|
||||
@ -8760,6 +8764,7 @@ AC_SUBST(MOZ_APP_BASENAME)
|
||||
AC_SUBST(MOZ_APP_VENDOR)
|
||||
AC_SUBST(MOZ_APP_PROFILE)
|
||||
AC_SUBST(MOZ_APP_ID)
|
||||
AC_SUBST(MOZ_APP_ANDROID_VERSION_CODE)
|
||||
AC_SUBST(MAR_CHANNEL_ID)
|
||||
AC_SUBST(ACCEPTED_MAR_CHANNEL_IDS)
|
||||
AC_SUBST(MOZ_PROFILE_MIGRATOR)
|
||||
|
@ -757,8 +757,8 @@ this.AppsUtils = {
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
// Returns the MD5 hash of a string.
|
||||
computeHash: function(aString) {
|
||||
// Returns the hash of a string, with MD5 as a default hashing function.
|
||||
computeHash: function(aString, aAlgorithm = "MD5") {
|
||||
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
@ -768,7 +768,7 @@ this.AppsUtils = {
|
||||
|
||||
let hasher = Cc["@mozilla.org/security/hash;1"]
|
||||
.createInstance(Ci.nsICryptoHash);
|
||||
hasher.init(hasher.MD5);
|
||||
hasher.initWithString(aAlgorithm);
|
||||
hasher.update(data, data.length);
|
||||
// We're passing false to get the binary hash and not base64.
|
||||
let hash = hasher.finish(false);
|
||||
|
1254
dom/bluetooth/bluedroid/BluetoothDaemonCoreInterface.cpp
Normal file
1254
dom/bluetooth/bluedroid/BluetoothDaemonCoreInterface.cpp
Normal file
File diff suppressed because it is too large
Load Diff
346
dom/bluetooth/bluedroid/BluetoothDaemonCoreInterface.h
Normal file
346
dom/bluetooth/bluedroid/BluetoothDaemonCoreInterface.h
Normal file
@ -0,0 +1,346 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
#ifndef mozilla_dom_bluetooth_bluedroid_BluetoothDaemonCoreInterface_h
|
||||
#define mozilla_dom_bluetooth_bluedroid_BluetoothDaemonCoreInterface_h
|
||||
|
||||
#include "BluetoothDaemonHelpers.h"
|
||||
#include "BluetoothInterface.h"
|
||||
#include "mozilla/ipc/DaemonRunnables.h"
|
||||
|
||||
BEGIN_BLUETOOTH_NAMESPACE
|
||||
|
||||
using mozilla::ipc::DaemonSocketPDU;
|
||||
using mozilla::ipc::DaemonSocketPDUHeader;
|
||||
using mozilla::ipc::DaemonSocketResultHandler;
|
||||
|
||||
class BluetoothDaemonCoreModule
|
||||
{
|
||||
public:
|
||||
enum {
|
||||
SERVICE_ID = 0x01
|
||||
};
|
||||
|
||||
enum {
|
||||
OPCODE_ERROR = 0x00,
|
||||
OPCODE_ENABLE = 0x01,
|
||||
OPCODE_DISABLE = 0x02,
|
||||
OPCODE_GET_ADAPTER_PROPERTIES = 0x03,
|
||||
OPCODE_GET_ADAPTER_PROPERTY = 0x04,
|
||||
OPCODE_SET_ADAPTER_PROPERTY = 0x05,
|
||||
OPCODE_GET_REMOTE_DEVICE_PROPERTIES = 0x06,
|
||||
OPCODE_GET_REMOTE_DEVICE_PROPERTY = 0x07,
|
||||
OPCODE_SET_REMOTE_DEVICE_PROPERTY = 0x08,
|
||||
OPCODE_GET_REMOTE_SERVICE_RECORD = 0x09,
|
||||
OPCODE_GET_REMOTE_SERVICES = 0x0a,
|
||||
OPCODE_START_DISCOVERY = 0x0b,
|
||||
OPCODE_CANCEL_DISCOVERY = 0x0c,
|
||||
OPCODE_CREATE_BOND = 0x0d,
|
||||
OPCODE_REMOVE_BOND = 0x0e,
|
||||
OPCODE_CANCEL_BOND = 0x0f,
|
||||
OPCODE_PIN_REPLY = 0x10,
|
||||
OPCODE_SSP_REPLY = 0x11,
|
||||
OPCODE_DUT_MODE_CONFIGURE = 0x12,
|
||||
OPCODE_DUT_MODE_SEND = 0x13,
|
||||
OPCODE_LE_TEST_MODE = 0x14,
|
||||
OPCODE_ADAPTER_STATE_CHANGED_NTF = 0x81,
|
||||
OPCODE_ADAPTER_PROPERTIES_NTF = 0x82,
|
||||
OPCODE_REMOTE_DEVICE_PROPERTIES_NTF = 0x83,
|
||||
OPCODE_DEVICE_FOUND_NTF = 0x84,
|
||||
OPCODE_DISCOVERY_STATE_CHANGED_NTF = 0x85,
|
||||
OPCODE_PIN_REQUEST_NTF = 0x86,
|
||||
OPCODE_SSP_REQUEST_NTF = 0x87,
|
||||
OPCODE_BOND_STATE_CHANGED_NTF = 0x88,
|
||||
OPCODE_ACL_STATE_CHANGED_NTF = 0x89,
|
||||
OPCODE_DUT_MODE_RECV_NTF = 0x8a,
|
||||
OPCODE_LE_TEST_MODE_NTF = 0x8b
|
||||
};
|
||||
|
||||
static const int MAX_NUM_CLIENTS;
|
||||
|
||||
virtual nsresult Send(DaemonSocketPDU* aPDU,
|
||||
DaemonSocketResultHandler* aRes) = 0;
|
||||
|
||||
void SetNotificationHandler(
|
||||
BluetoothNotificationHandler* aNotificationHandler);
|
||||
|
||||
BluetoothNotificationHandler* GetNotificationHandler();
|
||||
|
||||
//
|
||||
// Commands
|
||||
//
|
||||
|
||||
nsresult EnableCmd(BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult DisableCmd(BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult GetAdapterPropertiesCmd(BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult GetAdapterPropertyCmd(const nsAString& aName,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult SetAdapterPropertyCmd(const BluetoothNamedValue& aProperty,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult GetRemoteDevicePropertiesCmd(const nsAString& aRemoteAddr,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult GetRemoteDevicePropertyCmd(const nsAString& aRemoteAddr,
|
||||
const nsAString& aName,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult SetRemoteDevicePropertyCmd(const nsAString& aRemoteAddr,
|
||||
const BluetoothNamedValue& aProperty,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult GetRemoteServiceRecordCmd(const nsAString& aRemoteAddr,
|
||||
const uint8_t aUuid[16],
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult GetRemoteServicesCmd(const nsAString& aRemoteAddr,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult StartDiscoveryCmd(BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult CancelDiscoveryCmd(BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult CreateBondCmd(const nsAString& aBdAddr,
|
||||
BluetoothTransport aTransport,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult RemoveBondCmd(const nsAString& aBdAddr,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult CancelBondCmd(const nsAString& aBdAddr,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult PinReplyCmd(const nsAString& aBdAddr, bool aAccept,
|
||||
const nsAString& aPinCode,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult SspReplyCmd(const nsAString& aBdAddr, BluetoothSspVariant aVariant,
|
||||
bool aAccept, uint32_t aPasskey,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult DutModeConfigureCmd(bool aEnable, BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult DutModeSendCmd(uint16_t aOpcode, uint8_t* aBuf, uint8_t aLen,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
nsresult LeTestModeCmd(uint16_t aOpcode, uint8_t* aBuf, uint8_t aLen,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
protected:
|
||||
void HandleSvc(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
|
||||
|
||||
private:
|
||||
|
||||
//
|
||||
// Responses
|
||||
//
|
||||
|
||||
typedef mozilla::ipc::DaemonResultRunnable0<
|
||||
BluetoothResultHandler, void>
|
||||
ResultRunnable;
|
||||
|
||||
typedef mozilla::ipc::DaemonResultRunnable1<
|
||||
BluetoothResultHandler, void, BluetoothStatus, BluetoothStatus>
|
||||
ErrorRunnable;
|
||||
|
||||
void ErrorRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void EnableRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void DisableRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void GetAdapterPropertiesRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void GetAdapterPropertyRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void SetAdapterPropertyRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void GetRemoteDevicePropertiesRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void
|
||||
GetRemoteDevicePropertyRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void SetRemoteDevicePropertyRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
void GetRemoteServiceRecordRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
void GetRemoteServicesRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void StartDiscoveryRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
void CancelDiscoveryRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void CreateBondRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
void RemoveBondRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
void CancelBondRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void PinReplyRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
void SspReplyRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void DutModeConfigureRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void DutModeSendRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void LeTestModeRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU,
|
||||
BluetoothResultHandler* aRes);
|
||||
|
||||
void HandleRsp(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
|
||||
|
||||
//
|
||||
// Notifications
|
||||
//
|
||||
|
||||
class NotificationHandlerWrapper;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable1<
|
||||
NotificationHandlerWrapper, void, bool>
|
||||
AdapterStateChangedNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable3<
|
||||
NotificationHandlerWrapper, void, BluetoothStatus, int,
|
||||
nsAutoArrayPtr<BluetoothProperty>, BluetoothStatus, int,
|
||||
const BluetoothProperty*>
|
||||
AdapterPropertiesNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable4<
|
||||
NotificationHandlerWrapper, void, BluetoothStatus, nsString, int,
|
||||
nsAutoArrayPtr<BluetoothProperty>, BluetoothStatus, const nsAString&,
|
||||
int, const BluetoothProperty*>
|
||||
RemoteDevicePropertiesNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable2<
|
||||
NotificationHandlerWrapper, void, int, nsAutoArrayPtr<BluetoothProperty>,
|
||||
int, const BluetoothProperty*>
|
||||
DeviceFoundNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable1<
|
||||
NotificationHandlerWrapper, void, bool>
|
||||
DiscoveryStateChangedNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable3<
|
||||
NotificationHandlerWrapper, void, nsString, nsString, uint32_t,
|
||||
const nsAString&, const nsAString&>
|
||||
PinRequestNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable5<
|
||||
NotificationHandlerWrapper, void, nsString, nsString, uint32_t,
|
||||
BluetoothSspVariant, uint32_t, const nsAString&, const nsAString&>
|
||||
SspRequestNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable3<
|
||||
NotificationHandlerWrapper, void, BluetoothStatus, nsString,
|
||||
BluetoothBondState, BluetoothStatus, const nsAString&>
|
||||
BondStateChangedNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable3<
|
||||
NotificationHandlerWrapper, void, BluetoothStatus, nsString, bool,
|
||||
BluetoothStatus, const nsAString&>
|
||||
AclStateChangedNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable3<
|
||||
NotificationHandlerWrapper, void, uint16_t, nsAutoArrayPtr<uint8_t>,
|
||||
uint8_t, uint16_t, const uint8_t*>
|
||||
DutModeRecvNotification;
|
||||
|
||||
typedef mozilla::ipc::DaemonNotificationRunnable2<
|
||||
NotificationHandlerWrapper, void, BluetoothStatus, uint16_t>
|
||||
LeTestModeNotification;
|
||||
|
||||
class AclStateChangedInitOp;
|
||||
class AdapterPropertiesInitOp;
|
||||
class BondStateChangedInitOp;
|
||||
class DeviceFoundInitOp;
|
||||
class DutModeRecvInitOp;
|
||||
class PinRequestInitOp;
|
||||
class RemoteDevicePropertiesInitOp;
|
||||
class SspRequestInitOp;
|
||||
|
||||
void AdapterStateChangedNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void AdapterPropertiesNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void RemoteDevicePropertiesNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void DeviceFoundNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void DiscoveryStateChangedNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void PinRequestNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void SspRequestNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void BondStateChangedNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void AclStateChangedNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void DutModeRecvNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void LeTestModeNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU);
|
||||
|
||||
void HandleNtf(const DaemonSocketPDUHeader& aHeader,
|
||||
DaemonSocketPDU& aPDU, DaemonSocketResultHandler* aRes);
|
||||
|
||||
static BluetoothNotificationHandler* sNotificationHandler;
|
||||
};
|
||||
|
||||
END_BLUETOOTH_NAMESPACE
|
||||
|
||||
#endif // mozilla_dom_bluetooth_bluedroid_BluetoothDaemonCoreInterface_h
|
File diff suppressed because it is too large
Load Diff
@ -32,7 +32,9 @@ BluetoothDaemonSocketModule::ListenCmd(BluetoothSocketType aType,
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
nsAutoPtr<DaemonSocketPDU> pdu(new DaemonSocketPDU(0x02, 0x01, 0));
|
||||
nsAutoPtr<DaemonSocketPDU> pdu(
|
||||
new DaemonSocketPDU(SERVICE_ID, OPCODE_LISTEN,
|
||||
0));
|
||||
|
||||
nsresult rv = PackPDU(
|
||||
aType,
|
||||
@ -61,7 +63,9 @@ BluetoothDaemonSocketModule::ConnectCmd(const nsAString& aBdAddr,
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
nsAutoPtr<DaemonSocketPDU> pdu(new DaemonSocketPDU(0x02, 0x02, 0));
|
||||
nsAutoPtr<DaemonSocketPDU> pdu(
|
||||
new DaemonSocketPDU(SERVICE_ID, OPCODE_CONNECT,
|
||||
0));
|
||||
|
||||
nsresult rv = PackPDU(
|
||||
PackConversion<nsAString, BluetoothAddress>(aBdAddr),
|
||||
@ -166,9 +170,9 @@ BluetoothDaemonSocketModule::HandleSvc(const DaemonSocketPDUHeader& aHeader,
|
||||
const DaemonSocketPDUHeader&,
|
||||
DaemonSocketPDU&,
|
||||
BluetoothSocketResultHandler*) = {
|
||||
[0x00] = &BluetoothDaemonSocketModule::ErrorRsp,
|
||||
[0x01] = &BluetoothDaemonSocketModule::ListenRsp,
|
||||
[0x02] = &BluetoothDaemonSocketModule::ConnectRsp
|
||||
[OPCODE_ERROR] = &BluetoothDaemonSocketModule::ErrorRsp,
|
||||
[OPCODE_LISTEN] = &BluetoothDaemonSocketModule::ListenRsp,
|
||||
[OPCODE_CONNECT] = &BluetoothDaemonSocketModule::ConnectRsp
|
||||
};
|
||||
|
||||
if (NS_WARN_IF(MOZ_ARRAY_LENGTH(HandleRsp) <= aHeader.mOpcode) ||
|
||||
|
@ -20,6 +20,16 @@ using mozilla::ipc::DaemonSocketResultHandler;
|
||||
class BluetoothDaemonSocketModule
|
||||
{
|
||||
public:
|
||||
enum {
|
||||
SERVICE_ID = 0x02
|
||||
};
|
||||
|
||||
enum {
|
||||
OPCODE_ERROR = 0x00,
|
||||
OPCODE_LISTEN = 0x01,
|
||||
OPCODE_CONNECT = 0x02
|
||||
};
|
||||
|
||||
static const int MAX_NUM_CLIENTS;
|
||||
|
||||
virtual nsresult Send(DaemonSocketPDU* aPDU,
|
||||
|
@ -74,6 +74,7 @@ if CONFIG['MOZ_B2G_BT']:
|
||||
'bluedroid/BluetoothAvrcpManager.cpp',
|
||||
'bluedroid/BluetoothDaemonA2dpInterface.cpp',
|
||||
'bluedroid/BluetoothDaemonAvrcpInterface.cpp',
|
||||
'bluedroid/BluetoothDaemonCoreInterface.cpp',
|
||||
'bluedroid/BluetoothDaemonGattInterface.cpp',
|
||||
'bluedroid/BluetoothDaemonHandsfreeInterface.cpp',
|
||||
'bluedroid/BluetoothDaemonHelpers.cpp',
|
||||
|
@ -72,7 +72,8 @@ var CopyPasteAssistent = {
|
||||
collapsed: e.collapsed,
|
||||
caretVisible: e.caretVisible,
|
||||
selectionVisible: e.selectionVisible,
|
||||
selectionEditable: e.selectionEditable
|
||||
selectionEditable: e.selectionEditable,
|
||||
selectedTextContent: e.selectedTextContent
|
||||
};
|
||||
|
||||
// Get correct geometry information if we have nested iframe.
|
||||
|
@ -459,6 +459,8 @@ BrowserElementParent.prototype = {
|
||||
// - caretVisible: Indicate the caret visiibility.
|
||||
// - selectionVisible: Indicate current selection is visible or not.
|
||||
// - selectionEditable: Indicate current selection is editable or not.
|
||||
// - selectedTextContent: Contains current selected text content, which is
|
||||
// equivalent to the string returned by Selection.toString().
|
||||
_handleCaretStateChanged: function(data) {
|
||||
let evt = this._createEvent('caretstatechanged', data.json,
|
||||
/* cancelable = */ false);
|
||||
|
@ -20,6 +20,7 @@ dictionary CaretStateChangedEventInit : EventInit {
|
||||
boolean caretVisible = false;
|
||||
boolean selectionVisible = false;
|
||||
boolean selectionEditable = false;
|
||||
DOMString selectedTextContent = "";
|
||||
};
|
||||
|
||||
[Constructor(DOMString type, optional CaretStateChangedEventInit eventInit),
|
||||
@ -31,4 +32,5 @@ interface CaretStateChangedEvent : Event {
|
||||
readonly attribute boolean caretVisible;
|
||||
readonly attribute boolean selectionVisible;
|
||||
readonly attribute boolean selectionEditable;
|
||||
readonly attribute DOMString selectedTextContent;
|
||||
};
|
||||
|
@ -13,7 +13,7 @@
|
||||
#ifdef MOZ_NUWA_PROCESS
|
||||
#include "ipc/Nuwa.h"
|
||||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/dom/PContent.h"
|
||||
#include "mozilla/dom/ContentParent.h"
|
||||
#include "mozilla/dom/PNuwa.h"
|
||||
#include "mozilla/hal_sandbox/PHal.h"
|
||||
#if defined(DEBUG) || defined(ENABLE_TESTS)
|
||||
@ -144,7 +144,7 @@ ProcessLink::Open(mozilla::ipc::Transport* aTransport, MessageLoop *aIOLoop, Sid
|
||||
}
|
||||
|
||||
#ifdef MOZ_NUWA_PROCESS
|
||||
if (IsNuwaProcess() &&
|
||||
if (IsNuwaProcess() && NS_IsMainThread() &&
|
||||
Preferences::GetBool("dom.ipc.processPrelaunch.testMode")) {
|
||||
// The pref value is turned on in a deadlock test against the Nuwa
|
||||
// process. The sleep here makes it easy to trigger the deadlock
|
||||
|
@ -953,6 +953,7 @@ AccessibleCaretManager::DispatchCaretStateChangedEvent(CaretChangedReason aReaso
|
||||
init.mCollapsed = sel->IsCollapsed();
|
||||
init.mCaretVisible = mFirstCaret->IsLogicallyVisible() ||
|
||||
mSecondCaret->IsLogicallyVisible();
|
||||
sel->Stringify(init.mSelectedTextContent);
|
||||
|
||||
nsRefPtr<CaretStateChangedEvent> event =
|
||||
CaretStateChangedEvent::Constructor(doc, NS_LITERAL_STRING("mozcaretstatechanged"), init);
|
||||
|
@ -2751,7 +2751,15 @@ public class BrowserApp extends GeckoApp
|
||||
// prevents this issue.
|
||||
fm.executePendingTransactions();
|
||||
|
||||
fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
|
||||
Fragment f = fm.findFragmentById(R.id.search_container);
|
||||
|
||||
// checking if fragment is already present
|
||||
if (f != null) {
|
||||
fm.beginTransaction().show(f).commitAllowingStateLoss();
|
||||
} else {
|
||||
// add fragment if not already present
|
||||
fm.beginTransaction().add(R.id.search_container, mBrowserSearch, BROWSER_SEARCH_TAG).commitAllowingStateLoss();
|
||||
}
|
||||
mBrowserSearch.setUserVisibleHint(true);
|
||||
|
||||
// We want to adjust the window size when the keyboard appears to bring the
|
||||
@ -2777,7 +2785,7 @@ public class BrowserApp extends GeckoApp
|
||||
mBrowserSearchContainer.setVisibility(View.INVISIBLE);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.remove(mBrowserSearch).commitAllowingStateLoss();
|
||||
.hide(mBrowserSearch).commitAllowingStateLoss();
|
||||
mBrowserSearch.setUserVisibleHint(false);
|
||||
|
||||
getWindow().setBackgroundDrawable(null);
|
||||
|
@ -4,13 +4,19 @@
|
||||
|
||||
MOZ_APP_BUILDID=$(shell cat $(DEPTH)/config/buildid)
|
||||
|
||||
ANDROID_VERSION_CODE:=$(shell $(PYTHON) \
|
||||
$(topsrcdir)/python/mozbuild/mozbuild/android_version_code.py \
|
||||
--verbose \
|
||||
--with-android-cpu-arch=$(ANDROID_CPU_ARCH) \
|
||||
$(if $(MOZ_ANDROID_MIN_SDK_VERSION),--with-android-min-sdk=$(MOZ_ANDROID_MIN_SDK_VERSION)) \
|
||||
$(if $(MOZ_ANDROID_MAX_SDK_VERSION),--with-android-max-sdk=$(MOZ_ANDROID_MAX_SDK_VERSION)) \
|
||||
$(MOZ_APP_BUILDID))
|
||||
# Set the appropriate version code, based on the existance of the
|
||||
# MOZ_APP_ANDROID_VERSION_CODE variable.
|
||||
ifdef MOZ_APP_ANDROID_VERSION_CODE
|
||||
ANDROID_VERSION_CODE:=$(MOZ_APP_ANDROID_VERSION_CODE)
|
||||
else
|
||||
ANDROID_VERSION_CODE:=$(shell $(PYTHON) \
|
||||
$(topsrcdir)/python/mozbuild/mozbuild/android_version_code.py \
|
||||
--verbose \
|
||||
--with-android-cpu-arch=$(ANDROID_CPU_ARCH) \
|
||||
$(if $(MOZ_ANDROID_MIN_SDK_VERSION),--with-android-min-sdk=$(MOZ_ANDROID_MIN_SDK_VERSION)) \
|
||||
$(if $(MOZ_ANDROID_MAX_SDK_VERSION),--with-android-max-sdk=$(MOZ_ANDROID_MAX_SDK_VERSION)) \
|
||||
$(MOZ_APP_BUILDID))
|
||||
endif
|
||||
|
||||
DEFINES += \
|
||||
-DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \
|
||||
|
@ -21,7 +21,7 @@ import java.util.EnumSet;
|
||||
public class RestrictedWelcomePanel extends FirstrunPanel {
|
||||
public static final int TITLE_RES = R.string.firstrun_panel_title_welcome;
|
||||
|
||||
private static final String LEARN_MORE_URL = "https://support.mozilla.org/kb/kids";
|
||||
private static final String LEARN_MORE_URL = "https://support.mozilla.org/kb/controlledaccess";
|
||||
|
||||
private HomePager.OnUrlOpenListener onUrlOpenListener;
|
||||
|
||||
|
@ -41,6 +41,7 @@ import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
|
||||
import org.mozilla.gecko.sync.SyncConfiguration;
|
||||
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
|
||||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
@ -94,6 +95,7 @@ public class FxAccountStatusFragment
|
||||
protected PreferenceCategory accountCategory;
|
||||
protected Preference profilePreference;
|
||||
protected Preference emailPreference;
|
||||
protected Preference manageAccountPreference;
|
||||
protected Preference authServerPreference;
|
||||
|
||||
protected Preference needsPasswordPreference;
|
||||
@ -169,6 +171,10 @@ public class FxAccountStatusFragment
|
||||
} else {
|
||||
accountCategory.removePreference(profilePreference);
|
||||
}
|
||||
manageAccountPreference = ensureFindPreference("manage_account");
|
||||
if (AppConstants.MOZ_ANDROID_NATIVE_ACCOUNT_UI) {
|
||||
accountCategory.removePreference(manageAccountPreference);
|
||||
}
|
||||
authServerPreference = ensureFindPreference("auth_server");
|
||||
|
||||
needsPasswordPreference = ensureFindPreference("needs_credentials");
|
||||
@ -197,6 +203,7 @@ public class FxAccountStatusFragment
|
||||
} else {
|
||||
emailPreference.setOnPreferenceClickListener(this);
|
||||
}
|
||||
manageAccountPreference.setOnPreferenceClickListener(this);
|
||||
|
||||
needsPasswordPreference.setOnPreferenceClickListener(this);
|
||||
needsVerificationPreference.setOnPreferenceClickListener(this);
|
||||
@ -234,6 +241,12 @@ public class FxAccountStatusFragment
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
if (preference == manageAccountPreference) {
|
||||
// There's no native equivalent, so no need to re-direct through an Intent filter.
|
||||
ActivityUtils.openURLInFennec(getActivity().getApplicationContext(), "about:accounts?action=manage");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preference == needsPasswordPreference) {
|
||||
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_UPDATE_CREDENTIALS);
|
||||
final Bundle extras = getExtrasForAccount();
|
||||
|
@ -723,7 +723,7 @@ just addresses the organization to follow, e.g. "This site is run by " -->
|
||||
<!ENTITY bookmarks_support "Firefox: Support">
|
||||
<!--LOCALIZATION NOTE (bookmarks_marketplace):link title for https://marketplace.firefox.com -->
|
||||
<!ENTITY bookmarks_marketplace "Firefox Marketplace">
|
||||
<!-- LOCALIZATION NOTE (bookmarks_restricted_support): link title for https://support.mozilla.org/kb/kids -->
|
||||
<!ENTITY bookmarks_restricted_support "Firefox Help and Support for a simplified kid-friendly version of Firefox">
|
||||
<!-- LOCALIZATION NOTE (bookmarks_restricted_support): link title for https://support.mozilla.org/kb/controlledaccess -->
|
||||
<!ENTITY bookmarks_restricted_support2 "Firefox Help and Support for restricted profiles on Android tablets">
|
||||
<!-- LOCALIZATION NOTE (bookmarks_restricted_webmaker):link title for https://webmaker.org -->
|
||||
<!ENTITY bookmarks_restricted_webmaker "Learn the Web: Mozilla Webmaker">
|
||||
|
@ -197,6 +197,7 @@
|
||||
|
||||
<!ENTITY fxaccount_status_header2 'Firefox Account'>
|
||||
<!ENTITY fxaccount_status_signed_in_as 'Signed in as'>
|
||||
<!ENTITY fxaccount_status_manage_account 'Manage account'>
|
||||
<!ENTITY fxaccount_status_auth_server 'Account server'>
|
||||
<!ENTITY fxaccount_status_sync_now 'Sync now'>
|
||||
<!ENTITY fxaccount_status_syncing2 'Syncing…'>
|
||||
|
@ -16,6 +16,11 @@
|
||||
android:key="email"
|
||||
android:persistent="false"
|
||||
android:title="@string/fxaccount_email_hint" />
|
||||
<Preference
|
||||
android:editable="false"
|
||||
android:key="manage_account"
|
||||
android:persistent="false"
|
||||
android:title="@string/fxaccount_status_manage_account" />
|
||||
<Preference
|
||||
android:editable="false"
|
||||
android:key="auth_server"
|
||||
|
@ -472,8 +472,8 @@
|
||||
<string name="bookmarkdefaults_title_restricted_webmaker">&bookmarks_restricted_webmaker;</string>
|
||||
<string name="bookmarkdefaults_url_restricted_webmaker">https://webmaker.org/</string>
|
||||
|
||||
<string name="bookmarkdefaults_title_restricted_support">&bookmarks_restricted_support;</string>
|
||||
<string name="bookmarkdefaults_url_restricted_support">https://support.mozilla.org/kb/kids</string>
|
||||
<string name="bookmarkdefaults_title_restricted_support">&bookmarks_restricted_support2;</string>
|
||||
<string name="bookmarkdefaults_url_restricted_support">https://support.mozilla.org/kb/controlledaccess</string>
|
||||
|
||||
<!-- Site identity popup -->
|
||||
<string name="identity_connection_secure">&identity_connection_secure;</string>
|
||||
|
@ -519,6 +519,10 @@ var BrowserApp = {
|
||||
Services.prefs.setIntPref("extensions.enabledScopes", 1);
|
||||
Services.prefs.setIntPref("extensions.autoDisableScopes", 1);
|
||||
Services.prefs.setBoolPref("xpinstall.enabled", false);
|
||||
} else if (ParentalControls.parentalControlsEnabled) {
|
||||
Services.prefs.clearUserPref("extensions.enabledScopes");
|
||||
Services.prefs.clearUserPref("extensions.autoDisableScopes");
|
||||
Services.prefs.setBoolPref("xpinstall.enabled", true);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -186,6 +186,7 @@
|
||||
<string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string>
|
||||
<string name="fxaccount_status_header">&fxaccount_status_header2;</string>
|
||||
<string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string>
|
||||
<string name="fxaccount_status_manage_account">&fxaccount_status_manage_account;</string>
|
||||
<string name="fxaccount_status_auth_server">&fxaccount_status_auth_server;</string>
|
||||
<string name="fxaccount_status_sync_now">&fxaccount_status_sync_now;</string>
|
||||
<string name="fxaccount_status_syncing">&fxaccount_status_syncing2;</string>
|
||||
@ -240,4 +241,4 @@
|
||||
<string name="fxaccount_sync_finish_migrating_notification_text">&fxaccount_sync_finish_migrating_notification_text;</string>
|
||||
|
||||
<!-- Log Personal information -->
|
||||
<string name="fxaccount_enable_debug_mode">&fxaccount_enable_debug_mode;</string>
|
||||
<string name="fxaccount_enable_debug_mode">&fxaccount_enable_debug_mode;</string>
|
||||
|
@ -75,8 +75,8 @@ browser.suggestedsites.restricted.list.0=restricted_fxsupport
|
||||
browser.suggestedsites.restricted.list.1=webmaker
|
||||
browser.suggestedsites.restricted.list.2=restricted_mozilla
|
||||
|
||||
browser.suggestedsites.restricted_fxsupport.title=Firefox Help and Support for a simplified kid-friendly version of Firefox
|
||||
browser.suggestedsites.restricted_fxsupport.url=https://support.mozilla.org/kb/kids
|
||||
browser.suggestedsites.restricted_fxsupport.title=Firefox Help and Support for restricted profiles on Android tablets
|
||||
browser.suggestedsites.restricted_fxsupport.url=https://support.mozilla.org/kb/controlledaccess
|
||||
browser.suggestedsites.restricted_fxsupport.bgcolor=#f37c00
|
||||
|
||||
browser.suggestedsites.webmaker.title=Learn the Web: Mozilla Webmaker
|
||||
|
@ -196,7 +196,7 @@ def remove_caches_from_task(task):
|
||||
"""
|
||||
whitelist = [
|
||||
"tc-vcs",
|
||||
"tc-vcs-public-source",
|
||||
"tc-vcs-public-sources",
|
||||
"tooltool-cache",
|
||||
]
|
||||
try:
|
||||
|
@ -1176,7 +1176,7 @@ File.writeAtomic = function writeAtomic(path, buffer, options = {}) {
|
||||
let promise = Scheduler.post("writeAtomic",
|
||||
[Type.path.toMsg(path),
|
||||
Type.void_t.in_ptr.toMsg(buffer),
|
||||
options], [options, buffer]);
|
||||
options], [options, buffer, path]);
|
||||
TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj);
|
||||
return promise;
|
||||
};
|
||||
|
@ -7261,7 +7261,7 @@
|
||||
"expires_in_version": "never",
|
||||
"kind": "enumerated",
|
||||
"n_values": 13,
|
||||
"description": "OS of DevTools user (0:Windows XP, 1:Windows Vista, 2:Windows 7, 3:Windows 8, 4:Windows 8.1, 5:OSX, 6:Linux 7:reserved, 8:reserved, 9:reserved, 10:reserved, 11:reserved, 12:other)"
|
||||
"description": "OS of DevTools user (0:Windows XP, 1:Windows Vista, 2:Windows 7, 3:Windows 8, 4:Windows 8.1, 5:OSX, 6:Linux 7:Windows 10, 8:reserved, 9:reserved, 10:reserved, 11:reserved, 12:other)"
|
||||
},
|
||||
"DEVTOOLS_OS_IS_64_BITS_PER_USER": {
|
||||
"expires_in_version": "never",
|
||||
|
@ -16,6 +16,10 @@ Cu.import("resource://gre/modules/TelemetryArchive.jsm");
|
||||
Cu.import("resource://gre/modules/TelemetryUtils.jsm");
|
||||
Cu.import("resource://gre/modules/TelemetryLog.jsm");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
const Telemetry = Services.telemetry;
|
||||
const bundle = Services.strings.createBundle(
|
||||
@ -228,8 +232,17 @@ var Settings = {
|
||||
let elements = document.getElementsByClassName("change-data-choices-link");
|
||||
for (let el of elements) {
|
||||
el.addEventListener("click", function() {
|
||||
let mainWindow = getMainWindowWithPreferencesPane();
|
||||
mainWindow.openAdvancedPreferences("dataChoicesTab");
|
||||
if (AppConstants.platform == "android") {
|
||||
Cu.import("resource://gre/modules/Messaging.jsm");
|
||||
Messaging.sendRequest({
|
||||
type: "Settings:Show",
|
||||
resource: "preferences_vendor",
|
||||
});
|
||||
} else {
|
||||
// Show the data choices preferences on desktop.
|
||||
let mainWindow = getMainWindowWithPreferencesPane();
|
||||
mainWindow.openAdvancedPreferences("dataChoicesTab");
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
},
|
||||
|
@ -217,25 +217,13 @@
|
||||
<xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
|
||||
</xul:hbox>
|
||||
<xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
|
||||
<children/>
|
||||
<xul:hbox class="menu-accel-container" anonid="accel">
|
||||
<xul:label class="menu-iconic-accel" xbl:inherits="value=acceltext"/>
|
||||
</xul:hbox>
|
||||
</content>
|
||||
</binding>
|
||||
|
||||
<binding id="menuitem-iconic-both" extends="chrome://global/content/bindings/menu.xml#menuitem">
|
||||
<content>
|
||||
<xul:hbox class="menu-iconic-left" align="center" pack="center"
|
||||
xbl:inherits="selected,_moz-menuactive,disabled,checked">
|
||||
<xul:image class="menu-iconic-icon" xbl:inherits="src=image,validate,src"/>
|
||||
</xul:hbox>
|
||||
<xul:label class="menu-iconic-text" flex="1" xbl:inherits="value=label,accesskey,crop" crop="right"/>
|
||||
<xul:hbox class="menu-iconic-right" align="center" pack="center">
|
||||
<xul:image class="menu-iconic-icon" xbl:inherits="src=endimage"/>
|
||||
</xul:hbox>
|
||||
</content>
|
||||
</binding>
|
||||
|
||||
<binding id="menuitem-iconic-noaccel" extends="chrome://global/content/bindings/menu.xml#menuitem">
|
||||
<content>
|
||||
<xul:hbox class="menu-iconic-left" align="center" pack="center"
|
||||
|
@ -388,10 +388,6 @@ menuitem.menuitem-non-iconic {
|
||||
-moz-binding: url("chrome://global/content/bindings/menu.xml#menubutton-item");
|
||||
}
|
||||
|
||||
menuitem.menuitem-iconic[endimage] {
|
||||
-moz-binding: url("chrome://global/content/bindings/menu.xml#menuitem-iconic-both");
|
||||
}
|
||||
|
||||
menucaption {
|
||||
-moz-binding: url("chrome://global/content/bindings/menu.xml#menucaption");
|
||||
}
|
||||
|
@ -282,7 +282,9 @@ function getOSCPU() {
|
||||
if (oscpu.includes("Linux")) {
|
||||
return 6;
|
||||
}
|
||||
|
||||
if (oscpu.includes("NT 10.")) {
|
||||
return 7;
|
||||
}
|
||||
// Other OS.
|
||||
return 12;
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ const PREF_APP_UPDATE_CERT_ERRORS = "app.update.cert.errors";
|
||||
const PREF_APP_UPDATE_CERT_MAXERRORS = "app.update.cert.maxErrors";
|
||||
const PREF_APP_UPDATE_CERT_REQUIREBUILTIN = "app.update.cert.requireBuiltIn";
|
||||
const PREF_APP_UPDATE_CUSTOM = "app.update.custom";
|
||||
const PREF_APP_UPDATE_IMEI_HASH = "app.update.imei_hash";
|
||||
const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
|
||||
const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
|
||||
const PREF_APP_UPDATE_INCOMPATIBLE_MODE = "app.update.incompatible.mode";
|
||||
@ -3546,6 +3547,8 @@ Checker.prototype = {
|
||||
}
|
||||
url = url.replace(/%B2G_VERSION%/g,
|
||||
getPref("getCharPref", PREF_APP_B2G_VERSION, null));
|
||||
url = url.replace(/%IMEI%/g,
|
||||
getPref("getCharPref", PREF_APP_UPDATE_IMEI_HASH, "default"));
|
||||
}
|
||||
|
||||
if (force) {
|
||||
|
@ -77,12 +77,6 @@ menuitem[src] > .menu-iconic-left > .menu-iconic-icon {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
menuitem[endimage] > .menu-iconic-right > .menu-iconic-icon {
|
||||
-moz-margin-start: 2px;
|
||||
-moz-margin-end: 0;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
/* ..... menu arrow box ..... */
|
||||
|
||||
.menu-right,
|
||||
|
@ -13,7 +13,7 @@ menulist {
|
||||
}
|
||||
|
||||
menulist:not([popuponly="true"]) {
|
||||
min-height: 20px !important;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.menulist-label-box {
|
||||
|
@ -974,7 +974,7 @@ button.button-link {
|
||||
color: #0095dd;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ html|button {
|
||||
xul|colorpicker[type="button"],
|
||||
xul|menulist {
|
||||
-moz-appearance: none;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
color: var(--in-content-text-color);
|
||||
border: 1px solid var(--in-content-box-border-color);
|
||||
-moz-border-top-colors: none !important;
|
||||
|
@ -103,18 +103,11 @@ menucaption > .menu-iconic-text {
|
||||
|
||||
menu.menu-iconic > .menu-iconic-left,
|
||||
menuitem.menuitem-iconic > .menu-iconic-left,
|
||||
.splitmenu-menuitem[iconic="true"] > .menu-iconic-left,
|
||||
menuitem[endimage] > .menu-iconic-right {
|
||||
.splitmenu-menuitem[iconic="true"] > .menu-iconic-left {
|
||||
-moz-appearance: menuimage;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
menuitem[endimage] > .menu-iconic-right > .menu-iconic-icon {
|
||||
-moz-margin-start: 2px;
|
||||
-moz-margin-end: 5px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
/* ..... menu arrow box ..... */
|
||||
|
||||
.menu-right {
|
||||
|
Loading…
Reference in New Issue
Block a user