Merge m-c to inbound. a=merge
1
.hgtags
@ -101,3 +101,4 @@ ad0ae007aa9e03cd74e9005cd6652e544139b3b5 FIREFOX_AURORA_25_BASE
|
||||
ba2cc1eda988a1614d8986ae145d28e1268409b9 FIREFOX_AURORA_29_BASE
|
||||
83c9853e136451474dfa6d1aaa60a7fca7d2d83a FIREFOX_AURORA_30_BASE
|
||||
cfde3603b0206e119abea76fdd6e134b634348f1 FIREFOX_AURORA_31_BASE
|
||||
16f3cac5e8fe471e12f76d6a94a477b14e78df7c FIREFOX_AURORA_32_BASE
|
||||
|
@ -463,6 +463,17 @@ pref("services.push.pingInterval", 1800000); // 30 minutes
|
||||
pref("services.push.requestTimeout", 10000);
|
||||
// enable udp wakeup support
|
||||
pref("services.push.udp.wakeupEnabled", true);
|
||||
// This value should be the prefix to be added to the current PDP context[1]
|
||||
// domain or a full-qualified domain name.
|
||||
// If finished with a dot, it will be added as a prefix to the PDP context
|
||||
// domain. If not, will be used as the DNS query.
|
||||
// If the DNS query is unsuccessful, the push agent will send a null netid and
|
||||
// is a server decision what to do with the device. If the MCC-MNC identifies a
|
||||
// unique network the server will change to UDP mode. Otherwise, a websocket
|
||||
// connection will be maintained.
|
||||
// [1] Packet Data Protocol
|
||||
// http://en.wikipedia.org/wiki/GPRS_core_network#PDP_context
|
||||
pref("services.push.udp.well-known_netidAddress", "_wakeup_.");
|
||||
|
||||
// NetworkStats
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
|
@ -19,7 +19,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8e4420c0c5c8e8c8e58a000278a7129403769f96"/>
|
||||
|
@ -12,12 +12,12 @@
|
||||
<!--original fetch url was https://git.mozilla.org/releases-->
|
||||
<remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
|
||||
<!-- B2G specific things. -->
|
||||
<project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
|
||||
<project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<project name="platform_build" path="build" remote="b2g" revision="276ce45e78b09c4a4ee643646f691d22804754c1">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8e4420c0c5c8e8c8e58a000278a7129403769f96"/>
|
||||
|
@ -12,12 +12,12 @@
|
||||
<!--original fetch url was https://git.mozilla.org/releases-->
|
||||
<remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
|
||||
<!-- B2G specific things. -->
|
||||
<project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
|
||||
<project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>
|
||||
|
@ -4,6 +4,6 @@
|
||||
"remote": "",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "0d0d9de7b2534dbeb4809ad019294000eb719a80",
|
||||
"revision": "70cb8b50e7ad2bb64a7ac43e9b9e0c965ae8cd2f",
|
||||
"repo_path": "/integration/gaia-central"
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
@ -12,12 +12,12 @@
|
||||
<!--original fetch url was https://git.mozilla.org/releases-->
|
||||
<remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
|
||||
<!-- B2G specific things. -->
|
||||
<project name="platform_build" path="build" remote="b2g" revision="6e2a3b589d1e8cc1d9df25f5e630ce30a0aa39f3">
|
||||
<project name="platform_build" path="build" remote="b2g" revision="cc67f31dc638c0b7edba3cf7e3d87cadf0ed52bf">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>
|
||||
|
@ -17,7 +17,7 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="d283b742a12ac43ec087f45b02d3817cf7ddab69"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="3b2ae74d3a3aab77360559842467ba6f4ecb2b4a"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
|
@ -5,12 +5,12 @@
|
||||
MOZ_APP_BASENAME=B2G
|
||||
MOZ_APP_VENDOR=Mozilla
|
||||
|
||||
MOZ_APP_VERSION=32.0a1
|
||||
MOZ_APP_VERSION=33.0a1
|
||||
MOZ_APP_UA_NAME=Firefox
|
||||
|
||||
MOZ_UA_OS_AGNOSTIC=1
|
||||
|
||||
MOZ_B2G_VERSION=2.0.0.0-prerelease
|
||||
MOZ_B2G_VERSION=2.1.0.0-prerelease
|
||||
MOZ_B2G_OS_NAME=Boot2Gecko
|
||||
|
||||
MOZ_BRANDING_DIRECTORY=b2g/branding/unofficial
|
||||
|
68
browser/base/content/browser-loop.js
Normal file
@ -0,0 +1,68 @@
|
||||
// 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/.
|
||||
|
||||
// the "exported" symbols
|
||||
let LoopUI;
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
|
||||
|
||||
|
||||
(function() {
|
||||
|
||||
LoopUI = {
|
||||
/**
|
||||
* Opens the panel for Loop and sizes it appropriately.
|
||||
*
|
||||
* @param {event} event The event opening the panel, used to anchor
|
||||
* the panel to the button which triggers it.
|
||||
*/
|
||||
openCallPanel: function(event) {
|
||||
let panel = document.getElementById("loop-panel");
|
||||
let anchor = event.target;
|
||||
let iframe = document.getElementById("loop-panel-frame");
|
||||
|
||||
if (!iframe) {
|
||||
// XXX This should be using SharedFrame (bug 1011392 may do this).
|
||||
iframe = document.createElement("iframe");
|
||||
iframe.setAttribute("id", "loop-panel-frame");
|
||||
iframe.setAttribute("type", "content");
|
||||
iframe.setAttribute("class", "loop-frame social-panel-frame");
|
||||
iframe.setAttribute("flex", "1");
|
||||
panel.appendChild(iframe);
|
||||
}
|
||||
|
||||
// We inject in DOMContentLoaded as that is before any scripts have tun.
|
||||
iframe.addEventListener("DOMContentLoaded", function documentDOMLoaded() {
|
||||
iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
|
||||
injectLoopAPI(iframe.contentWindow);
|
||||
|
||||
// We use loopPanelInitialized so that we know we've finished localising before
|
||||
// sizing the panel.
|
||||
iframe.contentWindow.addEventListener("loopPanelInitialized",
|
||||
function documentLoaded() {
|
||||
iframe.contentWindow.removeEventListener("loopPanelInitialized",
|
||||
documentLoaded, true);
|
||||
// XXX We end up with the wrong size here, so this
|
||||
// needs further investigation (bug 1011394).
|
||||
sizeSocialPanelToContent(panel, iframe);
|
||||
}, true);
|
||||
|
||||
}, true);
|
||||
|
||||
iframe.setAttribute("src", "about:looppanel");
|
||||
panel.hidden = false;
|
||||
panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers the initialization of the loop service. Called by
|
||||
* delayedStartup.
|
||||
*/
|
||||
initialize: function() {
|
||||
MozLoopService.initialize();
|
||||
},
|
||||
|
||||
};
|
||||
})();
|
@ -172,6 +172,9 @@ let gInitialPages = [
|
||||
#include browser-feeds.js
|
||||
#include browser-fullScreen.js
|
||||
#include browser-fullZoom.js
|
||||
#ifdef MOZ_LOOP
|
||||
#include browser-loop.js
|
||||
#endif
|
||||
#include browser-places.js
|
||||
#include browser-plugins.js
|
||||
#include browser-safebrowsing.js
|
||||
@ -1182,6 +1185,10 @@ var gBrowserInit = {
|
||||
gDataNotificationInfoBar.init();
|
||||
#endif
|
||||
|
||||
#ifdef MOZ_LOOP
|
||||
LoopUI.initialize();
|
||||
#endif
|
||||
|
||||
gBrowserThumbnails.init();
|
||||
|
||||
// Add Devtools menuitems and listeners
|
||||
@ -5311,8 +5318,8 @@ function setStyleDisabled(disabled) {
|
||||
|
||||
var LanguageDetectionListener = {
|
||||
init: function() {
|
||||
window.messageManager.addMessageListener("LanguageDetection:Result", msg => {
|
||||
Translation.languageDetected(msg.target, msg.data);
|
||||
window.messageManager.addMessageListener("Translation:DocumentState", msg => {
|
||||
Translation.documentStateReceived(msg.target, msg.data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -226,7 +226,7 @@
|
||||
<hbox id="UITourTooltipButtons" flex="1" align="center"/>
|
||||
</vbox>
|
||||
</panel>
|
||||
<!-- type="default" forces frames to be created so that the panel's size can be determined -->
|
||||
<!-- type="default" forces frames to be created so that the panel's size can be determined -->
|
||||
<panel id="UITourHighlightContainer"
|
||||
type="default"
|
||||
hidden="true"
|
||||
@ -267,6 +267,14 @@
|
||||
noautofocus="true"
|
||||
position="topcenter topright"/>
|
||||
|
||||
#ifdef MOZ_LOOP
|
||||
<panel id="loop-panel"
|
||||
class="loop-panel social-panel"
|
||||
type="arrow"
|
||||
orient="horizontal"
|
||||
hidden="true"/>
|
||||
#endif
|
||||
|
||||
<menupopup id="toolbar-context-menu"
|
||||
onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator'));">
|
||||
<menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
|
||||
@ -635,7 +643,11 @@
|
||||
aria-label="&navbarCmd.label;"
|
||||
fullscreentoolbar="true" mode="icons" customizable="true"
|
||||
iconsize="small"
|
||||
#ifdef MOZ_LOOP
|
||||
defaultset="urlbar-container,search-container,webrtc-status-button,bookmarks-menu-button,downloads-button,home-button,loop-call-button,social-share-button,social-toolbar-item"
|
||||
#else
|
||||
defaultset="urlbar-container,search-container,webrtc-status-button,bookmarks-menu-button,downloads-button,home-button,social-share-button,social-toolbar-item"
|
||||
#endif
|
||||
customizationtarget="nav-bar-customization-target"
|
||||
overflowable="true"
|
||||
overflowbutton="nav-bar-overflow-button"
|
||||
@ -768,6 +780,20 @@
|
||||
oncommand="WebrtcIndicator.menuCommand(event.target);"/>
|
||||
</toolbarbutton>
|
||||
|
||||
#ifdef MOZ_LOOP
|
||||
<!-- XXX Bug 1013989 will provide a label for the button -->
|
||||
<toolbarbutton id="loop-call-button"
|
||||
class="toolbarbutton-1 chromeclass-toolbar-additional"
|
||||
persist="class"
|
||||
removable="true"
|
||||
tooltiptext="&loopCallButton.tooltip;"
|
||||
oncommand="LoopUI.openCallPanel(event);"
|
||||
cui-areatype="toolbar"
|
||||
overflows="false"
|
||||
>
|
||||
</toolbarbutton>
|
||||
#endif
|
||||
|
||||
<toolbarbutton id="bookmarks-menu-button"
|
||||
class="toolbarbutton-1 chromeclass-toolbar-additional"
|
||||
persist="class"
|
||||
|
@ -14,6 +14,7 @@ const kWhitelist = [
|
||||
{sourceName: /codemirror\.css/i}, /* CodeMirror is imported as-is, see bug 1004423 */
|
||||
{sourceName: /web\/viewer\.css/i, errorMessage: /Unknown pseudo-class.*(fullscreen|selection)/i }, /* PDFjs is futureproofing its pseudoselectors, and those rules are dropped. */
|
||||
{sourceName: /aboutaccounts\/(main|normalize)\.css/i}, /* Tracked in bug 1004428 */
|
||||
{sourceName: /otcdn\/webrtc\/.*\.css$/i /* TokBox SDK assets, see bug 1003029 */}
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -97,6 +97,16 @@ static RedirEntry kRedirMap[] = {
|
||||
nsIAboutModule::ALLOW_SCRIPT },
|
||||
{ "customizing", "chrome://browser/content/customizableui/aboutCustomizing.xul",
|
||||
nsIAboutModule::ALLOW_SCRIPT },
|
||||
#ifdef MOZ_LOOP
|
||||
{ "loopconversation", "chrome://browser/content/loop/conversation.html",
|
||||
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
|
||||
nsIAboutModule::ALLOW_SCRIPT |
|
||||
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
|
||||
{ "looppanel", "chrome://browser/content/loop/panel.html",
|
||||
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
|
||||
nsIAboutModule::ALLOW_SCRIPT |
|
||||
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
|
||||
#endif
|
||||
};
|
||||
static const int kRedirTotal = ArrayLength(kRedirMap);
|
||||
|
||||
@ -119,7 +129,7 @@ GetAboutModuleName(nsIURI *aURI)
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
AboutRedirector::NewChannel(nsIURI *aURI, nsIChannel **result)
|
||||
AboutRedirector::NewChannel(nsIURI *aURI, nsIChannel **result)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aURI);
|
||||
NS_ASSERTION(result, "must not be null");
|
||||
|
@ -112,6 +112,10 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
|
||||
#endif
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "app-manager", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "customizing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
#ifdef MOZ_LOOP
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "looppanel", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "loopconversation", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
|
||||
#endif
|
||||
#if defined(XP_WIN)
|
||||
{ NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
|
||||
#elif defined(XP_MACOSX)
|
||||
|
@ -200,6 +200,9 @@ let CustomizableUIInternal = {
|
||||
"bookmarks-menu-button",
|
||||
"downloads-button",
|
||||
"home-button",
|
||||
#ifdef MOZ_LOOP
|
||||
"loop-call-button",
|
||||
#endif
|
||||
"social-share-button",
|
||||
],
|
||||
defaultCollapsed: false,
|
||||
|
170
browser/components/loop/MozLoopAPI.jsm
Normal file
@ -0,0 +1,170 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource:///modules/loop/MozLoopService.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
|
||||
"resource://gre/modules/MozSocialAPI.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["injectLoopAPI"];
|
||||
|
||||
/**
|
||||
* Inject the loop API into the given window. The caller must be sure the
|
||||
* window is a loop content window (eg, a panel, chatwindow, or similar).
|
||||
*
|
||||
* See the documentation on the individual functions for details of the API.
|
||||
*
|
||||
* @param {nsIDOMWindow} targetWindow The content window to attach the API.
|
||||
*/
|
||||
function injectLoopAPI(targetWindow) {
|
||||
let api = {
|
||||
/**
|
||||
* Sets and gets the "do not disturb" mode activation flag.
|
||||
*/
|
||||
doNotDisturb: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return MozLoopService.doNotDisturb;
|
||||
},
|
||||
set: function(aFlag) {
|
||||
MozLoopService.doNotDisturb = aFlag;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the url for the Loop server from preferences.
|
||||
*
|
||||
* @return {String} The Loop server url
|
||||
*/
|
||||
serverUrl: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return Services.prefs.getCharPref("loop.server");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current locale of the browser.
|
||||
*
|
||||
* @returns {String} The locale string
|
||||
*/
|
||||
locale: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
return MozLoopService.locale;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns translated strings associated with an element. Designed
|
||||
* for use with l10n.js
|
||||
*
|
||||
* @param {String} key The element id
|
||||
* @returns {Object} A JSON string containing the localized
|
||||
* attribute/value pairs for the element.
|
||||
*/
|
||||
getStrings: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(key) {
|
||||
return MozLoopService.getStrings(key);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Call to ensure that any necessary registrations for the Loop Service
|
||||
* have taken place.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
*
|
||||
* @param {Function} callback Will be called once registration is complete,
|
||||
* or straight away if registration has already
|
||||
* happened.
|
||||
*/
|
||||
ensureRegistered: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(callback) {
|
||||
// We translate from a promise to a callback, as we can't pass promises from
|
||||
// Promise.jsm across the priv versus unpriv boundary.
|
||||
return MozLoopService.register().then(() => {
|
||||
callback(null);
|
||||
}, err => {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to note a call url expiry time. If the time is later than the current
|
||||
* latest expiry time, then the stored expiry time is increased. For times
|
||||
* sooner, this function is a no-op; this ensures we always have the latest
|
||||
* expiry time for a url.
|
||||
*
|
||||
* This is used to determine whether or not we should be registering with the
|
||||
* push server on start.
|
||||
*
|
||||
* @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
|
||||
* of the url.
|
||||
*/
|
||||
noteCallUrlExpiry: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(expiryTimeSeconds) {
|
||||
MozLoopService.noteCallUrlExpiry(expiryTimeSeconds);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return any preference under "loop." that's coercible to a character
|
||||
* preference.
|
||||
*
|
||||
* @param {String} prefName The name of the pref without the preceding
|
||||
* "loop."
|
||||
*
|
||||
* Any errors thrown by the Mozilla pref API are logged to the console
|
||||
* and cause null to be returned. This includes the case of the preference
|
||||
* not being found.
|
||||
*
|
||||
* @return {String} on success, null on error
|
||||
*/
|
||||
getLoopCharPref: {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: function(prefName) {
|
||||
return MozLoopService.getLoopCharPref(prefName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let contentObj = Cu.createObjectIn(targetWindow);
|
||||
Object.defineProperties(contentObj, api);
|
||||
Cu.makeObjectPropsNormal(contentObj);
|
||||
|
||||
targetWindow.navigator.wrappedJSObject.__defineGetter__("mozLoop", function() {
|
||||
// We do this in a getter, so that we create these objects
|
||||
// only on demand (this is a potential concern, since
|
||||
// otherwise we might add one per iframe, and keep them
|
||||
// alive for as long as the window is alive).
|
||||
delete targetWindow.navigator.wrappedJSObject.mozLoop;
|
||||
return targetWindow.navigator.wrappedJSObject.mozLoop = contentObj;
|
||||
});
|
||||
|
||||
// Handle window.close correctly on the panel and chatbox.
|
||||
hookWindowCloseForPanelClose(targetWindow);
|
||||
}
|
593
browser/components/loop/MozLoopService.jsm
Normal file
@ -0,0 +1,593 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
let console = (Cu.import("resource://gre/modules/devtools/Console.jsm", {})).console;
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MozLoopService"];
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
|
||||
"resource:///modules/loop/MozLoopAPI.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
|
||||
"resource://services-common/utils.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
|
||||
"resource://services-crypto/utils.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "HAWKAuthenticatedRESTRequest",
|
||||
"resource://services-common/hawkrequest.js");
|
||||
|
||||
/**
|
||||
* We don't have push notifications on desktop currently, so this is a
|
||||
* workaround to get them going for us.
|
||||
*
|
||||
* XXX Handle auto-reconnections if connection fails for whatever reason
|
||||
* (bug 1013248).
|
||||
*/
|
||||
let PushHandlerHack = {
|
||||
// This is the uri of the push server.
|
||||
pushServerUri: Services.prefs.getCharPref("services.push.serverURL"),
|
||||
// This is the channel id we're using for notifications
|
||||
channelID: "8b1081ce-9b35-42b5-b8f5-3ff8cb813a50",
|
||||
// Stores the push url if we're registered and we have one.
|
||||
pushUrl: undefined,
|
||||
|
||||
/**
|
||||
* Call to start the connection to the push socket server. On
|
||||
* connection, it will automatically say hello and register the channel
|
||||
* id with the server.
|
||||
*
|
||||
* Register callback parameters:
|
||||
* - {String|null} err: Encountered error, if any
|
||||
* - {String} url: The push url obtained from the server
|
||||
*
|
||||
* @param {Function} registerCallback Callback to be called once we are
|
||||
* registered.
|
||||
* @param {Function} notificationCallback Callback to be called when a
|
||||
* push notification is received.
|
||||
*/
|
||||
initialize: function(registerCallback, notificationCallback) {
|
||||
if (Services.io.offline) {
|
||||
registerCallback("offline");
|
||||
return;
|
||||
}
|
||||
|
||||
this._registerCallback = registerCallback;
|
||||
this._notificationCallback = notificationCallback;
|
||||
|
||||
this.websocket = Cc["@mozilla.org/network/protocol;1?name=wss"]
|
||||
.createInstance(Ci.nsIWebSocketChannel);
|
||||
|
||||
this.websocket.protocol = "push-notification";
|
||||
|
||||
var pushURI = Services.io.newURI(this.pushServerUri, null, null);
|
||||
this.websocket.asyncOpen(pushURI, this.pushServerUri, this, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener method, handles the start of the websocket stream.
|
||||
* Sends a hello message to the server.
|
||||
*
|
||||
* @param {nsISupports} aContext Not used
|
||||
*/
|
||||
onStart: function() {
|
||||
var helloMsg = { messageType: "hello", uaid: "", channelIDs: [] };
|
||||
this.websocket.sendMsg(JSON.stringify(helloMsg));
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener method, called when the websocket is closed.
|
||||
*
|
||||
* @param {nsISupports} aContext Not used
|
||||
* @param {nsresult} aStatusCode Reason for stopping (NS_OK = successful)
|
||||
*/
|
||||
onStop: function(aContext, aStatusCode) {
|
||||
// XXX We really should be handling auto-reconnect here, this will be
|
||||
// implemented in bug 994151. For now, just log a warning, so that a
|
||||
// developer can find out it has happened and not get too confused.
|
||||
Cu.reportError("Loop Push server web socket closed! Code: " + aStatusCode);
|
||||
this.pushUrl = undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener method, called when the websocket is closed by the server.
|
||||
* If there are errors, onStop may be called without ever calling this
|
||||
* method.
|
||||
*
|
||||
* @param {nsISupports} aContext Not used
|
||||
* @param {integer} aCode the websocket closing handshake close code
|
||||
* @param {String} aReason the websocket closing handshake close reason
|
||||
*/
|
||||
onServerClose: function(aContext, aCode) {
|
||||
// XXX We really should be handling auto-reconnect here, this will be
|
||||
// implemented in bug 994151. For now, just log a warning, so that a
|
||||
// developer can find out it has happened and not get too confused.
|
||||
Cu.reportError("Loop Push server web socket closed (server)! Code: " + aCode);
|
||||
this.pushUrl = undefined;
|
||||
},
|
||||
|
||||
/**
|
||||
* Listener method, called when the websocket receives a message.
|
||||
*
|
||||
* @param {nsISupports} aContext Not used
|
||||
* @param {String} aMsg The message data
|
||||
*/
|
||||
onMessageAvailable: function(aContext, aMsg) {
|
||||
var msg = JSON.parse(aMsg);
|
||||
|
||||
switch(msg.messageType) {
|
||||
case "hello":
|
||||
this._registerChannel();
|
||||
break;
|
||||
case "register":
|
||||
this.pushUrl = msg.pushEndpoint;
|
||||
this._registerCallback(null, this.pushUrl);
|
||||
break;
|
||||
case "notification":
|
||||
msg.updates.forEach(function(update) {
|
||||
if (update.channelID === this.channelID) {
|
||||
this._notificationCallback(update.version);
|
||||
}
|
||||
}.bind(this));
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles registering a service
|
||||
*/
|
||||
_registerChannel: function() {
|
||||
this.websocket.sendMsg(JSON.stringify({
|
||||
messageType: "register",
|
||||
channelID: this.channelID
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal helper methods and state
|
||||
*
|
||||
* The registration is a two-part process. First we need to connect to
|
||||
* and register with the push server. Then we need to take the result of that
|
||||
* and register with the Loop server.
|
||||
*/
|
||||
let MozLoopServiceInternal = {
|
||||
// The uri of the Loop server.
|
||||
loopServerUri: Services.prefs.getCharPref("loop.server"),
|
||||
|
||||
// The current deferred for the registration process. This is set if in progress
|
||||
// or the registration was successful. This is null if a registration attempt was
|
||||
// unsuccessful.
|
||||
_registeredDeferred: null,
|
||||
|
||||
/**
|
||||
* The initial delay for push registration. This ensures we don't start
|
||||
* kicking off straight after browser startup, just a few seconds later.
|
||||
*/
|
||||
get initialRegistrationDelayMilliseconds() {
|
||||
try {
|
||||
// Let a pref override this for developer & testing use.
|
||||
return Services.prefs.getIntPref("loop.initialDelay");
|
||||
} catch (x) {
|
||||
// Default to 5 seconds
|
||||
return 5000;
|
||||
}
|
||||
return initialDelay;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the current latest expiry time for urls.
|
||||
*
|
||||
* In seconds since epoch.
|
||||
*/
|
||||
get expiryTimeSeconds() {
|
||||
try {
|
||||
return Services.prefs.getIntPref("loop.urlsExpiryTimeSeconds");
|
||||
} catch (x) {
|
||||
// It is ok for the pref not to exist.
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the expiry time to either the specified time, or keeps it the same
|
||||
* depending on which is latest.
|
||||
*/
|
||||
set expiryTimeSeconds(time) {
|
||||
if (time > this.expiryTimeSeconds) {
|
||||
Services.prefs.setIntPref("loop.urlsExpiryTimeSeconds", time);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the expiry time is in the future.
|
||||
*/
|
||||
urlExpiryTimeIsInFuture: function() {
|
||||
return this.expiryTimeSeconds * 1000 > Date.now();
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves MozLoopService "do not disturb" pref value.
|
||||
*
|
||||
* @return {Boolean} aFlag
|
||||
*/
|
||||
get doNotDisturb() {
|
||||
return Services.prefs.getBoolPref("loop.do_not_disturb");
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets MozLoopService "do not disturb" pref value.
|
||||
*
|
||||
* @param {Boolean} aFlag
|
||||
*/
|
||||
set doNotDisturb(aFlag) {
|
||||
Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts registration of Loop with the push server, and then will register
|
||||
* with the Loop server. It will return early if already registered.
|
||||
*
|
||||
* @returns {Promise} a promise that is resolved with no params on completion, or
|
||||
* rejected with an error code or string.
|
||||
*/
|
||||
promiseRegisteredWithServers: function() {
|
||||
if (this._registeredDeferred) {
|
||||
return this._registeredDeferred.promise;
|
||||
}
|
||||
|
||||
this._registeredDeferred = Promise.defer();
|
||||
// We grab the promise early in case .initialize or its results sets
|
||||
// it back to null on error.
|
||||
let result = this._registeredDeferred.promise;
|
||||
|
||||
PushHandlerHack.initialize(this.onPushRegistered.bind(this),
|
||||
this.onHandleNotification.bind(this));
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Derives hawk credentials for the given token and context.
|
||||
*
|
||||
* @param {String} tokenHex The token value in hex.
|
||||
* @param {String} context The context for the token.
|
||||
*/
|
||||
deriveHawkCredentials: function(tokenHex, context) {
|
||||
const PREFIX_NAME = "identity.mozilla.com/picl/v1/";
|
||||
|
||||
let token = CommonUtils.hexToBytes(tokenHex);
|
||||
let keyWord = CommonUtils.stringToBytes(PREFIX_NAME + context);
|
||||
|
||||
// XXX Using 2 * 32 for now to be in sync with client.js, but we might
|
||||
// want to make this 3 * 32 to allow for extra, if we start using the extra
|
||||
// field.
|
||||
let out = CryptoUtils.hkdf(token, undefined, keyWord, 2 * 32);
|
||||
|
||||
return {
|
||||
algorithm: "sha256",
|
||||
key: out.slice(32, 64),
|
||||
id: CommonUtils.bytesAsHex(out.slice(0, 32))
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback from PushHandlerHack - The push server has been registered
|
||||
* and has given us a push url.
|
||||
*
|
||||
* @param {String} pushUrl The push url given by the push server.
|
||||
*/
|
||||
onPushRegistered: function(err, pushUrl) {
|
||||
if (err) {
|
||||
this._registeredDeferred.reject(err);
|
||||
this._registeredDeferred = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerWithLoopServer(pushUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
* Registers with the Loop server.
|
||||
*
|
||||
* @param {String} pushUrl The push url given by the push server.
|
||||
* @param {Boolean} noRetry Optional, don't retry if authentication fails.
|
||||
*/
|
||||
registerWithLoopServer: function(pushUrl, noRetry) {
|
||||
let sessionToken;
|
||||
try {
|
||||
sessionToken = Services.prefs.getCharPref("loop.hawk-session-token");
|
||||
} catch (x) {
|
||||
// It is ok for this not to exist, we'll default to sending no-creds
|
||||
}
|
||||
|
||||
let credentials;
|
||||
if (sessionToken) {
|
||||
credentials = this.deriveHawkCredentials(sessionToken, "sessionToken");
|
||||
}
|
||||
|
||||
let uri = Services.io.newURI(this.loopServerUri, null, null).resolve("/registration");
|
||||
this.loopXhr = new HAWKAuthenticatedRESTRequest(uri, credentials);
|
||||
|
||||
this.loopXhr.dispatch('POST', { simple_push_url: pushUrl }, (error) => {
|
||||
if (this.loopXhr.response.status == 401) {
|
||||
if (this.urlExpiryTimeIsInFuture()) {
|
||||
// XXX Should this be reported to the user is a visible manner?
|
||||
Cu.reportError("Loop session token is invalid, all previously "
|
||||
+ "generated urls will no longer work.");
|
||||
}
|
||||
|
||||
// Authorization failed, invalid token, we need to try again with a new token.
|
||||
Services.prefs.clearUserPref("loop.hawk-session-token");
|
||||
this.registerWithLoopServer(pushUrl, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// No authorization issues, so complete registration.
|
||||
this.onLoopRegistered(error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback from PushHandlerHack - A push notification has been received from
|
||||
* the server.
|
||||
*
|
||||
* @param {String} version The version information from the server.
|
||||
*/
|
||||
onHandleNotification: function(version) {
|
||||
if (this.doNotDisturb) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openChatWindow(null, "LooP", "about:loopconversation#incoming/" + version);
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback from the loopXhr. Checks the registration result.
|
||||
*/
|
||||
onLoopRegistered: function(error) {
|
||||
let status = this.loopXhr.response.status;
|
||||
if (status != 200) {
|
||||
// XXX Bubble the precise details up to the UI somehow (bug 1013248).
|
||||
Cu.reportError("Failed to register with the loop server. Code: " +
|
||||
status + " Text: " + this.loopXhr.response.statusText);
|
||||
this._registeredDeferred.reject(status);
|
||||
this._registeredDeferred = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionToken = this.loopXhr.response.headers["hawk-session-token"];
|
||||
if (sessionToken) {
|
||||
|
||||
// XXX should do more validation here
|
||||
if (sessionToken.length === 64) {
|
||||
|
||||
Services.prefs.setCharPref("loop.hawk-session-token", sessionToken);
|
||||
} else {
|
||||
// XXX Bubble the precise details up to the UI somehow (bug 1013248).
|
||||
console.warn("Loop server sent an invalid session token");
|
||||
this._registeredDeferred.reject("session-token-wrong-size");
|
||||
this._registeredDeferred = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we made it this far, we registered just fine.
|
||||
this.registeredLoopServer = true;
|
||||
this._registeredDeferred.resolve();
|
||||
// No need to clear the promise here, everything was good, so we don't need
|
||||
// to re-register.
|
||||
},
|
||||
|
||||
/**
|
||||
* A getter to obtain and store the strings for loop. This is structured
|
||||
* for use by l10n.js.
|
||||
*
|
||||
* @returns {Object} a map of element ids with attributes to set.
|
||||
*/
|
||||
get localizedStrings() {
|
||||
if (this._localizedStrings)
|
||||
return this._localizedStrings;
|
||||
|
||||
var stringBundle =
|
||||
Services.strings.createBundle('chrome://browser/locale/loop/loop.properties');
|
||||
|
||||
var map = {};
|
||||
var enumerator = stringBundle.getSimpleEnumeration();
|
||||
while (enumerator.hasMoreElements()) {
|
||||
var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
|
||||
|
||||
// 'textContent' is the default attribute to set if none are specified.
|
||||
var key = string.key, property = 'textContent';
|
||||
var i = key.lastIndexOf('.');
|
||||
if (i >= 0) {
|
||||
property = key.substring(i + 1);
|
||||
key = key.substring(0, i);
|
||||
}
|
||||
if (!(key in map))
|
||||
map[key] = {};
|
||||
map[key][property] = string.value;
|
||||
}
|
||||
|
||||
return this._localizedStrings = map;
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the chat window
|
||||
*
|
||||
* @param {Object} contentWindow The window to open the chat window in, may
|
||||
* be null.
|
||||
* @param {String} title The title of the chat window.
|
||||
* @param {String} url The page to load in the chat window.
|
||||
* @param {String} mode May be "minimized" or undefined.
|
||||
*/
|
||||
openChatWindow: function(contentWindow, title, url, mode) {
|
||||
// So I guess the origin is the loop server!?
|
||||
let origin = this.loopServerUri;
|
||||
url = url.spec || url;
|
||||
|
||||
let callback = chatbox => {
|
||||
// We need to use DOMContentLoaded as otherwise the injection will happen
|
||||
// in about:blank and then get lost.
|
||||
// Sadly we can't use chatbox.promiseChatLoaded() as promise chaining
|
||||
// involves event loop spins, which means it might be too late.
|
||||
// Have we already done it?
|
||||
if (chatbox.contentWindow.navigator.mozLoop) {
|
||||
return;
|
||||
}
|
||||
|
||||
chatbox.addEventListener("DOMContentLoaded", function loaded(event) {
|
||||
if (event.target != chatbox.contentDocument) {
|
||||
return;
|
||||
}
|
||||
chatbox.removeEventListener("DOMContentLoaded", loaded, true);
|
||||
injectLoopAPI(chatbox.contentWindow);
|
||||
}, true);
|
||||
};
|
||||
|
||||
Chat.open(contentWindow, origin, title, url, undefined, undefined, callback);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Public API
|
||||
*/
|
||||
this.MozLoopService = {
|
||||
/**
|
||||
* Initialized the loop service, and starts registration with the
|
||||
* push and loop servers.
|
||||
*/
|
||||
initialize: function() {
|
||||
// If expiresTime is in the future then kick-off registration.
|
||||
if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
|
||||
this._startInitializeTimer();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal function, exposed for testing purposes only. Used to start the
|
||||
* initialize timer.
|
||||
*/
|
||||
_startInitializeTimer: function() {
|
||||
// Kick off the push notification service into registering after a timeout
|
||||
// this ensures we're not doing too much straight after the browser's finished
|
||||
// starting up.
|
||||
this._initializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this._initializeTimer.initWithCallback(function() {
|
||||
this.register();
|
||||
this._initializeTimer = null;
|
||||
}.bind(this),
|
||||
MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts registration of Loop with the push server, and then will register
|
||||
* with the Loop server. It will return early if already registered.
|
||||
*
|
||||
* @returns {Promise} a promise that is resolved with no params on completion, or
|
||||
* rejected with an error code or string.
|
||||
*/
|
||||
register: function() {
|
||||
return MozLoopServiceInternal.promiseRegisteredWithServers();
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to note a call url expiry time. If the time is later than the current
|
||||
* latest expiry time, then the stored expiry time is increased. For times
|
||||
* sooner, this function is a no-op; this ensures we always have the latest
|
||||
* expiry time for a url.
|
||||
*
|
||||
* This is used to deterimine whether or not we should be registering with the
|
||||
* push server on start.
|
||||
*
|
||||
* @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
|
||||
* of the url.
|
||||
*/
|
||||
noteCallUrlExpiry: function(expiryTimeSeconds) {
|
||||
MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the strings for the specified element. Designed for use
|
||||
* with l10n.js.
|
||||
*
|
||||
* @param {key} The element id to get strings for.
|
||||
* @return {String} A JSON string containing the localized
|
||||
* attribute/value pairs for the element.
|
||||
*/
|
||||
getStrings: function(key) {
|
||||
var stringData = MozLoopServiceInternal.localizedStrings;
|
||||
if (!(key in stringData)) {
|
||||
Cu.reportError('No string for key: ' + key + 'found');
|
||||
return "";
|
||||
}
|
||||
|
||||
return JSON.stringify(stringData[key]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves MozLoopService "do not disturb" value.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
get doNotDisturb() {
|
||||
return MozLoopServiceInternal.doNotDisturb;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets MozLoopService "do not disturb" value.
|
||||
*
|
||||
* @param {Boolean} aFlag
|
||||
*/
|
||||
set doNotDisturb(aFlag) {
|
||||
MozLoopServiceInternal.doNotDisturb = aFlag;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current locale
|
||||
*
|
||||
* @return {String} The code of the current locale.
|
||||
*/
|
||||
get locale() {
|
||||
try {
|
||||
return Services.prefs.getComplexValue("general.useragent.locale",
|
||||
Ci.nsISupportsString).data;
|
||||
} catch (ex) {
|
||||
return "en-US";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return any preference under "loop." that's coercible to a character
|
||||
* preference.
|
||||
*
|
||||
* @param {String} prefName The name of the pref without the preceding
|
||||
* "loop."
|
||||
*
|
||||
* Any errors thrown by the Mozilla pref API are logged to the console
|
||||
* and cause null to be returned. This includes the case of the preference
|
||||
* not being found.
|
||||
*
|
||||
* @return {String} on success, null on error
|
||||
*/
|
||||
getLoopCharPref: function(prefName) {
|
||||
try {
|
||||
return Services.prefs.getCharPref("loop." + prefName);
|
||||
} catch (ex) {
|
||||
console.log("getLoopCharPref had trouble getting " + prefName +
|
||||
"; exception: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
8
browser/components/loop/README.txt
Normal file
@ -0,0 +1,8 @@
|
||||
This is the directory for the Loop desktop implementation and the standalone client.
|
||||
|
||||
The desktop implementation is the UX built into Firefox, activated by the Loop button on the toolbar. The standalone client is the link-clicker UX for any modern browser that supports WebRTC.
|
||||
|
||||
The standalone client is a set of web pages intended to be hosted on a standalone server referenced by the loop-server.
|
||||
|
||||
The standalone client exists in standalone/ but shares items (from content/shared/) with the desktop implementation. See the README.md file in the standalone/ directory for how to run the server locally.
|
||||
|
34
browser/components/loop/content/conversation.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Conversation</title>
|
||||
<link rel="stylesheet" type="text/css" href="loop/shared/css/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="loop/shared/css/conversation.css">
|
||||
</head>
|
||||
<body onload="loop.conversation.init();">
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<div id="main"></div>
|
||||
|
||||
<script type="text/javascript" src="loop/libs/l10n.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/sdk.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/token.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
|
||||
<script type="text/javascript" src="loop/shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
|
||||
<script type="text/javascript" src="loop/js/conversation.js"></script>
|
||||
</body>
|
||||
</html>
|
190
browser/components/loop/content/js/conversation.js
Normal file
@ -0,0 +1,190 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.conversation = (function(OT, mozL10n) {
|
||||
"use strict";
|
||||
|
||||
var sharedViews = loop.shared.views,
|
||||
// aliasing translation function as __ for concision
|
||||
__ = mozL10n.get;
|
||||
|
||||
/**
|
||||
* App router.
|
||||
* @type {loop.desktopRouter.DesktopConversationRouter}
|
||||
*/
|
||||
var router;
|
||||
|
||||
/**
|
||||
* Incoming call view.
|
||||
* @type {loop.shared.views.BaseView}
|
||||
*/
|
||||
var IncomingCallView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<h2 data-l10n-id="incoming_call"></h2>',
|
||||
'<p>',
|
||||
' <button class="btn btn-success btn-accept"',
|
||||
' data-l10n-id="accept_button"></button>',
|
||||
' <button class="btn btn-error btn-decline"',
|
||||
' data-l10n-id="decline_button"></button>',
|
||||
'</p>'
|
||||
].join("")),
|
||||
|
||||
className: "incoming-call",
|
||||
|
||||
events: {
|
||||
"click .btn-accept": "handleAccept",
|
||||
"click .btn-decline": "handleDecline"
|
||||
},
|
||||
|
||||
/**
|
||||
* User clicked on the "accept" button.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleAccept: function(event) {
|
||||
event.preventDefault();
|
||||
this.model.trigger("accept");
|
||||
},
|
||||
|
||||
/**
|
||||
* User clicked on the "decline" button.
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleDecline: function(event) {
|
||||
event.preventDefault();
|
||||
// XXX For now, we just close the window.
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Call ended view.
|
||||
* @type {loop.shared.views.BaseView}
|
||||
*/
|
||||
var EndedCallView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<p>',
|
||||
' <button class="btn btn-info" data-l10n-id="close_window"></button>',
|
||||
'</p>'
|
||||
].join("")),
|
||||
|
||||
className: "call-ended",
|
||||
|
||||
events: {
|
||||
"click button": "closeWindow"
|
||||
},
|
||||
|
||||
closeWindow: function(event) {
|
||||
event.preventDefault();
|
||||
// XXX For now, we just close the window.
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation router.
|
||||
*
|
||||
* Required options:
|
||||
* - {loop.shared.models.ConversationModel} conversation Conversation model.
|
||||
* - {loop.shared.components.Notifier} notifier Notifier component.
|
||||
*
|
||||
* @type {loop.shared.router.BaseConversationRouter}
|
||||
*/
|
||||
var ConversationRouter = loop.desktopRouter.DesktopConversationRouter.extend({
|
||||
routes: {
|
||||
"incoming/:version": "incoming",
|
||||
"call/accept": "accept",
|
||||
"call/ongoing": "conversation",
|
||||
"call/ended": "ended"
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {loop.shared.router.BaseConversationRouter.startCall}
|
||||
*/
|
||||
startCall: function() {
|
||||
this.navigate("call/ongoing", {trigger: true});
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {loop.shared.router.BaseConversationRouter.endCall}
|
||||
*/
|
||||
endCall: function() {
|
||||
this.navigate("call/ended", {trigger: true});
|
||||
},
|
||||
|
||||
/**
|
||||
* Incoming call route.
|
||||
*
|
||||
* @param {String} loopVersion The version from the push notification, set
|
||||
* by the router from the URL.
|
||||
*/
|
||||
incoming: function(loopVersion) {
|
||||
this._conversation.set({loopVersion: loopVersion});
|
||||
this._conversation.once("accept", function() {
|
||||
this.navigate("call/accept", {trigger: true});
|
||||
}.bind(this));
|
||||
this.loadView(new IncomingCallView({model: this._conversation}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Accepts an incoming call.
|
||||
*/
|
||||
accept: function() {
|
||||
this._conversation.initiate({
|
||||
baseServerUrl: window.navigator.mozLoop.serverUrl,
|
||||
outgoing: false
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* conversation is the route when the conversation is active. The start
|
||||
* route should be navigated to first.
|
||||
*/
|
||||
conversation: function() {
|
||||
if (!this._conversation.isSessionReady()) {
|
||||
console.error("Error: navigated to conversation route without " +
|
||||
"the start route to initialise the call first");
|
||||
this._notifier.errorL10n("cannot_start_call_session_not_ready");
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadView(
|
||||
new loop.shared.views.ConversationView({
|
||||
sdk: OT,
|
||||
model: this._conversation
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* XXX: load a view with a close button for now?
|
||||
*/
|
||||
ended: function() {
|
||||
this.loadView(new EndedCallView());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel initialisation.
|
||||
*/
|
||||
function init() {
|
||||
// Do the initial L10n setup, we do this before anything
|
||||
// else to ensure the L10n environment is setup correctly.
|
||||
mozL10n.initialize(window.navigator.mozLoop);
|
||||
|
||||
router = new ConversationRouter({
|
||||
conversation: new loop.shared.models.ConversationModel({}, {sdk: OT}),
|
||||
notifier: new sharedViews.NotificationListView({el: "#messages"})
|
||||
});
|
||||
Backbone.history.start();
|
||||
}
|
||||
|
||||
return {
|
||||
ConversationRouter: ConversationRouter,
|
||||
EndedCallView: EndedCallView,
|
||||
IncomingCallView: IncomingCallView,
|
||||
init: init
|
||||
};
|
||||
})(window.OT, document.mozL10n);
|
34
browser/components/loop/content/js/desktopRouter.js
Normal file
@ -0,0 +1,34 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.desktopRouter = (function() {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* On the desktop app, the use of about: uris prevents us from changing the
|
||||
* url of the location. As a result, we change the navigate function to simply
|
||||
* activate the new routes, and not try changing the url.
|
||||
*
|
||||
* XXX It is conceivable we might be able to remove this in future, if we
|
||||
* can either swap to resource uris or remove the limitation on the about uris.
|
||||
*/
|
||||
var extendedRouter = {
|
||||
navigate: function(to) {
|
||||
this[this.routes[to]]();
|
||||
}
|
||||
};
|
||||
|
||||
var DesktopRouter = loop.shared.router.BaseRouter.extend(extendedRouter);
|
||||
|
||||
var DesktopConversationRouter =
|
||||
loop.shared.router.BaseConversationRouter.extend(extendedRouter);
|
||||
|
||||
return {
|
||||
DesktopRouter: DesktopRouter,
|
||||
DesktopConversationRouter: DesktopConversationRouter
|
||||
};
|
||||
})();
|
248
browser/components/loop/content/js/panel.js
Normal file
@ -0,0 +1,248 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.panel = (function(_, mozL10n) {
|
||||
"use strict";
|
||||
|
||||
var sharedViews = loop.shared.views,
|
||||
// aliasing translation function as __ for concision
|
||||
__ = mozL10n.get;
|
||||
|
||||
/**
|
||||
* Panel router.
|
||||
* @type {loop.desktopRouter.DesktopRouter}
|
||||
*/
|
||||
var router;
|
||||
|
||||
/**
|
||||
* Do not disturb panel subview.
|
||||
*/
|
||||
var DoNotDisturbView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<label>',
|
||||
' <input type="checkbox" <%- checked %>>',
|
||||
' <span data-l10n-id="do_not_disturb"></span>',
|
||||
'</label>',
|
||||
].join('')),
|
||||
|
||||
events: {
|
||||
"click input[type=checkbox]": "toggle"
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles mozLoop activation status.
|
||||
*/
|
||||
toggle: function() {
|
||||
navigator.mozLoop.doNotDisturb = !navigator.mozLoop.doNotDisturb;
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
checked: navigator.mozLoop.doNotDisturb ? "checked" : ""
|
||||
}));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel view.
|
||||
*/
|
||||
var PanelView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<div class="description">',
|
||||
' <p data-l10n-id="get_link_to_share"></p>',
|
||||
'</div>',
|
||||
'<div class="action">',
|
||||
' <form class="invite">',
|
||||
' <input type="text" name="caller" data-l10n-id="caller" required>',
|
||||
' <button type="submit" class="get-url btn btn-success"',
|
||||
' data-l10n-id="get_a_call_url"></button>',
|
||||
' </form>',
|
||||
' <p class="result hide">',
|
||||
' <input id="call-url" type="url" readonly>',
|
||||
' <a class="go-back btn btn-info" href="" data-l10n-id="new_url"></a>',
|
||||
' </p>',
|
||||
' <p class="dnd"></p>',
|
||||
'</div>',
|
||||
].join("")),
|
||||
|
||||
className: "share generate-url",
|
||||
|
||||
/**
|
||||
* Do not disturb view.
|
||||
* @type {DoNotDisturbView|undefined}
|
||||
*/
|
||||
dndView: undefined,
|
||||
|
||||
events: {
|
||||
"keyup input[name=caller]": "changeButtonState",
|
||||
"submit form.invite": "getCallUrl",
|
||||
"click a.go-back": "goBack"
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
if (!options.notifier) {
|
||||
throw new Error("missing required notifier");
|
||||
}
|
||||
this.notifier = options.notifier;
|
||||
this.client = new loop.shared.Client({
|
||||
baseServerUrl: navigator.mozLoop.serverUrl
|
||||
});
|
||||
},
|
||||
|
||||
getNickname: function() {
|
||||
return this.$("input[name=caller]").val();
|
||||
},
|
||||
|
||||
getCallUrl: function(event) {
|
||||
this.notifier.clear();
|
||||
event.preventDefault();
|
||||
var callback = function(err, callUrlData) {
|
||||
this.clearPending();
|
||||
if (err) {
|
||||
this.notifier.errorL10n("unable_retrieve_url");
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
this.onCallUrlReceived(callUrlData);
|
||||
}.bind(this);
|
||||
|
||||
this.setPending();
|
||||
this.client.requestCallUrl(this.getNickname(), callback);
|
||||
},
|
||||
|
||||
goBack: function(event) {
|
||||
event.preventDefault();
|
||||
this.$(".action .result").hide();
|
||||
this.$(".action .invite").show();
|
||||
this.$(".description p").text(__("get_link_to_share"));
|
||||
this.changeButtonState();
|
||||
},
|
||||
|
||||
onCallUrlReceived: function(callUrlData) {
|
||||
this.notifier.clear();
|
||||
this.$(".action .invite").hide();
|
||||
this.$(".action .invite input").val("");
|
||||
this.$(".action .result input").val(callUrlData.call_url);
|
||||
this.$(".action .result").show();
|
||||
this.$(".description p").text(__("share_link_url"));
|
||||
},
|
||||
|
||||
setPending: function() {
|
||||
this.$("[name=caller]").addClass("pending");
|
||||
this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
|
||||
},
|
||||
|
||||
clearPending: function() {
|
||||
this.$("[name=caller]").removeClass("pending");
|
||||
this.changeButtonState();
|
||||
},
|
||||
|
||||
changeButtonState: function() {
|
||||
var enabled = !!this.$("input[name=caller]").val();
|
||||
if (enabled) {
|
||||
this.$(".get-url").removeClass("disabled")
|
||||
.removeAttr("disabled", "disabled");
|
||||
} else {
|
||||
this.$(".get-url").addClass("disabled").attr("disabled", "disabled");
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.template());
|
||||
// Do not Disturb sub view
|
||||
this.dndView = new DoNotDisturbView({el: this.$(".dnd")}).render();
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
|
||||
/**
|
||||
* DOM document object.
|
||||
* @type {HTMLDocument}
|
||||
*/
|
||||
document: undefined,
|
||||
|
||||
routes: {
|
||||
"": "home"
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
if (!options.document) {
|
||||
throw new Error("missing required document");
|
||||
}
|
||||
this.document = options.document;
|
||||
|
||||
this._registerVisibilityChangeEvent();
|
||||
|
||||
this.on("panel:open panel:closed", this.reset, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Register the DOM visibility API event for the whole document, and trigger
|
||||
* appropriate events accordingly:
|
||||
*
|
||||
* - `panel:opened` when the panel is open
|
||||
* - `panel:closed` when the panel is closed
|
||||
*
|
||||
* @link http://www.w3.org/TR/page-visibility/
|
||||
*/
|
||||
_registerVisibilityChangeEvent: function() {
|
||||
this.document.addEventListener("visibilitychange", function(event) {
|
||||
this.trigger(event.currentTarget.hidden ? "panel:closed"
|
||||
: "panel:open");
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Default entry point.
|
||||
*/
|
||||
home: function() {
|
||||
this.reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Resets this router to its initial state.
|
||||
*/
|
||||
reset: function() {
|
||||
// purge pending notifications
|
||||
this._notifier.clear();
|
||||
// reset home view
|
||||
this.loadView(new PanelView({notifier: this._notifier}));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Panel initialisation.
|
||||
*/
|
||||
function init() {
|
||||
// Do the initial L10n setup, we do this before anything
|
||||
// else to ensure the L10n environment is setup correctly.
|
||||
mozL10n.initialize(navigator.mozLoop);
|
||||
|
||||
router = new PanelRouter({
|
||||
document: document,
|
||||
notifier: new sharedViews.NotificationListView({el: "#messages"})
|
||||
});
|
||||
Backbone.history.start();
|
||||
|
||||
// Notify the window that we've finished initalization and initial layout
|
||||
var evtObject = document.createEvent('Event');
|
||||
evtObject.initEvent('loopPanelInitialized', true, false);
|
||||
window.dispatchEvent(evtObject);
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
PanelView: PanelView,
|
||||
DoNotDisturbView: DoNotDisturbView,
|
||||
PanelRouter: PanelRouter
|
||||
};
|
||||
})(_, document.mozL10n);
|
119
browser/components/loop/content/libs/l10n.js
Normal file
@ -0,0 +1,119 @@
|
||||
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
'use strict';
|
||||
|
||||
// This is a modified version of l10n.js that the pdf.js extension uses.
|
||||
// It uses an explicitly passed object for the strings/locale functionality,
|
||||
// and does not automatically translate on DOMContentLoaded, but requires
|
||||
// initialize to be called. This improves testability and helps to avoid race
|
||||
// conditions.
|
||||
(function(window) {
|
||||
var gL10nDetails;
|
||||
var gLanguage = '';
|
||||
|
||||
// fetch an l10n objects
|
||||
function getL10nData(key) {
|
||||
var response = gL10nDetails.getStrings(key);
|
||||
var data = JSON.parse(response);
|
||||
if (!data)
|
||||
console.warn('[l10n] #' + key + ' missing for [' + gLanguage + ']');
|
||||
return data;
|
||||
}
|
||||
|
||||
// replace {{arguments}} with their values
|
||||
function substArguments(text, args) {
|
||||
if (!args)
|
||||
return text;
|
||||
|
||||
return text.replace(/\{\{\s*(\w+)\s*\}\}/g, function(all, name) {
|
||||
return name in args ? args[name] : '{{' + name + '}}';
|
||||
});
|
||||
}
|
||||
|
||||
// translate a string
|
||||
function translateString(key, args, fallback) {
|
||||
var data = getL10nData(key);
|
||||
if (!data && fallback)
|
||||
data = {textContent: fallback};
|
||||
if (!data)
|
||||
return '{{' + key + '}}';
|
||||
return substArguments(data.textContent, args);
|
||||
}
|
||||
|
||||
// translate an HTML element
|
||||
function translateElement(element) {
|
||||
if (!element || !element.dataset)
|
||||
return;
|
||||
|
||||
// get the related l10n object
|
||||
var key = element.dataset.l10nId;
|
||||
var data = getL10nData(key);
|
||||
if (!data)
|
||||
return;
|
||||
|
||||
// get arguments (if any)
|
||||
// TODO: more flexible parser?
|
||||
var args;
|
||||
if (element.dataset.l10nArgs) try {
|
||||
args = JSON.parse(element.dataset.l10nArgs);
|
||||
} catch (e) {
|
||||
console.warn('[l10n] could not parse arguments for #' + key + '');
|
||||
}
|
||||
|
||||
// translate element
|
||||
// TODO: security check?
|
||||
for (var k in data)
|
||||
element[k] = substArguments(data[k], args);
|
||||
}
|
||||
|
||||
|
||||
// translate an HTML subtree
|
||||
function translateFragment(element) {
|
||||
element = element || document.querySelector('html');
|
||||
|
||||
// check all translatable children (= w/ a `data-l10n-id' attribute)
|
||||
var children = element.querySelectorAll('*[data-l10n-id]');
|
||||
var elementCount = children.length;
|
||||
for (var i = 0; i < elementCount; i++)
|
||||
translateElement(children[i]);
|
||||
|
||||
// translate element itself if necessary
|
||||
if (element.dataset.l10nId)
|
||||
translateElement(element);
|
||||
}
|
||||
|
||||
// Public API
|
||||
document.mozL10n = {
|
||||
/**
|
||||
* Called to do the initial translation, this should be called
|
||||
* when DOMContentLoaded is fired, or the equivalent time.
|
||||
*
|
||||
* @param {Object} l10nDetails An object implementing the locale attribute
|
||||
* and getStrings(key) function.
|
||||
*/
|
||||
initialize: function(l10nDetails) {
|
||||
gL10nDetails = l10nDetails;
|
||||
gLanguage = gL10nDetails.locale;
|
||||
|
||||
translateFragment();
|
||||
},
|
||||
|
||||
// get a localized string
|
||||
get: translateString,
|
||||
|
||||
// get the document language
|
||||
getLanguage: function() { return gLanguage; },
|
||||
|
||||
// get the direction (ltr|rtl) of the current language
|
||||
getDirection: function() {
|
||||
// http://www.w3.org/International/questions/qa-scripts
|
||||
// Arabic, Hebrew, Farsi, Pashto, Urdu
|
||||
var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
|
||||
return (rtlList.indexOf(gLanguage) >= 0) ? 'rtl' : 'ltr';
|
||||
},
|
||||
|
||||
// translate an element or document fragment
|
||||
translate: translateFragment
|
||||
};
|
||||
})(this);
|
33
browser/components/loop/content/panel.html
Normal file
@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Loop Panel</title>
|
||||
<link rel="stylesheet" type="text/css" href="loop/shared/css/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="loop/shared/css/panel.css">
|
||||
</head>
|
||||
<body class="panel" onload="loop.panel.init();">
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<div id="main"></div>
|
||||
|
||||
<script type="text/javascript" src="loop/libs/l10n.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/token.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
|
||||
<script type="text/javascript" src="loop/shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="loop/shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="loop/js/desktopRouter.js"></script>
|
||||
<script type="text/javascript" src="loop/js/panel.js"></script>
|
||||
</body>
|
||||
</html>
|
12
browser/components/loop/content/shared/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
Loop Shared Web Assets
|
||||
======================
|
||||
|
||||
This directory contains web assets shared across the Loop client webapp and the
|
||||
Loop Firefox Component.
|
||||
|
||||
Warning
|
||||
-------
|
||||
|
||||
Any modification in these files will have possible side effects on both the
|
||||
Firefox component and the webapp. The `css/readme.html` file uses all the shared
|
||||
styles, you should use it as a way of checking for visual regressions.
|
148
browser/components/loop/content/shared/css/common.css
Normal file
@ -0,0 +1,148 @@
|
||||
/* 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/. */
|
||||
|
||||
/* Generic rules */
|
||||
|
||||
/**
|
||||
* "Fixes" the Box Model.
|
||||
* @see http://www.paulirish.com/2012/box-sizing-border-box-ftw/
|
||||
*/
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans;
|
||||
font-size: 14px;
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: .9em; /* for some reason, text is larger within <button> */
|
||||
}
|
||||
|
||||
img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Helpers */
|
||||
|
||||
/**
|
||||
* Clearfix impl. for modern browsers
|
||||
* 1. The space content is one way to avoid an Opera bug when the
|
||||
* contenteditable attribute is included anywhere else in the document.
|
||||
* Otherwise it causes space to appear at the top and bottom of elements
|
||||
* that are clearfixed.
|
||||
* 2. The use of `table` rather than `block` is only necessary if using
|
||||
* `:before` to contain the top-margins of child elements.
|
||||
*/
|
||||
.cf:before,
|
||||
.cf:after {
|
||||
content: " "; /* 1 */
|
||||
display: table; /* 2 */
|
||||
}
|
||||
|
||||
.cf:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tc {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background: #a5a;
|
||||
border: none;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: .25em .5em .3em;
|
||||
border-radius: .2em;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: #428BCA;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #5cb85c;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f0ad4e;
|
||||
}
|
||||
|
||||
.btn-error {
|
||||
background: #d9534f;
|
||||
}
|
||||
|
||||
.disabled, button[disabled] {
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
background: #eee;
|
||||
padding: .2em 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.alert p.message {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.alert.alert-error {
|
||||
background: #f99;
|
||||
border: 1px solid #f77;
|
||||
}
|
||||
|
||||
.alert.alert-warning {
|
||||
background: #fcf8e3;
|
||||
border: 1px solid #fbeed5;
|
||||
}
|
||||
|
||||
.alert .close {
|
||||
position: relative;
|
||||
top: -.2em;
|
||||
right: -1em;
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 1em;
|
||||
color: #000;
|
||||
opacity: .2;
|
||||
}
|
||||
|
||||
.close:before {
|
||||
/* \2716 is unicode representation of the close button icon */
|
||||
content: '\2716';
|
||||
}
|
||||
|
||||
button.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.fade-out {
|
||||
transition: opacity 0.5s ease-in;
|
||||
opacity: 0;
|
||||
}
|
73
browser/components/loop/content/shared/css/conversation.css
Normal file
@ -0,0 +1,73 @@
|
||||
/* 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/. */
|
||||
|
||||
/* Conversation window styles */
|
||||
|
||||
.conversation .controls {
|
||||
background: #f2f2f2;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.conversation .media video {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
/* Nested video elements */
|
||||
|
||||
.conversation .media.nested {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.conversation .media.nested .remote {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.conversation .media.nested .local {
|
||||
position: absolute;
|
||||
bottom: .8em;
|
||||
right: .8em;
|
||||
width: 30%;
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
/* Side by side video elements */
|
||||
|
||||
.conversation .media.side-by-side .remote {
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.conversation .media.side-by-side .local {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriden SDK styles; .OT_video-container is using absolute positioning
|
||||
* therefore moves the video elements it contains outside of the current
|
||||
* viewport.
|
||||
*/
|
||||
.conversation .OT_video-container {
|
||||
position: inherit;
|
||||
}
|
||||
|
||||
/* Call ended view */
|
||||
.call-ended p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Incoming call */
|
||||
.incoming-call {
|
||||
text-align: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.incoming-call h2 {
|
||||
font-size: 1.5em;
|
||||
font-weight: normal;
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.incoming-call button {
|
||||
margin-right: .2em;
|
||||
}
|
83
browser/components/loop/content/shared/css/panel.css
Normal file
@ -0,0 +1,83 @@
|
||||
/* 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/. */
|
||||
|
||||
/* Panel styles */
|
||||
|
||||
.panel {
|
||||
/* XXX the Social API panel behaves weirdly on inner element size changes,
|
||||
adding unwanted scrollbars; quickfix is to hide these for now. */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.share {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.share .description {
|
||||
background: #f7f7f7 url("../img/icon_32.png") no-repeat 1em 1.5em;
|
||||
border-bottom: 1px solid #c3c3c3;
|
||||
padding: 1em 1em 0 4em;
|
||||
}
|
||||
|
||||
.share .description .field {
|
||||
padding-bottom: 1em;
|
||||
border-bottom: 1px dotted #ddd;
|
||||
}
|
||||
|
||||
.share .description select {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.share .description .preview video {
|
||||
background: #ccc;
|
||||
float: right;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.share .action {
|
||||
clear: right;
|
||||
padding: 1em;
|
||||
border-top: 1px solid #fafafa;
|
||||
}
|
||||
|
||||
.share .action p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.share .action p.dnd {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.share .action input[type="text"],
|
||||
.share .action input[type="url"] {
|
||||
border: 1px solid #ccc; /* Overriding background style for a text input (see
|
||||
below) resets its borders to a weird beveled style;
|
||||
defining a default 1px border solves the issue. */
|
||||
font-size: .9em;
|
||||
width: 65%;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.share .action input.pending {
|
||||
background-image: url(../img/loading-icon.gif);
|
||||
background-repeat: no-repeat;
|
||||
background-position: right;
|
||||
}
|
||||
|
||||
/* For some reason, buttons have a bigger default font size in FF; we're
|
||||
reducing a bit for graphical consistency here. */
|
||||
.share .action button {
|
||||
font-size: .9em;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* Specific cases */
|
||||
|
||||
.panel #messages .alert {
|
||||
margin-bottom: 0;
|
||||
}
|
187
browser/components/loop/content/shared/css/readme.html
Normal file
@ -0,0 +1,187 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
|
||||
<!-- This file is intended to help frontend developers to easily identify what
|
||||
are the available styles for the Loop UI components. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Loop UI shared CSS information/demo</title>
|
||||
<link type="text/css" rel="stylesheet" href="common.css">
|
||||
<link type="text/css" rel="stylesheet" href="panel.css">
|
||||
<link type="text/css" rel="stylesheet" href="conversation.css">
|
||||
<style>
|
||||
body {
|
||||
width: 600px;
|
||||
margin: 1em auto;
|
||||
background: #fff;
|
||||
font-family: Helvetica, Arial, sans;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Loop UI toolkit</h1>
|
||||
|
||||
<h2>Logo icons</h2>
|
||||
|
||||
<p>
|
||||
<img src="../img/icon_32.png"> 32x32 transparent PNG
|
||||
<img src="../img/icon_64.png"> 64x64 transparent PNG
|
||||
</p>
|
||||
|
||||
<p><em><strong>Note:</strong> these are temporary.</em></p>
|
||||
|
||||
<h2>Share panel</h2>
|
||||
|
||||
<h3>Simple</h3>
|
||||
|
||||
<div class="share">
|
||||
<form class="description">
|
||||
<p>This is a simple message.</p>
|
||||
</form>
|
||||
<div class="action">
|
||||
<p><input type="url" value="http://loop.im/plop75"></p>
|
||||
<p>Your name will appear as <a href="">Unnamed</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Featuring options</h3>
|
||||
|
||||
<div class="share">
|
||||
<form class="description">
|
||||
<p class="field">
|
||||
<label>Share this link with a friend to
|
||||
<select>
|
||||
<option>browse together</option>
|
||||
<option selected="selected">video chat</option>
|
||||
<option>audio chat</option>
|
||||
<option>text chat</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p class="field">
|
||||
<label>
|
||||
Use webcam <select><option>Foo</option></select>
|
||||
</label>
|
||||
</p>
|
||||
<p class="field">
|
||||
<label>Use whatever
|
||||
<select><option>Long foo is long indeed</option></select>
|
||||
</label>
|
||||
</p>
|
||||
<p class="preview cf">
|
||||
Preview <video></video>
|
||||
</p>
|
||||
</form>
|
||||
<div class="action">
|
||||
<p><input type="url" value="http://loop.im/plop75"></p>
|
||||
<p>Your name will appear as <a href="">Unnamed</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Conversation window</h2>
|
||||
|
||||
<p><em>The conversation component adapts automatically to its container to
|
||||
occupy all the available space.</em></p>
|
||||
|
||||
<h3>Large</h3>
|
||||
|
||||
<div class="conversation">
|
||||
<div class="media nested">
|
||||
<video class="remote"></video>
|
||||
<video class="local"></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Large with controls</h3>
|
||||
|
||||
<div class="conversation">
|
||||
<nav class="controls">
|
||||
<button class="btn">Start</button>
|
||||
<button class="btn">Stop</button>
|
||||
</nav>
|
||||
<div class="media nested">
|
||||
<video class="remote"></video>
|
||||
<video class="local"></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Small (think chat window)</h3>
|
||||
|
||||
<div style="width: 204px">
|
||||
<div class="conversation">
|
||||
<div class="media nested">
|
||||
<video class="remote"></video>
|
||||
<video class="local"></video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Side by side</h3>
|
||||
|
||||
<div class="conversation">
|
||||
<div class="media side-by-side">
|
||||
<video class="remote"></video>
|
||||
<video class="local"></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Buttons</h2>
|
||||
|
||||
<h3>Using <code><a></code></h3>
|
||||
|
||||
<p>
|
||||
<a href="" class="btn">default</a>
|
||||
<a href="" class="btn btn-info">info</a>
|
||||
<a href="" class="btn btn-success">success</a>
|
||||
<a href="" class="btn btn-warning">warning</a>
|
||||
<a href="" class="btn btn-error">error</a>
|
||||
</p>
|
||||
|
||||
<h3>Inline</h3>
|
||||
|
||||
<p>Click <a href="" class="btn btn-info">here</a>.</p>
|
||||
|
||||
<h3>Using <code><button></code></h3>
|
||||
|
||||
<p>
|
||||
<button class="btn">default</button>
|
||||
<button class="btn btn-info">info</button>
|
||||
<button class="btn btn-success">success</button>
|
||||
<button class="btn btn-warning">warning</button>
|
||||
<button class="btn btn-error">error</button>
|
||||
</p>
|
||||
|
||||
<h2>Alerts</h2>
|
||||
|
||||
<div class="alert alert-error">
|
||||
<button class="close"></button>
|
||||
<p class="message">Oops! Something went really wrong.</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<button class="close"></button>
|
||||
<p class="message">Oops! This is a warning.</p>
|
||||
</div>
|
||||
|
||||
<h2>Incoming call</h2>
|
||||
|
||||
<div class="incoming-call">
|
||||
<h2>Incoming call</h2>
|
||||
<p>
|
||||
<button class="btn btn-success btn-accept">Accept</button>
|
||||
<button class="btn btn-error btn-decline">Decline</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.onload = function() {
|
||||
[].forEach.call(document.querySelectorAll("video"), function(video) {
|
||||
video.setAttribute("src", "http://v2v.cc/~j/theora_testsuite/320x240.ogg");
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
browser/components/loop/content/shared/img/icon_32.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
browser/components/loop/content/shared/img/icon_64.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
browser/components/loop/content/shared/img/loading-icon.gif
Normal file
After Width: | Height: | Size: 2.5 KiB |
362
browser/components/loop/content/shared/js/client.js
Normal file
@ -0,0 +1,362 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true, hawk, deriveHawkCredentials */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.Client = (function($) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Loop server client.
|
||||
*
|
||||
* @param {Object} settings Settings object.
|
||||
*/
|
||||
function Client(settings) {
|
||||
settings = settings || {};
|
||||
if (!settings.hasOwnProperty("baseServerUrl") ||
|
||||
!settings.baseServerUrl) {
|
||||
throw new Error("missing required baseServerUrl");
|
||||
}
|
||||
|
||||
// allowing an |in| test rather than a more type || allows us to dependency
|
||||
// inject a non-existent mozLoop
|
||||
if ("mozLoop" in settings) {
|
||||
this.mozLoop = settings.mozLoop;
|
||||
} else {
|
||||
this.mozLoop = navigator.mozLoop;
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
Client.prototype = {
|
||||
/**
|
||||
* Converts from hours to seconds
|
||||
*/
|
||||
_hoursToSeconds: function(value) {
|
||||
return value * 60 * 60;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validates a data object to confirm it has the specified properties.
|
||||
*
|
||||
* @param {Object} The data object to verify
|
||||
* @param {Array} 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 jqXHR See jQuery docs
|
||||
* @param textStatus See jQuery docs
|
||||
* @param errorThrown See jQuery docs
|
||||
*/
|
||||
_failureHandler: function(cb, jqXHR, textStatus, errorThrown) {
|
||||
var error = "Unknown error.",
|
||||
jsonRes = jqXHR && jqXHR.responseJSON || {};
|
||||
// Received error response format:
|
||||
// { "status": "errors",
|
||||
// "errors": [{
|
||||
// "location": "url",
|
||||
// "name": "token",
|
||||
// "description": "invalid token"
|
||||
// }]}
|
||||
if (jsonRes.status === "errors" && Array.isArray(jsonRes.errors)) {
|
||||
error = "Details: " + jsonRes.errors.map(function(err) {
|
||||
return Object.keys(err).map(function(field) {
|
||||
return field + ": " + err[field];
|
||||
}).join(", ");
|
||||
}).join("; ");
|
||||
}
|
||||
var message = "HTTP " + jqXHR.status + " " + errorThrown +
|
||||
"; " + error;
|
||||
console.error(message);
|
||||
cb(new Error(message));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures the client is registered with the push server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
*/
|
||||
_ensureRegistered: function(cb) {
|
||||
navigator.mozLoop.ensureRegistered(function(err) {
|
||||
cb(err);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensures that the client picks up the hawk-session-token
|
||||
* put in preferences by the LoopService registration code,
|
||||
* derives hawk credentials from them, and saves them in
|
||||
* this._credentials.
|
||||
*
|
||||
* @param {Function} cb Callback(err)
|
||||
* if err is set to null in the callback, that indicates that the
|
||||
* credentials have been successfully attached to this object.
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @note That as currently written, this is only ever expected to be called
|
||||
* from browser UI code (ie it relies on mozLoop).
|
||||
*/
|
||||
_ensureCredentials: function(cb) {
|
||||
if (this._credentials) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var hawkSessionToken =
|
||||
this.mozLoop.getLoopCharPref("hawk-session-token");
|
||||
if (!hawkSessionToken) {
|
||||
var msg = "loop.hawk-session-token pref not found";
|
||||
console.warn(msg);
|
||||
cb(new Error(msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX do we want to use any of the other hawk params (eg to track clock
|
||||
// skew, etc)?
|
||||
var serverDerivedKeyLengthInBytes = 2 * 32;
|
||||
deriveHawkCredentials(hawkSessionToken, "sessionToken",
|
||||
serverDerivedKeyLengthInBytes, function (hawkCredentials) {
|
||||
this._credentials = hawkCredentials;
|
||||
cb(null);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal handler for requesting a call url from the server.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
* - callUrlData an object of the obtained call url data if successful:
|
||||
* -- call_url: The url of the call
|
||||
* -- expiresAt: The amount of hours until expiry of the url
|
||||
*
|
||||
* @param {String} simplepushUrl a registered Simple Push URL
|
||||
* @param {string} nickname the nickname of the future caller
|
||||
* @param {Function} cb Callback(err, callUrlData)
|
||||
*/
|
||||
_requestCallUrlInternal: function(nickname, cb) {
|
||||
var endpoint = this.settings.baseServerUrl + "/call-url/",
|
||||
reqData = {callerId: nickname};
|
||||
|
||||
var req = $.ajax({
|
||||
type: "POST",
|
||||
url: endpoint,
|
||||
data: reqData,
|
||||
xhrFields: {
|
||||
withCredentials: false
|
||||
},
|
||||
crossDomain: true,
|
||||
beforeSend: function (xhr, settings) {
|
||||
try {
|
||||
this._attachAnyServerCreds(xhr, settings);
|
||||
} catch (ex) {
|
||||
cb(ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}.bind(this),
|
||||
success: function(callUrlData) {
|
||||
// XXX split this out into two functions for better readability
|
||||
try {
|
||||
cb(null, this._validate(callUrlData, ["call_url", "expiresAt"]));
|
||||
|
||||
var expiresHours = this._hoursToSeconds(callUrlData.expiresAt);
|
||||
navigator.mozLoop.noteCallUrlExpiry(expiresHours);
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests a call URL from the Loop server. It will note the
|
||||
* expiry time for the url with the mozLoop api.
|
||||
*
|
||||
* Callback parameters:
|
||||
* - err null on successful registration, non-null otherwise.
|
||||
* - callUrlData an object of the obtained call url data if successful:
|
||||
* -- call_url: The url of the call
|
||||
* -- expiresAt: The amount of hours until expiry of the url
|
||||
*
|
||||
* @param {String} simplepushUrl a registered Simple Push URL
|
||||
* @param {string} nickname the nickname of the future caller
|
||||
* @param {Function} cb Callback(err, callUrlData)
|
||||
*/
|
||||
requestCallUrl: function(nickname, cb) {
|
||||
this._ensureRegistered(function(err) {
|
||||
if (err) {
|
||||
console.log("Error registering with Loop server, code: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._ensureCredentials(function (err) {
|
||||
if (err) {
|
||||
console.log("Error setting up credentials: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._requestCallUrlInternal(nickname, cb);
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests call information from the server for all calls since the
|
||||
* given version.
|
||||
*
|
||||
* @param {String} version the version identifier from the push
|
||||
* notification
|
||||
* @param {Function} cb Callback(err, calls)
|
||||
*/
|
||||
requestCallsInfo: function(version, cb) {
|
||||
this._ensureCredentials(function (err) {
|
||||
if (err) {
|
||||
console.log("Error setting up credentials: " + err);
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this._requestCallsInfoInternal(version, cb);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
_requestCallsInfoInternal: function(version, cb) {
|
||||
if (!version) {
|
||||
throw new Error("missing required parameter version");
|
||||
}
|
||||
|
||||
var endpoint = this.settings.baseServerUrl + "/calls";
|
||||
|
||||
// XXX It is likely that we'll want to move some of this to whatever
|
||||
// opens the chat window, but we'll need to decide that once we make a
|
||||
// decision on chrome versus content, and know if we're going with
|
||||
// LoopService or a frameworker.
|
||||
var req = $.ajax({
|
||||
type: "GET",
|
||||
url: endpoint + "?version=" + version,
|
||||
xhrFields: {
|
||||
withCredentials: false
|
||||
},
|
||||
crossDomain: true,
|
||||
beforeSend: function (xhr, settings) {
|
||||
try {
|
||||
this._attachAnyServerCreds(xhr, settings);
|
||||
} catch (ex) {
|
||||
cb(ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}.bind(this),
|
||||
success: function(callsData) {
|
||||
try {
|
||||
cb(null, this._validate(callsData, ["calls"]));
|
||||
} catch (err) {
|
||||
console.log("Error requesting calls info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 {Function} cb Callback(err, sessionData)
|
||||
*/
|
||||
requestCallInfo: function(loopToken, cb) {
|
||||
if (!loopToken) {
|
||||
throw new Error("missing required parameter loopToken");
|
||||
}
|
||||
|
||||
var req = $.ajax({
|
||||
url: this.settings.baseServerUrl + "/calls/" + loopToken,
|
||||
method: "POST",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({}),
|
||||
dataType: "json"
|
||||
});
|
||||
|
||||
req.done(function(sessionData) {
|
||||
try {
|
||||
cb(null, this._validate(sessionData, [
|
||||
"sessionId", "sessionToken", "apiKey"
|
||||
]));
|
||||
} catch (err) {
|
||||
console.log("Error requesting call info", err);
|
||||
cb(err);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
req.fail(this._failureHandler.bind(this, cb));
|
||||
},
|
||||
|
||||
/**
|
||||
* If this._credentials is set, adds a hawk Authorization header based
|
||||
* based on those credentials to the passed-in XHR.
|
||||
*
|
||||
* @param xhr request to add any header to
|
||||
* @param settings settings object passed to jQuery.ajax()
|
||||
* @private
|
||||
*/
|
||||
_attachAnyServerCreds: function(xhr, settings) {
|
||||
// if the server needs credentials and didn't get them, it will
|
||||
// return failure for us, so if we don't have any creds, don't try to
|
||||
// attach them.
|
||||
if (!this._credentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
var header = hawk.client.header(settings.url, settings.type,
|
||||
{ credentials: this._credentials });
|
||||
if (header.err) {
|
||||
throw new Error(header.err);
|
||||
}
|
||||
|
||||
xhr.setRequestHeader("Authorization", header.field);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return Client;
|
||||
})(jQuery);
|
241
browser/components/loop/content/shared/js/models.js
Normal file
@ -0,0 +1,241 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.models = (function() {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Conversation model.
|
||||
*/
|
||||
var ConversationModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
ongoing: false, // Ongoing call flag
|
||||
callerId: undefined, // Loop caller id
|
||||
loopToken: undefined, // Loop conversation token
|
||||
loopVersion: undefined, // Loop version for /calls/ information. This
|
||||
// is the version received from the push
|
||||
// notification and is used by the server to
|
||||
// determine the pending calls
|
||||
sessionId: undefined, // OT session id
|
||||
sessionToken: undefined, // OT session token
|
||||
apiKey: undefined // OT api key
|
||||
},
|
||||
|
||||
/**
|
||||
* SDK object.
|
||||
* @type {OT}
|
||||
*/
|
||||
sdk: undefined,
|
||||
|
||||
/**
|
||||
* SDK session object.
|
||||
* @type {XXX}
|
||||
*/
|
||||
session: undefined,
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Required options:
|
||||
* - {OT} sdk: SDK object.
|
||||
*
|
||||
* @param {Object} attributes Attributes object.
|
||||
* @param {Object} options Options object.
|
||||
*/
|
||||
initialize: function(attributes, options) {
|
||||
options = options || {};
|
||||
if (!options.sdk) {
|
||||
throw new Error("missing required sdk");
|
||||
}
|
||||
this.sdk = options.sdk;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates a conversation, requesting call session information to the Loop
|
||||
* server and updates appropriately the current model attributes with the
|
||||
* data.
|
||||
*
|
||||
* Available options:
|
||||
*
|
||||
* - {String} baseServerUrl The server URL
|
||||
* - {Boolean} outgoing Set to true if this model represents the
|
||||
* outgoing call.
|
||||
*
|
||||
* Triggered events:
|
||||
*
|
||||
* - `session:ready` when the session information have been successfully
|
||||
* retrieved from the server;
|
||||
* - `session:error` when the request failed.
|
||||
*
|
||||
* @param {Object} options Options object
|
||||
*/
|
||||
initiate: function(options) {
|
||||
var client = new loop.shared.Client({
|
||||
baseServerUrl: options.baseServerUrl
|
||||
});
|
||||
|
||||
function handleResult(err, sessionData) {
|
||||
/*jshint validthis:true */
|
||||
if (err) {
|
||||
this.trigger("session:error", new Error(
|
||||
"Retrieval of session information failed: HTTP " + err));
|
||||
return;
|
||||
}
|
||||
|
||||
// XXX For incoming calls we might have more than one call queued.
|
||||
// For now, we'll just assume the first call is the right information.
|
||||
// We'll probably really want to be getting this data from the
|
||||
// background worker on the desktop client.
|
||||
// Bug 990714 should fix this.
|
||||
if (!options.outgoing)
|
||||
sessionData = sessionData[0];
|
||||
|
||||
this.setReady(sessionData);
|
||||
}
|
||||
|
||||
if (options.outgoing) {
|
||||
client.requestCallInfo(this.get("loopToken"), handleResult.bind(this));
|
||||
}
|
||||
else {
|
||||
client.requestCallsInfo(this.get("loopVersion"),
|
||||
handleResult.bind(this));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the session is ready.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
isSessionReady: function() {
|
||||
return !!this.get("sessionId");
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets session information and triggers the `session:ready` event.
|
||||
*
|
||||
* @param {Object} sessionData Conversation session information.
|
||||
*/
|
||||
setReady: function(sessionData) {
|
||||
// Explicit property assignment to prevent later "surprises"
|
||||
this.set({
|
||||
sessionId: sessionData.sessionId,
|
||||
sessionToken: sessionData.sessionToken,
|
||||
apiKey: sessionData.apiKey
|
||||
}).trigger("session:ready", this);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.session = this.sdk.initSession(this.get("sessionId"));
|
||||
this.listenTo(this.session, "sessionConnected", this._sessionConnected);
|
||||
this.listenTo(this.session, "streamCreated", this._streamCreated);
|
||||
this.listenTo(this.session, "connectionDestroyed",
|
||||
this._connectionDestroyed);
|
||||
this.listenTo(this.session, "sessionDisconnected",
|
||||
this._sessionDisconnected);
|
||||
this.listenTo(this.session, "networkDisconnected",
|
||||
this._networkDisconnected);
|
||||
this.session.connect(this.get("apiKey"), this.get("sessionToken"));
|
||||
},
|
||||
|
||||
/**
|
||||
* Ends current session.
|
||||
*/
|
||||
endSession: function() {
|
||||
this.session.disconnect();
|
||||
this.once("session:ended", this.stopListening, this);
|
||||
this.set("ongoing", false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Session is created.
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
|
||||
*
|
||||
* @param {SessionConnectEvent} event
|
||||
*/
|
||||
_sessionConnected: function(event) {
|
||||
this.trigger("session:connected", event);
|
||||
this.set("ongoing", true);
|
||||
},
|
||||
|
||||
/**
|
||||
* New created streams are available.
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
|
||||
*
|
||||
* @param {StreamEvent} event
|
||||
*/
|
||||
_streamCreated: function(event) {
|
||||
this.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) {
|
||||
this.trigger("session:ended");
|
||||
this.set("ongoing", false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Peer hung up. Disconnects local session.
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
|
||||
*
|
||||
* @param {ConnectionEvent} event
|
||||
*/
|
||||
_connectionDestroyed: function(event) {
|
||||
this.trigger("session:peer-hungup", {
|
||||
connectionId: event.connection.connectionId
|
||||
});
|
||||
this.endSession();
|
||||
},
|
||||
|
||||
/**
|
||||
* Network was disconnected.
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
|
||||
*
|
||||
* @param {ConnectionEvent} event
|
||||
*/
|
||||
_networkDisconnected: function(event) {
|
||||
this.trigger("session:network-disconnected");
|
||||
this.endSession();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification model.
|
||||
*/
|
||||
var NotificationModel = Backbone.Model.extend({
|
||||
defaults: {
|
||||
level: "info",
|
||||
message: ""
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification collection
|
||||
*/
|
||||
var NotificationCollection = Backbone.Collection.extend({
|
||||
model: NotificationModel
|
||||
});
|
||||
|
||||
return {
|
||||
ConversationModel: ConversationModel,
|
||||
NotificationCollection: NotificationCollection,
|
||||
NotificationModel: NotificationModel
|
||||
};
|
||||
})();
|
161
browser/components/loop/content/shared/js/router.js
Normal file
@ -0,0 +1,161 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.router = (function(l10n) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Base Router. Allows defining a main active view and ease toggling it when
|
||||
* the active route changes.
|
||||
*
|
||||
* @link http://mikeygee.com/blog/backbone.html
|
||||
*/
|
||||
var BaseRouter = Backbone.Router.extend({
|
||||
/**
|
||||
* Active view.
|
||||
* @type {loop.shared.views.BaseView}
|
||||
*/
|
||||
_activeView: undefined,
|
||||
|
||||
/**
|
||||
* Notifications dispatcher.
|
||||
* @type {loop.shared.views.NotificationListView}
|
||||
*/
|
||||
_notifier: undefined,
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Required options:
|
||||
* - {loop.shared.views.NotificationListView} notifier Notifier view.
|
||||
*
|
||||
* @param {Object} options Options object.
|
||||
*/
|
||||
constructor: function(options) {
|
||||
options = options || {};
|
||||
if (!options.notifier) {
|
||||
throw new Error("missing required notifier");
|
||||
}
|
||||
this._notifier = options.notifier;
|
||||
|
||||
Backbone.Router.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads and render current active view.
|
||||
*
|
||||
* @param {loop.shared.views.BaseView} view View.
|
||||
*/
|
||||
loadView : function(view) {
|
||||
if (this._activeView) {
|
||||
this._activeView.remove();
|
||||
}
|
||||
this._activeView = view.render().show();
|
||||
this.updateView(this._activeView.$el);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates main div element with provided contents.
|
||||
*
|
||||
* @param {jQuery} $el Element.
|
||||
*/
|
||||
updateView: function($el) {
|
||||
$("#main").html($el);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Base conversation router, implementing common behaviors when handling
|
||||
* a conversation.
|
||||
*/
|
||||
var BaseConversationRouter = BaseRouter.extend({
|
||||
/**
|
||||
* Current conversation.
|
||||
* @type {loop.shared.models.ConversationModel}
|
||||
*/
|
||||
_conversation: undefined,
|
||||
|
||||
/**
|
||||
* Constructor. Defining it as `constructor` allows implementing an
|
||||
* `initialize` method in child classes without needing calling this parent
|
||||
* one. See http://backbonejs.org/#Model-constructor (same for Router)
|
||||
*
|
||||
* Required options:
|
||||
* - {loop.shared.model.ConversationModel} model Conversation model.
|
||||
*
|
||||
* @param {Object} options Options object.
|
||||
*/
|
||||
constructor: function(options) {
|
||||
options = options || {};
|
||||
if (!options.conversation) {
|
||||
throw new Error("missing required conversation");
|
||||
}
|
||||
this._conversation = options.conversation;
|
||||
|
||||
this.listenTo(this._conversation, "session:ready", this._onSessionReady);
|
||||
this.listenTo(this._conversation, "session:ended", this._onSessionEnded);
|
||||
this.listenTo(this._conversation, "session:peer-hungup",
|
||||
this._onPeerHungup);
|
||||
this.listenTo(this._conversation, "session:network-disconnected",
|
||||
this._onNetworkDisconnected);
|
||||
|
||||
BaseRouter.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the call. This method should be overriden.
|
||||
*/
|
||||
startCall: function() {},
|
||||
|
||||
/**
|
||||
* Ends the call. This method should be overriden.
|
||||
*/
|
||||
endCall: function() {},
|
||||
|
||||
/**
|
||||
* Session is ready.
|
||||
*/
|
||||
_onSessionReady: function() {
|
||||
this.startCall();
|
||||
},
|
||||
|
||||
/**
|
||||
* Session has ended. Notifies the user and ends the call.
|
||||
*/
|
||||
_onSessionEnded: function() {
|
||||
this._notifier.warnL10n("call_has_ended");
|
||||
this.endCall();
|
||||
},
|
||||
|
||||
/**
|
||||
* Peer hung up. Notifies the user and ends the call.
|
||||
*
|
||||
* Event properties:
|
||||
* - {String} connectionId: OT session id
|
||||
*
|
||||
* @param {Object} event
|
||||
*/
|
||||
_onPeerHungup: function() {
|
||||
this._notifier.warnL10n("peer_ended_conversation");
|
||||
this.endCall();
|
||||
},
|
||||
|
||||
/**
|
||||
* Network disconnected. Notifies the user and ends the call.
|
||||
*/
|
||||
_onNetworkDisconnected: function() {
|
||||
this._notifier.warnL10n("network_disconnected");
|
||||
this.endCall();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
BaseRouter: BaseRouter,
|
||||
BaseConversationRouter: BaseConversationRouter
|
||||
};
|
||||
})(document.webL10n || document.mozL10n);
|
386
browser/components/loop/content/shared/js/views.js
Normal file
@ -0,0 +1,386 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.shared = loop.shared || {};
|
||||
loop.shared.views = (function(_, OT, l10n) {
|
||||
"use strict";
|
||||
|
||||
var sharedModels = loop.shared.models;
|
||||
|
||||
/**
|
||||
* L10n view. Translates resulting view DOM fragment once rendered.
|
||||
*/
|
||||
var L10nView = (function() {
|
||||
var L10nViewImpl = Backbone.View.extend(), // Original View constructor
|
||||
originalExtend = L10nViewImpl.extend; // Original static extend fn
|
||||
|
||||
/**
|
||||
* Patches View extend() method so we can hook and patch any declared render
|
||||
* method.
|
||||
*
|
||||
* @return {Backbone.View} Extended view with patched render() method.
|
||||
*/
|
||||
L10nViewImpl.extend = function() {
|
||||
var ExtendedView = originalExtend.apply(this, arguments),
|
||||
originalRender = ExtendedView.prototype.render;
|
||||
|
||||
/**
|
||||
* Wraps original render() method to translate contents once they're
|
||||
* rendered.
|
||||
*
|
||||
* @return {Backbone.View} Extended view instance.
|
||||
*/
|
||||
ExtendedView.prototype.render = function() {
|
||||
if (originalRender) {
|
||||
originalRender.apply(this, arguments);
|
||||
l10n.translate(this.el);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
return ExtendedView;
|
||||
};
|
||||
|
||||
return L10nViewImpl;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Base view.
|
||||
*/
|
||||
var BaseView = L10nView.extend({
|
||||
/**
|
||||
* Hides view element.
|
||||
*
|
||||
* @return {BaseView}
|
||||
*/
|
||||
hide: function() {
|
||||
this.$el.hide();
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows view element.
|
||||
*
|
||||
* @return {BaseView}
|
||||
*/
|
||||
show: function() {
|
||||
this.$el.show();
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Base render implementation: renders an attached template if available.
|
||||
*
|
||||
* Note: You need to override this if you want to do fancier stuff, eg.
|
||||
* rendering the template using model data.
|
||||
*
|
||||
* @return {BaseView}
|
||||
*/
|
||||
render: function() {
|
||||
if (this.template) {
|
||||
this.$el.html(this.template());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation view.
|
||||
*/
|
||||
var ConversationView = BaseView.extend({
|
||||
className: "conversation",
|
||||
|
||||
template: _.template([
|
||||
'<nav class="controls">',
|
||||
' <button class="btn stop" data-l10n-id="stop"></button>',
|
||||
'</nav>',
|
||||
'<div class="media nested">',
|
||||
// Both these wrappers are required by the SDK; this is fragile and
|
||||
// will break if a future version of the SDK updates this generated DOM,
|
||||
// especially as the SDK seems to actually move wrapped contents into
|
||||
// their own generated stuff.
|
||||
' <div class="remote"><div class="incoming"></div></div>',
|
||||
' <div class="local"><div class="outgoing"></div></div>',
|
||||
'</div>'
|
||||
].join("")),
|
||||
|
||||
// height set to "auto" to fix video layout on Google Chrome
|
||||
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=991122
|
||||
videoStyles: {
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
style: { "bugDisplayMode": "off" }
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .btn.stop': 'hangup'
|
||||
},
|
||||
|
||||
/**
|
||||
* Establishes webrtc communication using OT sdk.
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
if (!options.sdk) {
|
||||
throw new Error("missing required sdk");
|
||||
}
|
||||
this.sdk = options.sdk;
|
||||
|
||||
this.listenTo(this.model, "session:connected", this.publish);
|
||||
this.listenTo(this.model, "session:stream-created", this._streamCreated);
|
||||
this.listenTo(this.model, ["session:peer-hungup",
|
||||
"session:network-disconnected",
|
||||
"session:ended"].join(" "), this.unpublish);
|
||||
this.model.startSession();
|
||||
},
|
||||
|
||||
/**
|
||||
* 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.$(".incoming").get(0);
|
||||
event.streams.forEach(function(stream) {
|
||||
if (stream.connection.connectionId !==
|
||||
this.model.session.connection.connectionId) {
|
||||
this.model.session.subscribe(stream, incoming, this.videoStyles);
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Hangs up current conversation.
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
hangup: function(event) {
|
||||
event.preventDefault();
|
||||
this.unpublish();
|
||||
this.model.endSession();
|
||||
},
|
||||
|
||||
/**
|
||||
* Publishes remote streams available once a session is connected.
|
||||
*
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
|
||||
*
|
||||
* @param {SessionConnectEvent} event
|
||||
*/
|
||||
publish: function(event) {
|
||||
var outgoing = this.$(".outgoing").get(0);
|
||||
|
||||
this.publisher = this.sdk.initPublisher(outgoing, this.videoStyles);
|
||||
|
||||
// Suppress OT GuM custom dialog, see bug 1018875
|
||||
function preventOpeningAccessDialog(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this.publisher.on("accessDialogOpened", preventOpeningAccessDialog);
|
||||
this.publisher.on("accessDenied", preventOpeningAccessDialog);
|
||||
|
||||
this.model.session.publish(this.publisher);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unpublishes local stream.
|
||||
*/
|
||||
unpublish: function() {
|
||||
// Unregister access OT GuM custom dialog listeners, see bug 1018875
|
||||
this.publisher.off("accessDialogOpened");
|
||||
this.publisher.off("accessDenied");
|
||||
|
||||
this.model.session.unpublish(this.publisher);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders this view.
|
||||
*
|
||||
* @return {ConversationView}
|
||||
*/
|
||||
render: function() {
|
||||
this.$el.html(this.template(this.model.toJSON()));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification view.
|
||||
*/
|
||||
var NotificationView = BaseView.extend({
|
||||
template: _.template([
|
||||
'<div class="alert alert-<%- level %>">',
|
||||
' <button class="close"></button>',
|
||||
' <p class="message"><%- message %></p>',
|
||||
'</div>'
|
||||
].join("")),
|
||||
|
||||
events: {
|
||||
"click .close": "dismiss"
|
||||
},
|
||||
|
||||
dismiss: function(event) {
|
||||
event.preventDefault();
|
||||
this.$el.addClass("fade-out");
|
||||
setTimeout(function() {
|
||||
this.collection.remove(this.model);
|
||||
this.remove();
|
||||
}.bind(this), 500); // XXX make timeout value configurable
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.template(this.model.toJSON()));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification list view.
|
||||
*/
|
||||
var NotificationListView = Backbone.View.extend({
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Available options:
|
||||
* - {loop.shared.models.NotificationCollection} collection Notifications
|
||||
* collection
|
||||
*
|
||||
* @param {Object} options Options object
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
if (!options.collection) {
|
||||
this.collection = new sharedModels.NotificationCollection();
|
||||
}
|
||||
this.listenTo(this.collection, "reset add remove", this.render);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the notification stack.
|
||||
*/
|
||||
clear: function() {
|
||||
this.collection.reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new notification to the stack, triggering rendering of it.
|
||||
*
|
||||
* @param {Object|NotificationModel} notification Notification data.
|
||||
*/
|
||||
notify: function(notification) {
|
||||
this.collection.add(notification);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new notification to the stack using an l10n message identifier,
|
||||
* triggering rendering of it.
|
||||
*
|
||||
* @param {String} messageId L10n message id
|
||||
* @param {String} level Notification level
|
||||
*/
|
||||
notifyL10n: function(messageId, level) {
|
||||
this.notify({
|
||||
message: l10n.get(messageId),
|
||||
level: level
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a warning notification to the stack and renders it.
|
||||
*
|
||||
* @return {String} message
|
||||
*/
|
||||
warn: function(message) {
|
||||
this.notify({level: "warning", message: message});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a l10n warning notification to the stack and renders it.
|
||||
*
|
||||
* @param {String} messageId L10n message id
|
||||
*/
|
||||
warnL10n: function(messageId) {
|
||||
this.warn(l10n.get(messageId));
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an error notification to the stack and renders it.
|
||||
*
|
||||
* @return {String} message
|
||||
*/
|
||||
error: function(message) {
|
||||
this.notify({level: "error", message: message});
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a l10n rror notification to the stack and renders it.
|
||||
*
|
||||
* @param {String} messageId L10n message id
|
||||
*/
|
||||
errorL10n: function(messageId) {
|
||||
this.error(l10n.get(messageId));
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders this view.
|
||||
*
|
||||
* @return {loop.shared.views.NotificationListView}
|
||||
*/
|
||||
render: function() {
|
||||
this.$el.html(this.collection.map(function(notification) {
|
||||
return new NotificationView({
|
||||
model: notification,
|
||||
collection: this.collection
|
||||
}).render().$el;
|
||||
}.bind(this)));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Unsupported Browsers view.
|
||||
*/
|
||||
var UnsupportedBrowserView = BaseView.extend({
|
||||
template: _.template([
|
||||
'<div>',
|
||||
' <h2 data-l10n-id="incompatible_browser"></h2>',
|
||||
' <p data-l10n-id="powered_by_webrtc"></p>',
|
||||
' <p data-l10n-id="use_latest_firefox" ',
|
||||
' data-l10n-args=\'{"ff_url": "https://www.mozilla.org/firefox/"}\'>',
|
||||
' </p>',
|
||||
'</div>'
|
||||
].join(""))
|
||||
});
|
||||
|
||||
/**
|
||||
* Unsupported Browsers view.
|
||||
*/
|
||||
var UnsupportedDeviceView = BaseView.extend({
|
||||
template: _.template([
|
||||
'<div>',
|
||||
' <h2 data-l10n-id="incompatible_device"></h2>',
|
||||
' <p data-l10n-id="sorry_device_unsupported"></p>',
|
||||
' <p data-l10n-id="use_firefox_windows_mac_linux"></p>',
|
||||
'</div>'
|
||||
].join(""))
|
||||
});
|
||||
|
||||
return {
|
||||
L10nView: L10nView,
|
||||
BaseView: BaseView,
|
||||
ConversationView: ConversationView,
|
||||
NotificationListView: NotificationListView,
|
||||
NotificationView: NotificationView,
|
||||
UnsupportedBrowserView: UnsupportedBrowserView,
|
||||
UnsupportedDeviceView: UnsupportedDeviceView
|
||||
};
|
||||
})(_, window.OT, document.webL10n || document.mozL10n);
|
@ -0,0 +1,556 @@
|
||||
/*
|
||||
HTTP Hawk Authentication Scheme
|
||||
Copyright (c) 2012-2014, Eran Hammer <eran@hammer.io>
|
||||
BSD Licensed
|
||||
*/
|
||||
|
||||
|
||||
// Declare namespace
|
||||
|
||||
var hawk = {
|
||||
internals: {}
|
||||
};
|
||||
|
||||
|
||||
hawk.client = {
|
||||
|
||||
// Generate an Authorization header for a given request
|
||||
|
||||
/*
|
||||
uri: 'http://example.com/resource?a=b' or object generated by hawk.utils.parseUri()
|
||||
method: HTTP verb (e.g. 'GET', 'POST')
|
||||
options: {
|
||||
|
||||
// Required
|
||||
|
||||
credentials: {
|
||||
id: 'dh37fgj492je',
|
||||
key: 'aoijedoaijsdlaksjdl',
|
||||
algorithm: 'sha256' // 'sha1', 'sha256'
|
||||
},
|
||||
|
||||
// Optional
|
||||
|
||||
ext: 'application-specific', // Application specific data sent via the ext attribute
|
||||
timestamp: Date.now() / 1000, // A pre-calculated timestamp in seconds
|
||||
nonce: '2334f34f', // A pre-generated nonce
|
||||
localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided)
|
||||
payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
|
||||
contentType: 'application/json', // Payload content-type (ignored if hash provided)
|
||||
hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash
|
||||
app: '24s23423f34dx', // Oz application id
|
||||
dlg: '234sz34tww3sd' // Oz delegated-by application id
|
||||
}
|
||||
*/
|
||||
|
||||
header: function (uri, method, options) {
|
||||
|
||||
var result = {
|
||||
field: '',
|
||||
artifacts: {}
|
||||
};
|
||||
|
||||
// Validate inputs
|
||||
|
||||
if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') ||
|
||||
!method || typeof method !== 'string' ||
|
||||
!options || typeof options !== 'object') {
|
||||
|
||||
result.err = 'Invalid argument type';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Application time
|
||||
|
||||
var timestamp = options.timestamp || hawk.utils.now(options.localtimeOffsetMsec);
|
||||
|
||||
// Validate credentials
|
||||
|
||||
var credentials = options.credentials;
|
||||
if (!credentials ||
|
||||
!credentials.id ||
|
||||
!credentials.key ||
|
||||
!credentials.algorithm) {
|
||||
|
||||
result.err = 'Invalid credentials object';
|
||||
return result;
|
||||
}
|
||||
|
||||
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
|
||||
result.err = 'Unknown algorithm';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Parse URI
|
||||
|
||||
if (typeof uri === 'string') {
|
||||
uri = hawk.utils.parseUri(uri);
|
||||
}
|
||||
|
||||
// Calculate signature
|
||||
|
||||
var artifacts = {
|
||||
ts: timestamp,
|
||||
nonce: options.nonce || hawk.utils.randomString(6),
|
||||
method: method,
|
||||
resource: uri.relative,
|
||||
host: uri.hostname,
|
||||
port: uri.port,
|
||||
hash: options.hash,
|
||||
ext: options.ext,
|
||||
app: options.app,
|
||||
dlg: options.dlg
|
||||
};
|
||||
|
||||
result.artifacts = artifacts;
|
||||
|
||||
// Calculate payload hash
|
||||
|
||||
if (!artifacts.hash &&
|
||||
(options.payload || options.payload === '')) {
|
||||
|
||||
artifacts.hash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
|
||||
}
|
||||
|
||||
var mac = hawk.crypto.calculateMac('header', credentials, artifacts);
|
||||
|
||||
// Construct header
|
||||
|
||||
var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed
|
||||
var header = 'Hawk id="' + credentials.id +
|
||||
'", ts="' + artifacts.ts +
|
||||
'", nonce="' + artifacts.nonce +
|
||||
(artifacts.hash ? '", hash="' + artifacts.hash : '') +
|
||||
(hasExt ? '", ext="' + hawk.utils.escapeHeaderAttribute(artifacts.ext) : '') +
|
||||
'", mac="' + mac + '"';
|
||||
|
||||
if (artifacts.app) {
|
||||
header += ', app="' + artifacts.app +
|
||||
(artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"';
|
||||
}
|
||||
|
||||
result.field = header;
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
|
||||
// Validate server response
|
||||
|
||||
/*
|
||||
request: object created via 'new XMLHttpRequest()' after response received
|
||||
artifacts: object received from header().artifacts
|
||||
options: {
|
||||
payload: optional payload received
|
||||
required: specifies if a Server-Authorization header is required. Defaults to 'false'
|
||||
}
|
||||
*/
|
||||
|
||||
authenticate: function (request, credentials, artifacts, options) {
|
||||
|
||||
options = options || {};
|
||||
|
||||
var getHeader = function (name) {
|
||||
|
||||
return request.getResponseHeader ? request.getResponseHeader(name) : request.getHeader(name);
|
||||
};
|
||||
|
||||
var wwwAuthenticate = getHeader('www-authenticate');
|
||||
if (wwwAuthenticate) {
|
||||
|
||||
// Parse HTTP WWW-Authenticate header
|
||||
|
||||
var attributes = hawk.utils.parseAuthorizationHeader(wwwAuthenticate, ['ts', 'tsm', 'error']);
|
||||
if (!attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attributes.ts) {
|
||||
var tsm = hawk.crypto.calculateTsMac(attributes.ts, credentials);
|
||||
if (tsm !== attributes.tsm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hawk.utils.setNtpOffset(attributes.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
|
||||
}
|
||||
}
|
||||
|
||||
// Parse HTTP Server-Authorization header
|
||||
|
||||
var serverAuthorization = getHeader('server-authorization');
|
||||
if (!serverAuthorization &&
|
||||
!options.required) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var attributes = hawk.utils.parseAuthorizationHeader(serverAuthorization, ['mac', 'ext', 'hash']);
|
||||
if (!attributes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var modArtifacts = {
|
||||
ts: artifacts.ts,
|
||||
nonce: artifacts.nonce,
|
||||
method: artifacts.method,
|
||||
resource: artifacts.resource,
|
||||
host: artifacts.host,
|
||||
port: artifacts.port,
|
||||
hash: attributes.hash,
|
||||
ext: attributes.ext,
|
||||
app: artifacts.app,
|
||||
dlg: artifacts.dlg
|
||||
};
|
||||
|
||||
var mac = hawk.crypto.calculateMac('response', credentials, modArtifacts);
|
||||
if (mac !== attributes.mac) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!options.payload &&
|
||||
options.payload !== '') {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!attributes.hash) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var calculatedHash = hawk.crypto.calculatePayloadHash(options.payload, credentials.algorithm, getHeader('content-type'));
|
||||
return (calculatedHash === attributes.hash);
|
||||
},
|
||||
|
||||
message: function (host, port, message, options) {
|
||||
|
||||
// Validate inputs
|
||||
|
||||
if (!host || typeof host !== 'string' ||
|
||||
!port || typeof port !== 'number' ||
|
||||
message === null || message === undefined || typeof message !== 'string' ||
|
||||
!options || typeof options !== 'object') {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Application time
|
||||
|
||||
var timestamp = options.timestamp || hawk.utils.now(options.localtimeOffsetMsec);
|
||||
|
||||
// Validate credentials
|
||||
|
||||
var credentials = options.credentials;
|
||||
if (!credentials ||
|
||||
!credentials.id ||
|
||||
!credentials.key ||
|
||||
!credentials.algorithm) {
|
||||
|
||||
// Invalid credential object
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hawk.crypto.algorithms.indexOf(credentials.algorithm) === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate signature
|
||||
|
||||
var artifacts = {
|
||||
ts: timestamp,
|
||||
nonce: options.nonce || hawk.utils.randomString(6),
|
||||
host: host,
|
||||
port: port,
|
||||
hash: hawk.crypto.calculatePayloadHash(message, credentials.algorithm)
|
||||
};
|
||||
|
||||
// Construct authorization
|
||||
|
||||
var result = {
|
||||
id: credentials.id,
|
||||
ts: artifacts.ts,
|
||||
nonce: artifacts.nonce,
|
||||
hash: artifacts.hash,
|
||||
mac: hawk.crypto.calculateMac('message', credentials, artifacts)
|
||||
};
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
authenticateTimestamp: function (message, credentials, updateClock) { // updateClock defaults to true
|
||||
|
||||
var tsm = hawk.crypto.calculateTsMac(message.ts, credentials);
|
||||
if (tsm !== message.tsm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (updateClock !== false) {
|
||||
hawk.utils.setNtpOffset(message.ts - Math.floor((new Date()).getTime() / 1000)); // Keep offset at 1 second precision
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
hawk.crypto = {
|
||||
|
||||
headerVersion: '1',
|
||||
|
||||
algorithms: ['sha1', 'sha256'],
|
||||
|
||||
calculateMac: function (type, credentials, options) {
|
||||
|
||||
var normalized = hawk.crypto.generateNormalizedString(type, options);
|
||||
|
||||
var hmac = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()](normalized, credentials.key);
|
||||
return hmac.toString(CryptoJS.enc.Base64);
|
||||
},
|
||||
|
||||
generateNormalizedString: function (type, options) {
|
||||
|
||||
var normalized = 'hawk.' + hawk.crypto.headerVersion + '.' + type + '\n' +
|
||||
options.ts + '\n' +
|
||||
options.nonce + '\n' +
|
||||
(options.method || '').toUpperCase() + '\n' +
|
||||
(options.resource || '') + '\n' +
|
||||
options.host.toLowerCase() + '\n' +
|
||||
options.port + '\n' +
|
||||
(options.hash || '') + '\n';
|
||||
|
||||
if (options.ext) {
|
||||
normalized += options.ext.replace('\\', '\\\\').replace('\n', '\\n');
|
||||
}
|
||||
|
||||
normalized += '\n';
|
||||
|
||||
if (options.app) {
|
||||
normalized += options.app + '\n' +
|
||||
(options.dlg || '') + '\n';
|
||||
}
|
||||
|
||||
return normalized;
|
||||
},
|
||||
|
||||
calculatePayloadHash: function (payload, algorithm, contentType) {
|
||||
|
||||
var hash = CryptoJS.algo[algorithm.toUpperCase()].create();
|
||||
hash.update('hawk.' + hawk.crypto.headerVersion + '.payload\n');
|
||||
hash.update(hawk.utils.parseContentType(contentType) + '\n');
|
||||
hash.update(payload);
|
||||
hash.update('\n');
|
||||
return hash.finalize().toString(CryptoJS.enc.Base64);
|
||||
},
|
||||
|
||||
calculateTsMac: function (ts, credentials) {
|
||||
|
||||
var hash = CryptoJS['Hmac' + credentials.algorithm.toUpperCase()]('hawk.' + hawk.crypto.headerVersion + '.ts\n' + ts + '\n', credentials.key);
|
||||
return hash.toString(CryptoJS.enc.Base64);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// localStorage compatible interface
|
||||
|
||||
hawk.internals.LocalStorage = function () {
|
||||
|
||||
this._cache = {};
|
||||
this.length = 0;
|
||||
|
||||
this.getItem = function (key) {
|
||||
|
||||
return this._cache.hasOwnProperty(key) ? String(this._cache[key]) : null;
|
||||
};
|
||||
|
||||
this.setItem = function (key, value) {
|
||||
|
||||
this._cache[key] = String(value);
|
||||
this.length = Object.keys(this._cache).length;
|
||||
};
|
||||
|
||||
this.removeItem = function (key) {
|
||||
|
||||
delete this._cache[key];
|
||||
this.length = Object.keys(this._cache).length;
|
||||
};
|
||||
|
||||
this.clear = function () {
|
||||
|
||||
this._cache = {};
|
||||
this.length = 0;
|
||||
};
|
||||
|
||||
this.key = function (i) {
|
||||
|
||||
return Object.keys(this._cache)[i || 0];
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
hawk.utils = {
|
||||
|
||||
storage: new hawk.internals.LocalStorage(),
|
||||
|
||||
setStorage: function (storage) {
|
||||
|
||||
var ntpOffset = hawk.utils.storage.getItem('hawk_ntp_offset');
|
||||
hawk.utils.storage = storage;
|
||||
if (ntpOffset) {
|
||||
hawk.utils.setNtpOffset(ntpOffset);
|
||||
}
|
||||
},
|
||||
|
||||
setNtpOffset: function (offset) {
|
||||
|
||||
try {
|
||||
hawk.utils.storage.setItem('hawk_ntp_offset', offset);
|
||||
}
|
||||
catch (err) {
|
||||
console.error('[hawk] could not write to storage.');
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
|
||||
getNtpOffset: function () {
|
||||
|
||||
var offset = hawk.utils.storage.getItem('hawk_ntp_offset');
|
||||
if (!offset) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return parseInt(offset, 10);
|
||||
},
|
||||
|
||||
now: function (localtimeOffsetMsec) {
|
||||
|
||||
return Math.floor(((new Date()).getTime() + (localtimeOffsetMsec || 0)) / 1000) + hawk.utils.getNtpOffset();
|
||||
},
|
||||
|
||||
escapeHeaderAttribute: function (attribute) {
|
||||
|
||||
return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"');
|
||||
},
|
||||
|
||||
parseContentType: function (header) {
|
||||
|
||||
if (!header) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return header.split(';')[0].replace(/^\s+|\s+$/g, '').toLowerCase();
|
||||
},
|
||||
|
||||
parseAuthorizationHeader: function (header, keys) {
|
||||
|
||||
if (!header) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var headerParts = header.match(/^(\w+)(?:\s+(.*))?$/); // Header: scheme[ something]
|
||||
if (!headerParts) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var scheme = headerParts[1];
|
||||
if (scheme.toLowerCase() !== 'hawk') {
|
||||
return null;
|
||||
}
|
||||
|
||||
var attributesString = headerParts[2];
|
||||
if (!attributesString) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var attributes = {};
|
||||
var verify = attributesString.replace(/(\w+)="([^"\\]*)"\s*(?:,\s*|$)/g, function ($0, $1, $2) {
|
||||
|
||||
// Check valid attribute names
|
||||
|
||||
if (keys.indexOf($1) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allowed attribute value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9
|
||||
|
||||
if ($2.match(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~]+$/) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
|
||||
if (attributes.hasOwnProperty($1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
attributes[$1] = $2;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (verify !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attributes;
|
||||
},
|
||||
|
||||
randomString: function (size) {
|
||||
|
||||
var randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
var len = randomSource.length;
|
||||
|
||||
var result = [];
|
||||
for (var i = 0; i < size; ++i) {
|
||||
result[i] = randomSource[Math.floor(Math.random() * len)];
|
||||
}
|
||||
|
||||
return result.join('');
|
||||
},
|
||||
|
||||
parseUri: function (input) {
|
||||
|
||||
// Based on: parseURI 1.2.2
|
||||
// http://blog.stevenlevithan.com/archives/parseuri
|
||||
// (c) Steven Levithan <stevenlevithan.com>
|
||||
// MIT License
|
||||
|
||||
var keys = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'hostname', 'port', 'resource', 'relative', 'pathname', 'directory', 'file', 'query', 'fragment'];
|
||||
|
||||
var uriRegex = /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?)(?:#(.*))?)/;
|
||||
var uriByNumber = input.match(uriRegex);
|
||||
var uri = {};
|
||||
|
||||
for (var i = 0, il = keys.length; i < il; ++i) {
|
||||
uri[keys[i]] = uriByNumber[i] || '';
|
||||
}
|
||||
|
||||
if (uri.port === '') {
|
||||
uri.port = (uri.protocol.toLowerCase() === 'http' ? '80' : (uri.protocol.toLowerCase() === 'https' ? '443' : ''));
|
||||
}
|
||||
|
||||
return uri;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// $lab:coverage:off$
|
||||
|
||||
// Based on: Crypto-JS v3.1.2
|
||||
// Copyright (c) 2009-2013, Jeff Mott. All rights reserved.
|
||||
// http://code.google.com/p/crypto-js/
|
||||
// http://code.google.com/p/crypto-js/wiki/License
|
||||
|
||||
var CryptoJS = CryptoJS || function (h, r) { var k = {}, l = k.lib = {}, n = function () { }, f = l.Base = { extend: function (a) { n.prototype = this; var b = new n; a && b.mixIn(a); b.hasOwnProperty("init") || (b.init = function () { b.$super.init.apply(this, arguments) }); b.init.prototype = b; b.$super = this; return b }, create: function () { var a = this.extend(); a.init.apply(a, arguments); return a }, init: function () { }, mixIn: function (a) { for (var b in a) a.hasOwnProperty(b) && (this[b] = a[b]); a.hasOwnProperty("toString") && (this.toString = a.toString) }, clone: function () { return this.init.prototype.extend(this) } }, j = l.WordArray = f.extend({ init: function (a, b) { a = this.words = a || []; this.sigBytes = b != r ? b : 4 * a.length }, toString: function (a) { return (a || s).stringify(this) }, concat: function (a) { var b = this.words, d = a.words, c = this.sigBytes; a = a.sigBytes; this.clamp(); if (c % 4) for (var e = 0; e < a; e++) b[c + e >>> 2] |= (d[e >>> 2] >>> 24 - 8 * (e % 4) & 255) << 24 - 8 * ((c + e) % 4); else if (65535 < d.length) for (e = 0; e < a; e += 4) b[c + e >>> 2] = d[e >>> 2]; else b.push.apply(b, d); this.sigBytes += a; return this }, clamp: function () { var a = this.words, b = this.sigBytes; a[b >>> 2] &= 4294967295 << 32 - 8 * (b % 4); a.length = h.ceil(b / 4) }, clone: function () { var a = f.clone.call(this); a.words = this.words.slice(0); return a }, random: function (a) { for (var b = [], d = 0; d < a; d += 4) b.push(4294967296 * h.random() | 0); return new j.init(b, a) } }), m = k.enc = {}, s = m.Hex = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) { var e = b[c >>> 2] >>> 24 - 8 * (c % 4) & 255; d.push((e >>> 4).toString(16)); d.push((e & 15).toString(16)) } return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c += 2) d[c >>> 3] |= parseInt(a.substr(c, 2), 16) << 24 - 4 * (c % 8); return new j.init(d, b / 2) } }, p = m.Latin1 = { stringify: function (a) { var b = a.words; a = a.sigBytes; for (var d = [], c = 0; c < a; c++) d.push(String.fromCharCode(b[c >>> 2] >>> 24 - 8 * (c % 4) & 255)); return d.join("") }, parse: function (a) { for (var b = a.length, d = [], c = 0; c < b; c++) d[c >>> 2] |= (a.charCodeAt(c) & 255) << 24 - 8 * (c % 4); return new j.init(d, b) } }, t = m.Utf8 = { stringify: function (a) { try { return decodeURIComponent(escape(p.stringify(a))) } catch (b) { throw Error("Malformed UTF-8 data"); } }, parse: function (a) { return p.parse(unescape(encodeURIComponent(a))) } }, q = l.BufferedBlockAlgorithm = f.extend({ reset: function () { this._data = new j.init; this._nDataBytes = 0 }, _append: function (a) { "string" == typeof a && (a = t.parse(a)); this._data.concat(a); this._nDataBytes += a.sigBytes }, _process: function (a) { var b = this._data, d = b.words, c = b.sigBytes, e = this.blockSize, f = c / (4 * e), f = a ? h.ceil(f) : h.max((f | 0) - this._minBufferSize, 0); a = f * e; c = h.min(4 * a, c); if (a) { for (var g = 0; g < a; g += e) this._doProcessBlock(d, g); g = d.splice(0, a); b.sigBytes -= c } return new j.init(g, c) }, clone: function () { var a = f.clone.call(this); a._data = this._data.clone(); return a }, _minBufferSize: 0 }); l.Hasher = q.extend({ cfg: f.extend(), init: function (a) { this.cfg = this.cfg.extend(a); this.reset() }, reset: function () { q.reset.call(this); this._doReset() }, update: function (a) { this._append(a); this._process(); return this }, finalize: function (a) { a && this._append(a); return this._doFinalize() }, blockSize: 16, _createHelper: function (a) { return function (b, d) { return (new a.init(d)).finalize(b) } }, _createHmacHelper: function (a) { return function (b, d) { return (new u.HMAC.init(a, d)).finalize(b) } } }); var u = k.algo = {}; return k }(Math);
|
||||
(function () { var k = CryptoJS, b = k.lib, m = b.WordArray, l = b.Hasher, d = [], b = k.algo.SHA1 = l.extend({ _doReset: function () { this._hash = new m.init([1732584193, 4023233417, 2562383102, 271733878, 3285377520]) }, _doProcessBlock: function (n, p) { for (var a = this._hash.words, e = a[0], f = a[1], h = a[2], j = a[3], b = a[4], c = 0; 80 > c; c++) { if (16 > c) d[c] = n[p + c] | 0; else { var g = d[c - 3] ^ d[c - 8] ^ d[c - 14] ^ d[c - 16]; d[c] = g << 1 | g >>> 31 } g = (e << 5 | e >>> 27) + b + d[c]; g = 20 > c ? g + ((f & h | ~f & j) + 1518500249) : 40 > c ? g + ((f ^ h ^ j) + 1859775393) : 60 > c ? g + ((f & h | f & j | h & j) - 1894007588) : g + ((f ^ h ^ j) - 899497514); b = j; j = h; h = f << 30 | f >>> 2; f = e; e = g } a[0] = a[0] + e | 0; a[1] = a[1] + f | 0; a[2] = a[2] + h | 0; a[3] = a[3] + j | 0; a[4] = a[4] + b | 0 }, _doFinalize: function () { var b = this._data, d = b.words, a = 8 * this._nDataBytes, e = 8 * b.sigBytes; d[e >>> 5] |= 128 << 24 - e % 32; d[(e + 64 >>> 9 << 4) + 14] = Math.floor(a / 4294967296); d[(e + 64 >>> 9 << 4) + 15] = a; b.sigBytes = 4 * d.length; this._process(); return this._hash }, clone: function () { var b = l.clone.call(this); b._hash = this._hash.clone(); return b } }); k.SHA1 = l._createHelper(b); k.HmacSHA1 = l._createHmacHelper(b) })();
|
||||
(function (k) { for (var g = CryptoJS, h = g.lib, v = h.WordArray, j = h.Hasher, h = g.algo, s = [], t = [], u = function (q) { return 4294967296 * (q - (q | 0)) | 0 }, l = 2, b = 0; 64 > b;) { var d; a: { d = l; for (var w = k.sqrt(d), r = 2; r <= w; r++) if (!(d % r)) { d = !1; break a } d = !0 } d && (8 > b && (s[b] = u(k.pow(l, 0.5))), t[b] = u(k.pow(l, 1 / 3)), b++); l++ } var n = [], h = h.SHA256 = j.extend({ _doReset: function () { this._hash = new v.init(s.slice(0)) }, _doProcessBlock: function (q, h) { for (var a = this._hash.words, c = a[0], d = a[1], b = a[2], k = a[3], f = a[4], g = a[5], j = a[6], l = a[7], e = 0; 64 > e; e++) { if (16 > e) n[e] = q[h + e] | 0; else { var m = n[e - 15], p = n[e - 2]; n[e] = ((m << 25 | m >>> 7) ^ (m << 14 | m >>> 18) ^ m >>> 3) + n[e - 7] + ((p << 15 | p >>> 17) ^ (p << 13 | p >>> 19) ^ p >>> 10) + n[e - 16] } m = l + ((f << 26 | f >>> 6) ^ (f << 21 | f >>> 11) ^ (f << 7 | f >>> 25)) + (f & g ^ ~f & j) + t[e] + n[e]; p = ((c << 30 | c >>> 2) ^ (c << 19 | c >>> 13) ^ (c << 10 | c >>> 22)) + (c & d ^ c & b ^ d & b); l = j; j = g; g = f; f = k + m | 0; k = b; b = d; d = c; c = m + p | 0 } a[0] = a[0] + c | 0; a[1] = a[1] + d | 0; a[2] = a[2] + b | 0; a[3] = a[3] + k | 0; a[4] = a[4] + f | 0; a[5] = a[5] + g | 0; a[6] = a[6] + j | 0; a[7] = a[7] + l | 0 }, _doFinalize: function () { var d = this._data, b = d.words, a = 8 * this._nDataBytes, c = 8 * d.sigBytes; b[c >>> 5] |= 128 << 24 - c % 32; b[(c + 64 >>> 9 << 4) + 14] = k.floor(a / 4294967296); b[(c + 64 >>> 9 << 4) + 15] = a; d.sigBytes = 4 * b.length; this._process(); return this._hash }, clone: function () { var b = j.clone.call(this); b._hash = this._hash.clone(); return b } }); g.SHA256 = j._createHelper(h); g.HmacSHA256 = j._createHmacHelper(h) })(Math);
|
||||
(function () { var c = CryptoJS, k = c.enc.Utf8; c.algo.HMAC = c.lib.Base.extend({ init: function (a, b) { a = this._hasher = new a.init; "string" == typeof b && (b = k.parse(b)); var c = a.blockSize, e = 4 * c; b.sigBytes > e && (b = a.finalize(b)); b.clamp(); for (var f = this._oKey = b.clone(), g = this._iKey = b.clone(), h = f.words, j = g.words, d = 0; d < c; d++) h[d] ^= 1549556828, j[d] ^= 909522486; f.sigBytes = g.sigBytes = e; this.reset() }, reset: function () { var a = this._hasher; a.reset(); a.update(this._iKey) }, update: function (a) { this._hasher.update(a); return this }, finalize: function (a) { var b = this._hasher; a = b.finalize(a); b.reset(); return b.finalize(this._oKey.clone().concat(a)) } }) })();
|
||||
(function () { var h = CryptoJS, j = h.lib.WordArray; h.enc.Base64 = { stringify: function (b) { var e = b.words, f = b.sigBytes, c = this._map; b.clamp(); b = []; for (var a = 0; a < f; a += 3) for (var d = (e[a >>> 2] >>> 24 - 8 * (a % 4) & 255) << 16 | (e[a + 1 >>> 2] >>> 24 - 8 * ((a + 1) % 4) & 255) << 8 | e[a + 2 >>> 2] >>> 24 - 8 * ((a + 2) % 4) & 255, g = 0; 4 > g && a + 0.75 * g < f; g++) b.push(c.charAt(d >>> 6 * (3 - g) & 63)); if (e = c.charAt(64)) for (; b.length % 4;) b.push(e); return b.join("") }, parse: function (b) { var e = b.length, f = this._map, c = f.charAt(64); c && (c = b.indexOf(c), -1 != c && (e = c)); for (var c = [], a = 0, d = 0; d < e; d++) if (d % 4) { var g = f.indexOf(b.charAt(d - 1)) << 2 * (d % 4), h = f.indexOf(b.charAt(d)) >>> 6 - 2 * (d % 4); c[a >>> 2] |= (g | h) << 24 - 8 * (a % 4); a++ } return j.create(c, a) }, _map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" } })();
|
||||
|
||||
hawk.crypto.internals = CryptoJS;
|
||||
|
||||
|
||||
// Export if used as a module
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = hawk;
|
||||
}
|
||||
|
||||
// $lab:coverage:on$
|
4
browser/components/loop/content/shared/libs/jquery-2.1.0.js
vendored
Normal file
56
browser/components/loop/content/shared/libs/lodash-2.4.1.js
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Lo-Dash 2.4.1 (Custom Build) lodash.com/license | Underscore.js 1.5.2 underscorejs.org/LICENSE
|
||||
* Build: `lodash modern -o ./dist/lodash.js`
|
||||
*/
|
||||
;(function(){function n(n,t,e){e=(e||0)-1;for(var r=n?n.length:0;++e<r;)if(n[e]===t)return e;return-1}function t(t,e){var r=typeof e;if(t=t.l,"boolean"==r||null==e)return t[e]?0:-1;"number"!=r&&"string"!=r&&(r="object");var u="number"==r?e:m+e;return t=(t=t[r])&&t[u],"object"==r?t&&-1<n(t,e)?0:-1:t?0:-1}function e(n){var t=this.l,e=typeof n;if("boolean"==e||null==n)t[n]=true;else{"number"!=e&&"string"!=e&&(e="object");var r="number"==e?n:m+n,t=t[e]||(t[e]={});"object"==e?(t[r]||(t[r]=[])).push(n):t[r]=true
|
||||
}}function r(n){return n.charCodeAt(0)}function u(n,t){for(var e=n.m,r=t.m,u=-1,o=e.length;++u<o;){var i=e[u],a=r[u];if(i!==a){if(i>a||typeof i=="undefined")return 1;if(i<a||typeof a=="undefined")return-1}}return n.n-t.n}function o(n){var t=-1,r=n.length,u=n[0],o=n[r/2|0],i=n[r-1];if(u&&typeof u=="object"&&o&&typeof o=="object"&&i&&typeof i=="object")return false;for(u=f(),u["false"]=u["null"]=u["true"]=u.undefined=false,o=f(),o.k=n,o.l=u,o.push=e;++t<r;)o.push(n[t]);return o}function i(n){return"\\"+U[n]
|
||||
}function a(){return h.pop()||[]}function f(){return g.pop()||{k:null,l:null,m:null,"false":false,n:0,"null":false,number:null,object:null,push:null,string:null,"true":false,undefined:false,o:null}}function l(n){n.length=0,h.length<_&&h.push(n)}function c(n){var t=n.l;t&&c(t),n.k=n.l=n.m=n.object=n.number=n.string=n.o=null,g.length<_&&g.push(n)}function p(n,t,e){t||(t=0),typeof e=="undefined"&&(e=n?n.length:0);var r=-1;e=e-t||0;for(var u=Array(0>e?0:e);++r<e;)u[r]=n[t+r];return u}function s(e){function h(n,t,e){if(!n||!V[typeof n])return n;
|
||||
t=t&&typeof e=="undefined"?t:tt(t,e,3);for(var r=-1,u=V[typeof n]&&Fe(n),o=u?u.length:0;++r<o&&(e=u[r],false!==t(n[e],e,n)););return n}function g(n,t,e){var r;if(!n||!V[typeof n])return n;t=t&&typeof e=="undefined"?t:tt(t,e,3);for(r in n)if(false===t(n[r],r,n))break;return n}function _(n,t,e){var r,u=n,o=u;if(!u)return o;for(var i=arguments,a=0,f=typeof e=="number"?2:i.length;++a<f;)if((u=i[a])&&V[typeof u])for(var l=-1,c=V[typeof u]&&Fe(u),p=c?c.length:0;++l<p;)r=c[l],"undefined"==typeof o[r]&&(o[r]=u[r]);
|
||||
return o}function U(n,t,e){var r,u=n,o=u;if(!u)return o;var i=arguments,a=0,f=typeof e=="number"?2:i.length;if(3<f&&"function"==typeof i[f-2])var l=tt(i[--f-1],i[f--],2);else 2<f&&"function"==typeof i[f-1]&&(l=i[--f]);for(;++a<f;)if((u=i[a])&&V[typeof u])for(var c=-1,p=V[typeof u]&&Fe(u),s=p?p.length:0;++c<s;)r=p[c],o[r]=l?l(o[r],u[r]):u[r];return o}function H(n){var t,e=[];if(!n||!V[typeof n])return e;for(t in n)me.call(n,t)&&e.push(t);return e}function J(n){return n&&typeof n=="object"&&!Te(n)&&me.call(n,"__wrapped__")?n:new Q(n)
|
||||
}function Q(n,t){this.__chain__=!!t,this.__wrapped__=n}function X(n){function t(){if(r){var n=p(r);be.apply(n,arguments)}if(this instanceof t){var o=nt(e.prototype),n=e.apply(o,n||arguments);return wt(n)?n:o}return e.apply(u,n||arguments)}var e=n[0],r=n[2],u=n[4];return $e(t,n),t}function Z(n,t,e,r,u){if(e){var o=e(n);if(typeof o!="undefined")return o}if(!wt(n))return n;var i=ce.call(n);if(!K[i])return n;var f=Ae[i];switch(i){case T:case F:return new f(+n);case W:case P:return new f(n);case z:return o=f(n.source,C.exec(n)),o.lastIndex=n.lastIndex,o
|
||||
}if(i=Te(n),t){var c=!r;r||(r=a()),u||(u=a());for(var s=r.length;s--;)if(r[s]==n)return u[s];o=i?f(n.length):{}}else o=i?p(n):U({},n);return i&&(me.call(n,"index")&&(o.index=n.index),me.call(n,"input")&&(o.input=n.input)),t?(r.push(n),u.push(o),(i?St:h)(n,function(n,i){o[i]=Z(n,t,e,r,u)}),c&&(l(r),l(u)),o):o}function nt(n){return wt(n)?ke(n):{}}function tt(n,t,e){if(typeof n!="function")return Ut;if(typeof t=="undefined"||!("prototype"in n))return n;var r=n.__bindData__;if(typeof r=="undefined"&&(De.funcNames&&(r=!n.name),r=r||!De.funcDecomp,!r)){var u=ge.call(n);
|
||||
De.funcNames||(r=!O.test(u)),r||(r=E.test(u),$e(n,r))}if(false===r||true!==r&&1&r[1])return n;switch(e){case 1:return function(e){return n.call(t,e)};case 2:return function(e,r){return n.call(t,e,r)};case 3:return function(e,r,u){return n.call(t,e,r,u)};case 4:return function(e,r,u,o){return n.call(t,e,r,u,o)}}return Mt(n,t)}function et(n){function t(){var n=f?i:this;if(u){var h=p(u);be.apply(h,arguments)}return(o||c)&&(h||(h=p(arguments)),o&&be.apply(h,o),c&&h.length<a)?(r|=16,et([e,s?r:-4&r,h,null,i,a])):(h||(h=arguments),l&&(e=n[v]),this instanceof t?(n=nt(e.prototype),h=e.apply(n,h),wt(h)?h:n):e.apply(n,h))
|
||||
}var e=n[0],r=n[1],u=n[2],o=n[3],i=n[4],a=n[5],f=1&r,l=2&r,c=4&r,s=8&r,v=e;return $e(t,n),t}function rt(e,r){var u=-1,i=st(),a=e?e.length:0,f=a>=b&&i===n,l=[];if(f){var p=o(r);p?(i=t,r=p):f=false}for(;++u<a;)p=e[u],0>i(r,p)&&l.push(p);return f&&c(r),l}function ut(n,t,e,r){r=(r||0)-1;for(var u=n?n.length:0,o=[];++r<u;){var i=n[r];if(i&&typeof i=="object"&&typeof i.length=="number"&&(Te(i)||yt(i))){t||(i=ut(i,t,e));var a=-1,f=i.length,l=o.length;for(o.length+=f;++a<f;)o[l++]=i[a]}else e||o.push(i)}return o
|
||||
}function ot(n,t,e,r,u,o){if(e){var i=e(n,t);if(typeof i!="undefined")return!!i}if(n===t)return 0!==n||1/n==1/t;if(n===n&&!(n&&V[typeof n]||t&&V[typeof t]))return false;if(null==n||null==t)return n===t;var f=ce.call(n),c=ce.call(t);if(f==D&&(f=q),c==D&&(c=q),f!=c)return false;switch(f){case T:case F:return+n==+t;case W:return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case z:case P:return n==oe(t)}if(c=f==$,!c){var p=me.call(n,"__wrapped__"),s=me.call(t,"__wrapped__");if(p||s)return ot(p?n.__wrapped__:n,s?t.__wrapped__:t,e,r,u,o);
|
||||
if(f!=q)return false;if(f=n.constructor,p=t.constructor,f!=p&&!(dt(f)&&f instanceof f&&dt(p)&&p instanceof p)&&"constructor"in n&&"constructor"in t)return false}for(f=!u,u||(u=a()),o||(o=a()),p=u.length;p--;)if(u[p]==n)return o[p]==t;var v=0,i=true;if(u.push(n),o.push(t),c){if(p=n.length,v=t.length,(i=v==p)||r)for(;v--;)if(c=p,s=t[v],r)for(;c--&&!(i=ot(n[c],s,e,r,u,o)););else if(!(i=ot(n[v],s,e,r,u,o)))break}else g(t,function(t,a,f){return me.call(f,a)?(v++,i=me.call(n,a)&&ot(n[a],t,e,r,u,o)):void 0}),i&&!r&&g(n,function(n,t,e){return me.call(e,t)?i=-1<--v:void 0
|
||||
});return u.pop(),o.pop(),f&&(l(u),l(o)),i}function it(n,t,e,r,u){(Te(t)?St:h)(t,function(t,o){var i,a,f=t,l=n[o];if(t&&((a=Te(t))||Pe(t))){for(f=r.length;f--;)if(i=r[f]==t){l=u[f];break}if(!i){var c;e&&(f=e(l,t),c=typeof f!="undefined")&&(l=f),c||(l=a?Te(l)?l:[]:Pe(l)?l:{}),r.push(t),u.push(l),c||it(l,t,e,r,u)}}else e&&(f=e(l,t),typeof f=="undefined"&&(f=t)),typeof f!="undefined"&&(l=f);n[o]=l})}function at(n,t){return n+he(Re()*(t-n+1))}function ft(e,r,u){var i=-1,f=st(),p=e?e.length:0,s=[],v=!r&&p>=b&&f===n,h=u||v?a():s;
|
||||
for(v&&(h=o(h),f=t);++i<p;){var g=e[i],y=u?u(g,i,e):g;(r?!i||h[h.length-1]!==y:0>f(h,y))&&((u||v)&&h.push(y),s.push(g))}return v?(l(h.k),c(h)):u&&l(h),s}function lt(n){return function(t,e,r){var u={};e=J.createCallback(e,r,3),r=-1;var o=t?t.length:0;if(typeof o=="number")for(;++r<o;){var i=t[r];n(u,i,e(i,r,t),t)}else h(t,function(t,r,o){n(u,t,e(t,r,o),o)});return u}}function ct(n,t,e,r,u,o){var i=1&t,a=4&t,f=16&t,l=32&t;if(!(2&t||dt(n)))throw new ie;f&&!e.length&&(t&=-17,f=e=false),l&&!r.length&&(t&=-33,l=r=false);
|
||||
var c=n&&n.__bindData__;return c&&true!==c?(c=p(c),c[2]&&(c[2]=p(c[2])),c[3]&&(c[3]=p(c[3])),!i||1&c[1]||(c[4]=u),!i&&1&c[1]&&(t|=8),!a||4&c[1]||(c[5]=o),f&&be.apply(c[2]||(c[2]=[]),e),l&&we.apply(c[3]||(c[3]=[]),r),c[1]|=t,ct.apply(null,c)):(1==t||17===t?X:et)([n,t,e,r,u,o])}function pt(n){return Be[n]}function st(){var t=(t=J.indexOf)===Wt?n:t;return t}function vt(n){return typeof n=="function"&&pe.test(n)}function ht(n){var t,e;return n&&ce.call(n)==q&&(t=n.constructor,!dt(t)||t instanceof t)?(g(n,function(n,t){e=t
|
||||
}),typeof e=="undefined"||me.call(n,e)):false}function gt(n){return We[n]}function yt(n){return n&&typeof n=="object"&&typeof n.length=="number"&&ce.call(n)==D||false}function mt(n,t,e){var r=Fe(n),u=r.length;for(t=tt(t,e,3);u--&&(e=r[u],false!==t(n[e],e,n)););return n}function bt(n){var t=[];return g(n,function(n,e){dt(n)&&t.push(e)}),t.sort()}function _t(n){for(var t=-1,e=Fe(n),r=e.length,u={};++t<r;){var o=e[t];u[n[o]]=o}return u}function dt(n){return typeof n=="function"}function wt(n){return!(!n||!V[typeof n])
|
||||
}function jt(n){return typeof n=="number"||n&&typeof n=="object"&&ce.call(n)==W||false}function kt(n){return typeof n=="string"||n&&typeof n=="object"&&ce.call(n)==P||false}function xt(n){for(var t=-1,e=Fe(n),r=e.length,u=Xt(r);++t<r;)u[t]=n[e[t]];return u}function Ct(n,t,e){var r=-1,u=st(),o=n?n.length:0,i=false;return e=(0>e?Ie(0,o+e):e)||0,Te(n)?i=-1<u(n,t,e):typeof o=="number"?i=-1<(kt(n)?n.indexOf(t,e):u(n,t,e)):h(n,function(n){return++r<e?void 0:!(i=n===t)}),i}function Ot(n,t,e){var r=true;t=J.createCallback(t,e,3),e=-1;
|
||||
var u=n?n.length:0;if(typeof u=="number")for(;++e<u&&(r=!!t(n[e],e,n)););else h(n,function(n,e,u){return r=!!t(n,e,u)});return r}function Nt(n,t,e){var r=[];t=J.createCallback(t,e,3),e=-1;var u=n?n.length:0;if(typeof u=="number")for(;++e<u;){var o=n[e];t(o,e,n)&&r.push(o)}else h(n,function(n,e,u){t(n,e,u)&&r.push(n)});return r}function It(n,t,e){t=J.createCallback(t,e,3),e=-1;var r=n?n.length:0;if(typeof r!="number"){var u;return h(n,function(n,e,r){return t(n,e,r)?(u=n,false):void 0}),u}for(;++e<r;){var o=n[e];
|
||||
if(t(o,e,n))return o}}function St(n,t,e){var r=-1,u=n?n.length:0;if(t=t&&typeof e=="undefined"?t:tt(t,e,3),typeof u=="number")for(;++r<u&&false!==t(n[r],r,n););else h(n,t);return n}function Et(n,t,e){var r=n?n.length:0;if(t=t&&typeof e=="undefined"?t:tt(t,e,3),typeof r=="number")for(;r--&&false!==t(n[r],r,n););else{var u=Fe(n),r=u.length;h(n,function(n,e,o){return e=u?u[--r]:--r,t(o[e],e,o)})}return n}function Rt(n,t,e){var r=-1,u=n?n.length:0;if(t=J.createCallback(t,e,3),typeof u=="number")for(var o=Xt(u);++r<u;)o[r]=t(n[r],r,n);
|
||||
else o=[],h(n,function(n,e,u){o[++r]=t(n,e,u)});return o}function At(n,t,e){var u=-1/0,o=u;if(typeof t!="function"&&e&&e[t]===n&&(t=null),null==t&&Te(n)){e=-1;for(var i=n.length;++e<i;){var a=n[e];a>o&&(o=a)}}else t=null==t&&kt(n)?r:J.createCallback(t,e,3),St(n,function(n,e,r){e=t(n,e,r),e>u&&(u=e,o=n)});return o}function Dt(n,t,e,r){if(!n)return e;var u=3>arguments.length;t=J.createCallback(t,r,4);var o=-1,i=n.length;if(typeof i=="number")for(u&&(e=n[++o]);++o<i;)e=t(e,n[o],o,n);else h(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o)
|
||||
});return e}function $t(n,t,e,r){var u=3>arguments.length;return t=J.createCallback(t,r,4),Et(n,function(n,r,o){e=u?(u=false,n):t(e,n,r,o)}),e}function Tt(n){var t=-1,e=n?n.length:0,r=Xt(typeof e=="number"?e:0);return St(n,function(n){var e=at(0,++t);r[t]=r[e],r[e]=n}),r}function Ft(n,t,e){var r;t=J.createCallback(t,e,3),e=-1;var u=n?n.length:0;if(typeof u=="number")for(;++e<u&&!(r=t(n[e],e,n)););else h(n,function(n,e,u){return!(r=t(n,e,u))});return!!r}function Bt(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=-1;
|
||||
for(t=J.createCallback(t,e,3);++o<u&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n?n[0]:v;return p(n,0,Se(Ie(0,r),u))}function Wt(t,e,r){if(typeof r=="number"){var u=t?t.length:0;r=0>r?Ie(0,u+r):r||0}else if(r)return r=zt(t,e),t[r]===e?r:-1;return n(t,e,r)}function qt(n,t,e){if(typeof t!="number"&&null!=t){var r=0,u=-1,o=n?n.length:0;for(t=J.createCallback(t,e,3);++u<o&&t(n[u],u,n);)r++}else r=null==t||e?1:Ie(0,t);return p(n,r)}function zt(n,t,e,r){var u=0,o=n?n.length:u;for(e=e?J.createCallback(e,r,1):Ut,t=e(t);u<o;)r=u+o>>>1,e(n[r])<t?u=r+1:o=r;
|
||||
return u}function Pt(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=typeof t!="function"&&r&&r[t]===n?null:t,t=false),null!=e&&(e=J.createCallback(e,r,3)),ft(n,t,e)}function Kt(){for(var n=1<arguments.length?arguments:arguments[0],t=-1,e=n?At(Ve(n,"length")):0,r=Xt(0>e?0:e);++t<e;)r[t]=Ve(n,t);return r}function Lt(n,t){var e=-1,r=n?n.length:0,u={};for(t||!r||Te(n[0])||(t=[]);++e<r;){var o=n[e];t?u[o]=t[e]:o&&(u[o[0]]=o[1])}return u}function Mt(n,t){return 2<arguments.length?ct(n,17,p(arguments,2),null,t):ct(n,1,null,null,t)
|
||||
}function Vt(n,t,e){function r(){c&&ve(c),i=c=p=v,(g||h!==t)&&(s=Ue(),a=n.apply(l,o),c||i||(o=l=null))}function u(){var e=t-(Ue()-f);0<e?c=_e(u,e):(i&&ve(i),e=p,i=c=p=v,e&&(s=Ue(),a=n.apply(l,o),c||i||(o=l=null)))}var o,i,a,f,l,c,p,s=0,h=false,g=true;if(!dt(n))throw new ie;if(t=Ie(0,t)||0,true===e)var y=true,g=false;else wt(e)&&(y=e.leading,h="maxWait"in e&&(Ie(t,e.maxWait)||0),g="trailing"in e?e.trailing:g);return function(){if(o=arguments,f=Ue(),l=this,p=g&&(c||!y),false===h)var e=y&&!c;else{i||y||(s=f);var v=h-(f-s),m=0>=v;
|
||||
m?(i&&(i=ve(i)),s=f,a=n.apply(l,o)):i||(i=_e(r,v))}return m&&c?c=ve(c):c||t===h||(c=_e(u,t)),e&&(m=true,a=n.apply(l,o)),!m||c||i||(o=l=null),a}}function Ut(n){return n}function Gt(n,t,e){var r=true,u=t&&bt(t);t&&(e||u.length)||(null==e&&(e=t),o=Q,t=n,n=J,u=bt(t)),false===e?r=false:wt(e)&&"chain"in e&&(r=e.chain);var o=n,i=dt(o);St(u,function(e){var u=n[e]=t[e];i&&(o.prototype[e]=function(){var t=this.__chain__,e=this.__wrapped__,i=[e];if(be.apply(i,arguments),i=u.apply(n,i),r||t){if(e===i&&wt(i))return this;
|
||||
i=new o(i),i.__chain__=t}return i})})}function Ht(){}function Jt(n){return function(t){return t[n]}}function Qt(){return this.__wrapped__}e=e?Y.defaults(G.Object(),e,Y.pick(G,A)):G;var Xt=e.Array,Yt=e.Boolean,Zt=e.Date,ne=e.Function,te=e.Math,ee=e.Number,re=e.Object,ue=e.RegExp,oe=e.String,ie=e.TypeError,ae=[],fe=re.prototype,le=e._,ce=fe.toString,pe=ue("^"+oe(ce).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/toString| for [^\]]+/g,".*?")+"$"),se=te.ceil,ve=e.clearTimeout,he=te.floor,ge=ne.prototype.toString,ye=vt(ye=re.getPrototypeOf)&&ye,me=fe.hasOwnProperty,be=ae.push,_e=e.setTimeout,de=ae.splice,we=ae.unshift,je=function(){try{var n={},t=vt(t=re.defineProperty)&&t,e=t(n,n,n)&&t
|
||||
}catch(r){}return e}(),ke=vt(ke=re.create)&&ke,xe=vt(xe=Xt.isArray)&&xe,Ce=e.isFinite,Oe=e.isNaN,Ne=vt(Ne=re.keys)&&Ne,Ie=te.max,Se=te.min,Ee=e.parseInt,Re=te.random,Ae={};Ae[$]=Xt,Ae[T]=Yt,Ae[F]=Zt,Ae[B]=ne,Ae[q]=re,Ae[W]=ee,Ae[z]=ue,Ae[P]=oe,Q.prototype=J.prototype;var De=J.support={};De.funcDecomp=!vt(e.a)&&E.test(s),De.funcNames=typeof ne.name=="string",J.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:N,variable:"",imports:{_:J}},ke||(nt=function(){function n(){}return function(t){if(wt(t)){n.prototype=t;
|
||||
var r=new n;n.prototype=null}return r||e.Object()}}());var $e=je?function(n,t){M.value=t,je(n,"__bindData__",M)}:Ht,Te=xe||function(n){return n&&typeof n=="object"&&typeof n.length=="number"&&ce.call(n)==$||false},Fe=Ne?function(n){return wt(n)?Ne(n):[]}:H,Be={"&":"&","<":"<",">":">",'"':""","'":"'"},We=_t(Be),qe=ue("("+Fe(We).join("|")+")","g"),ze=ue("["+Fe(Be).join("")+"]","g"),Pe=ye?function(n){if(!n||ce.call(n)!=q)return false;var t=n.valueOf,e=vt(t)&&(e=ye(t))&&ye(e);return e?n==e||ye(n)==e:ht(n)
|
||||
}:ht,Ke=lt(function(n,t,e){me.call(n,e)?n[e]++:n[e]=1}),Le=lt(function(n,t,e){(me.call(n,e)?n[e]:n[e]=[]).push(t)}),Me=lt(function(n,t,e){n[e]=t}),Ve=Rt,Ue=vt(Ue=Zt.now)&&Ue||function(){return(new Zt).getTime()},Ge=8==Ee(d+"08")?Ee:function(n,t){return Ee(kt(n)?n.replace(I,""):n,t||0)};return J.after=function(n,t){if(!dt(t))throw new ie;return function(){return 1>--n?t.apply(this,arguments):void 0}},J.assign=U,J.at=function(n){for(var t=arguments,e=-1,r=ut(t,true,false,1),t=t[2]&&t[2][t[1]]===n?1:r.length,u=Xt(t);++e<t;)u[e]=n[r[e]];
|
||||
return u},J.bind=Mt,J.bindAll=function(n){for(var t=1<arguments.length?ut(arguments,true,false,1):bt(n),e=-1,r=t.length;++e<r;){var u=t[e];n[u]=ct(n[u],1,null,null,n)}return n},J.bindKey=function(n,t){return 2<arguments.length?ct(t,19,p(arguments,2),null,n):ct(t,3,null,null,n)},J.chain=function(n){return n=new Q(n),n.__chain__=true,n},J.compact=function(n){for(var t=-1,e=n?n.length:0,r=[];++t<e;){var u=n[t];u&&r.push(u)}return r},J.compose=function(){for(var n=arguments,t=n.length;t--;)if(!dt(n[t]))throw new ie;
|
||||
return function(){for(var t=arguments,e=n.length;e--;)t=[n[e].apply(this,t)];return t[0]}},J.constant=function(n){return function(){return n}},J.countBy=Ke,J.create=function(n,t){var e=nt(n);return t?U(e,t):e},J.createCallback=function(n,t,e){var r=typeof n;if(null==n||"function"==r)return tt(n,t,e);if("object"!=r)return Jt(n);var u=Fe(n),o=u[0],i=n[o];return 1!=u.length||i!==i||wt(i)?function(t){for(var e=u.length,r=false;e--&&(r=ot(t[u[e]],n[u[e]],null,true)););return r}:function(n){return n=n[o],i===n&&(0!==i||1/i==1/n)
|
||||
}},J.curry=function(n,t){return t=typeof t=="number"?t:+t||n.length,ct(n,4,null,null,null,t)},J.debounce=Vt,J.defaults=_,J.defer=function(n){if(!dt(n))throw new ie;var t=p(arguments,1);return _e(function(){n.apply(v,t)},1)},J.delay=function(n,t){if(!dt(n))throw new ie;var e=p(arguments,2);return _e(function(){n.apply(v,e)},t)},J.difference=function(n){return rt(n,ut(arguments,true,true,1))},J.filter=Nt,J.flatten=function(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=typeof t!="function"&&r&&r[t]===n?null:t,t=false),null!=e&&(n=Rt(n,e,r)),ut(n,t)
|
||||
},J.forEach=St,J.forEachRight=Et,J.forIn=g,J.forInRight=function(n,t,e){var r=[];g(n,function(n,t){r.push(t,n)});var u=r.length;for(t=tt(t,e,3);u--&&false!==t(r[u--],r[u],n););return n},J.forOwn=h,J.forOwnRight=mt,J.functions=bt,J.groupBy=Le,J.indexBy=Me,J.initial=function(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=u;for(t=J.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else r=null==t||e?1:t||r;return p(n,0,Se(Ie(0,u-r),u))},J.intersection=function(){for(var e=[],r=-1,u=arguments.length,i=a(),f=st(),p=f===n,s=a();++r<u;){var v=arguments[r];
|
||||
(Te(v)||yt(v))&&(e.push(v),i.push(p&&v.length>=b&&o(r?e[r]:s)))}var p=e[0],h=-1,g=p?p.length:0,y=[];n:for(;++h<g;){var m=i[0],v=p[h];if(0>(m?t(m,v):f(s,v))){for(r=u,(m||s).push(v);--r;)if(m=i[r],0>(m?t(m,v):f(e[r],v)))continue n;y.push(v)}}for(;u--;)(m=i[u])&&c(m);return l(i),l(s),y},J.invert=_t,J.invoke=function(n,t){var e=p(arguments,2),r=-1,u=typeof t=="function",o=n?n.length:0,i=Xt(typeof o=="number"?o:0);return St(n,function(n){i[++r]=(u?t:n[t]).apply(n,e)}),i},J.keys=Fe,J.map=Rt,J.mapValues=function(n,t,e){var r={};
|
||||
return t=J.createCallback(t,e,3),h(n,function(n,e,u){r[e]=t(n,e,u)}),r},J.max=At,J.memoize=function(n,t){function e(){var r=e.cache,u=t?t.apply(this,arguments):m+arguments[0];return me.call(r,u)?r[u]:r[u]=n.apply(this,arguments)}if(!dt(n))throw new ie;return e.cache={},e},J.merge=function(n){var t=arguments,e=2;if(!wt(n))return n;if("number"!=typeof t[2]&&(e=t.length),3<e&&"function"==typeof t[e-2])var r=tt(t[--e-1],t[e--],2);else 2<e&&"function"==typeof t[e-1]&&(r=t[--e]);for(var t=p(arguments,1,e),u=-1,o=a(),i=a();++u<e;)it(n,t[u],r,o,i);
|
||||
return l(o),l(i),n},J.min=function(n,t,e){var u=1/0,o=u;if(typeof t!="function"&&e&&e[t]===n&&(t=null),null==t&&Te(n)){e=-1;for(var i=n.length;++e<i;){var a=n[e];a<o&&(o=a)}}else t=null==t&&kt(n)?r:J.createCallback(t,e,3),St(n,function(n,e,r){e=t(n,e,r),e<u&&(u=e,o=n)});return o},J.omit=function(n,t,e){var r={};if(typeof t!="function"){var u=[];g(n,function(n,t){u.push(t)});for(var u=rt(u,ut(arguments,true,false,1)),o=-1,i=u.length;++o<i;){var a=u[o];r[a]=n[a]}}else t=J.createCallback(t,e,3),g(n,function(n,e,u){t(n,e,u)||(r[e]=n)
|
||||
});return r},J.once=function(n){var t,e;if(!dt(n))throw new ie;return function(){return t?e:(t=true,e=n.apply(this,arguments),n=null,e)}},J.pairs=function(n){for(var t=-1,e=Fe(n),r=e.length,u=Xt(r);++t<r;){var o=e[t];u[t]=[o,n[o]]}return u},J.partial=function(n){return ct(n,16,p(arguments,1))},J.partialRight=function(n){return ct(n,32,null,p(arguments,1))},J.pick=function(n,t,e){var r={};if(typeof t!="function")for(var u=-1,o=ut(arguments,true,false,1),i=wt(n)?o.length:0;++u<i;){var a=o[u];a in n&&(r[a]=n[a])
|
||||
}else t=J.createCallback(t,e,3),g(n,function(n,e,u){t(n,e,u)&&(r[e]=n)});return r},J.pluck=Ve,J.property=Jt,J.pull=function(n){for(var t=arguments,e=0,r=t.length,u=n?n.length:0;++e<r;)for(var o=-1,i=t[e];++o<u;)n[o]===i&&(de.call(n,o--,1),u--);return n},J.range=function(n,t,e){n=+n||0,e=typeof e=="number"?e:+e||1,null==t&&(t=n,n=0);var r=-1;t=Ie(0,se((t-n)/(e||1)));for(var u=Xt(t);++r<t;)u[r]=n,n+=e;return u},J.reject=function(n,t,e){return t=J.createCallback(t,e,3),Nt(n,function(n,e,r){return!t(n,e,r)
|
||||
})},J.remove=function(n,t,e){var r=-1,u=n?n.length:0,o=[];for(t=J.createCallback(t,e,3);++r<u;)e=n[r],t(e,r,n)&&(o.push(e),de.call(n,r--,1),u--);return o},J.rest=qt,J.shuffle=Tt,J.sortBy=function(n,t,e){var r=-1,o=Te(t),i=n?n.length:0,p=Xt(typeof i=="number"?i:0);for(o||(t=J.createCallback(t,e,3)),St(n,function(n,e,u){var i=p[++r]=f();o?i.m=Rt(t,function(t){return n[t]}):(i.m=a())[0]=t(n,e,u),i.n=r,i.o=n}),i=p.length,p.sort(u);i--;)n=p[i],p[i]=n.o,o||l(n.m),c(n);return p},J.tap=function(n,t){return t(n),n
|
||||
},J.throttle=function(n,t,e){var r=true,u=true;if(!dt(n))throw new ie;return false===e?r=false:wt(e)&&(r="leading"in e?e.leading:r,u="trailing"in e?e.trailing:u),L.leading=r,L.maxWait=t,L.trailing=u,Vt(n,t,L)},J.times=function(n,t,e){n=-1<(n=+n)?n:0;var r=-1,u=Xt(n);for(t=tt(t,e,1);++r<n;)u[r]=t(r);return u},J.toArray=function(n){return n&&typeof n.length=="number"?p(n):xt(n)},J.transform=function(n,t,e,r){var u=Te(n);if(null==e)if(u)e=[];else{var o=n&&n.constructor;e=nt(o&&o.prototype)}return t&&(t=J.createCallback(t,r,4),(u?St:h)(n,function(n,r,u){return t(e,n,r,u)
|
||||
})),e},J.union=function(){return ft(ut(arguments,true,true))},J.uniq=Pt,J.values=xt,J.where=Nt,J.without=function(n){return rt(n,p(arguments,1))},J.wrap=function(n,t){return ct(t,16,[n])},J.xor=function(){for(var n=-1,t=arguments.length;++n<t;){var e=arguments[n];if(Te(e)||yt(e))var r=r?ft(rt(r,e).concat(rt(e,r))):e}return r||[]},J.zip=Kt,J.zipObject=Lt,J.collect=Rt,J.drop=qt,J.each=St,J.eachRight=Et,J.extend=U,J.methods=bt,J.object=Lt,J.select=Nt,J.tail=qt,J.unique=Pt,J.unzip=Kt,Gt(J),J.clone=function(n,t,e,r){return typeof t!="boolean"&&null!=t&&(r=e,e=t,t=false),Z(n,t,typeof e=="function"&&tt(e,r,1))
|
||||
},J.cloneDeep=function(n,t,e){return Z(n,true,typeof t=="function"&&tt(t,e,1))},J.contains=Ct,J.escape=function(n){return null==n?"":oe(n).replace(ze,pt)},J.every=Ot,J.find=It,J.findIndex=function(n,t,e){var r=-1,u=n?n.length:0;for(t=J.createCallback(t,e,3);++r<u;)if(t(n[r],r,n))return r;return-1},J.findKey=function(n,t,e){var r;return t=J.createCallback(t,e,3),h(n,function(n,e,u){return t(n,e,u)?(r=e,false):void 0}),r},J.findLast=function(n,t,e){var r;return t=J.createCallback(t,e,3),Et(n,function(n,e,u){return t(n,e,u)?(r=n,false):void 0
|
||||
}),r},J.findLastIndex=function(n,t,e){var r=n?n.length:0;for(t=J.createCallback(t,e,3);r--;)if(t(n[r],r,n))return r;return-1},J.findLastKey=function(n,t,e){var r;return t=J.createCallback(t,e,3),mt(n,function(n,e,u){return t(n,e,u)?(r=e,false):void 0}),r},J.has=function(n,t){return n?me.call(n,t):false},J.identity=Ut,J.indexOf=Wt,J.isArguments=yt,J.isArray=Te,J.isBoolean=function(n){return true===n||false===n||n&&typeof n=="object"&&ce.call(n)==T||false},J.isDate=function(n){return n&&typeof n=="object"&&ce.call(n)==F||false
|
||||
},J.isElement=function(n){return n&&1===n.nodeType||false},J.isEmpty=function(n){var t=true;if(!n)return t;var e=ce.call(n),r=n.length;return e==$||e==P||e==D||e==q&&typeof r=="number"&&dt(n.splice)?!r:(h(n,function(){return t=false}),t)},J.isEqual=function(n,t,e,r){return ot(n,t,typeof e=="function"&&tt(e,r,2))},J.isFinite=function(n){return Ce(n)&&!Oe(parseFloat(n))},J.isFunction=dt,J.isNaN=function(n){return jt(n)&&n!=+n},J.isNull=function(n){return null===n},J.isNumber=jt,J.isObject=wt,J.isPlainObject=Pe,J.isRegExp=function(n){return n&&typeof n=="object"&&ce.call(n)==z||false
|
||||
},J.isString=kt,J.isUndefined=function(n){return typeof n=="undefined"},J.lastIndexOf=function(n,t,e){var r=n?n.length:0;for(typeof e=="number"&&(r=(0>e?Ie(0,r+e):Se(e,r-1))+1);r--;)if(n[r]===t)return r;return-1},J.mixin=Gt,J.noConflict=function(){return e._=le,this},J.noop=Ht,J.now=Ue,J.parseInt=Ge,J.random=function(n,t,e){var r=null==n,u=null==t;return null==e&&(typeof n=="boolean"&&u?(e=n,n=1):u||typeof t!="boolean"||(e=t,u=true)),r&&u&&(t=1),n=+n||0,u?(t=n,n=0):t=+t||0,e||n%1||t%1?(e=Re(),Se(n+e*(t-n+parseFloat("1e-"+((e+"").length-1))),t)):at(n,t)
|
||||
},J.reduce=Dt,J.reduceRight=$t,J.result=function(n,t){if(n){var e=n[t];return dt(e)?n[t]():e}},J.runInContext=s,J.size=function(n){var t=n?n.length:0;return typeof t=="number"?t:Fe(n).length},J.some=Ft,J.sortedIndex=zt,J.template=function(n,t,e){var r=J.templateSettings;n=oe(n||""),e=_({},e,r);var u,o=_({},e.imports,r.imports),r=Fe(o),o=xt(o),a=0,f=e.interpolate||S,l="__p+='",f=ue((e.escape||S).source+"|"+f.source+"|"+(f===N?x:S).source+"|"+(e.evaluate||S).source+"|$","g");n.replace(f,function(t,e,r,o,f,c){return r||(r=o),l+=n.slice(a,c).replace(R,i),e&&(l+="'+__e("+e+")+'"),f&&(u=true,l+="';"+f+";\n__p+='"),r&&(l+="'+((__t=("+r+"))==null?'':__t)+'"),a=c+t.length,t
|
||||
}),l+="';",f=e=e.variable,f||(e="obj",l="with("+e+"){"+l+"}"),l=(u?l.replace(w,""):l).replace(j,"$1").replace(k,"$1;"),l="function("+e+"){"+(f?"":e+"||("+e+"={});")+"var __t,__p='',__e=_.escape"+(u?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}";try{var c=ne(r,"return "+l).apply(v,o)}catch(p){throw p.source=l,p}return t?c(t):(c.source=l,c)},J.unescape=function(n){return null==n?"":oe(n).replace(qe,gt)},J.uniqueId=function(n){var t=++y;return oe(null==n?"":n)+t
|
||||
},J.all=Ot,J.any=Ft,J.detect=It,J.findWhere=It,J.foldl=Dt,J.foldr=$t,J.include=Ct,J.inject=Dt,Gt(function(){var n={};return h(J,function(t,e){J.prototype[e]||(n[e]=t)}),n}(),false),J.first=Bt,J.last=function(n,t,e){var r=0,u=n?n.length:0;if(typeof t!="number"&&null!=t){var o=u;for(t=J.createCallback(t,e,3);o--&&t(n[o],o,n);)r++}else if(r=t,null==r||e)return n?n[u-1]:v;return p(n,Ie(0,u-r))},J.sample=function(n,t,e){return n&&typeof n.length!="number"&&(n=xt(n)),null==t||e?n?n[at(0,n.length-1)]:v:(n=Tt(n),n.length=Se(Ie(0,t),n.length),n)
|
||||
},J.take=Bt,J.head=Bt,h(J,function(n,t){var e="sample"!==t;J.prototype[t]||(J.prototype[t]=function(t,r){var u=this.__chain__,o=n(this.__wrapped__,t,r);return u||null!=t&&(!r||e&&typeof t=="function")?new Q(o,u):o})}),J.VERSION="2.4.1",J.prototype.chain=function(){return this.__chain__=true,this},J.prototype.toString=function(){return oe(this.__wrapped__)},J.prototype.value=Qt,J.prototype.valueOf=Qt,St(["join","pop","shift"],function(n){var t=ae[n];J.prototype[n]=function(){var n=this.__chain__,e=t.apply(this.__wrapped__,arguments);
|
||||
return n?new Q(e,n):e}}),St(["push","reverse","sort","unshift"],function(n){var t=ae[n];J.prototype[n]=function(){return t.apply(this.__wrapped__,arguments),this}}),St(["concat","slice","splice"],function(n){var t=ae[n];J.prototype[n]=function(){return new Q(t.apply(this.__wrapped__,arguments),this.__chain__)}}),J}var v,h=[],g=[],y=0,m=+new Date+"",b=75,_=40,d=" \t\x0B\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",w=/\b__p\+='';/g,j=/\b(__p\+=)''\+/g,k=/(__e\(.*?\)|\b__t\))\+'';/g,x=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,C=/\w*$/,O=/^\s*function[ \n\r\t]+\w/,N=/<%=([\s\S]+?)%>/g,I=RegExp("^["+d+"]*0+(?=.$)"),S=/($^)/,E=/\bthis\b/,R=/['\n\r\t\u2028\u2029\\]/g,A="Array Boolean Date Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setTimeout".split(" "),D="[object Arguments]",$="[object Array]",T="[object Boolean]",F="[object Date]",B="[object Function]",W="[object Number]",q="[object Object]",z="[object RegExp]",P="[object String]",K={};
|
||||
K[B]=false,K[D]=K[$]=K[T]=K[F]=K[W]=K[q]=K[z]=K[P]=true;var L={leading:false,maxWait:0,trailing:false},M={configurable:false,enumerable:false,value:null,writable:false},V={"boolean":false,"function":true,object:true,number:false,string:false,undefined:false},U={"\\":"\\","'":"'","\n":"n","\r":"r","\t":"t","\u2028":"u2028","\u2029":"u2029"},G=V[typeof window]&&window||this,H=V[typeof exports]&&exports&&!exports.nodeType&&exports,J=V[typeof module]&&module&&!module.nodeType&&module,Q=J&&J.exports===H&&H,X=V[typeof global]&&global;!X||X.global!==X&&X.window!==X||(G=X);
|
||||
var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G._=Y, define(function(){return Y})):H&&J?Q?(J.exports=Y)._=Y:H._=Y:G._=Y}).call(this);
|
12
browser/components/loop/content/shared/libs/otcdn/webrtc/v2.2.5/css/ot.min.css
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 734 B |
After Width: | Height: | Size: 861 B |
After Width: | Height: | Size: 622 B |
28
browser/components/loop/content/shared/libs/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @license OpenTok JavaScript Library v2.2.5
|
||||
* http://www.tokbox.com/
|
||||
*
|
||||
* Copyright (c) 2014 TokBox, Inc.
|
||||
* Released under the MIT license
|
||||
* http://opensource.org/licenses/MIT
|
||||
*
|
||||
* Date: May 22 07:14:18 2014
|
||||
*/
|
||||
|
||||
!(function() {
|
||||
TB.Config.replaceWith({
|
||||
global: {
|
||||
exceptionLogging: {
|
||||
enabled: true,
|
||||
messageLimitPerPartner: 100
|
||||
},
|
||||
|
||||
iceServers: {
|
||||
enabled: false
|
||||
},
|
||||
},
|
||||
|
||||
partners: {
|
||||
}
|
||||
});
|
||||
})(TB);
|
18115
browser/components/loop/content/shared/libs/sdk.js
Normal file
606
browser/components/loop/content/shared/libs/sjcl-dev20140604.js
Normal file
@ -0,0 +1,606 @@
|
||||
/** @fileOverview Javascript cryptography implementation.
|
||||
*
|
||||
* Crush to remove comments, shorten variable names and
|
||||
* generally reduce transmission size.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
/*jslint indent: 2, bitwise: false, nomen: false, plusplus: false, white: false, regexp: false */
|
||||
/*global document, window, escape, unescape, module, require, Uint32Array */
|
||||
|
||||
/** @namespace The Stanford Javascript Crypto Library, top-level namespace. */
|
||||
var sjcl = {
|
||||
/** @namespace Symmetric ciphers. */
|
||||
cipher: {},
|
||||
|
||||
/** @namespace Hash functions. Right now only SHA256 is implemented. */
|
||||
hash: {},
|
||||
|
||||
/** @namespace Key exchange functions. Right now only SRP is implemented. */
|
||||
keyexchange: {},
|
||||
|
||||
/** @namespace Block cipher modes of operation. */
|
||||
mode: {},
|
||||
|
||||
/** @namespace Miscellaneous. HMAC and PBKDF2. */
|
||||
misc: {},
|
||||
|
||||
/**
|
||||
* @namespace Bit array encoders and decoders.
|
||||
*
|
||||
* @description
|
||||
* The members of this namespace are functions which translate between
|
||||
* SJCL's bitArrays and other objects (usually strings). Because it
|
||||
* isn't always clear which direction is encoding and which is decoding,
|
||||
* the method names are "fromBits" and "toBits".
|
||||
*/
|
||||
codec: {},
|
||||
|
||||
/** @namespace Exceptions. */
|
||||
exception: {
|
||||
/** @constructor Ciphertext is corrupt. */
|
||||
corrupt: function(message) {
|
||||
this.toString = function() { return "CORRUPT: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Invalid parameter. */
|
||||
invalid: function(message) {
|
||||
this.toString = function() { return "INVALID: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Bug or missing feature in SJCL. @constructor */
|
||||
bug: function(message) {
|
||||
this.toString = function() { return "BUG: "+this.message; };
|
||||
this.message = message;
|
||||
},
|
||||
|
||||
/** @constructor Something isn't ready. */
|
||||
notReady: function(message) {
|
||||
this.toString = function() { return "NOT READY: "+this.message; };
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(typeof module !== 'undefined' && module.exports){
|
||||
module.exports = sjcl;
|
||||
}
|
||||
/** @fileOverview Arrays of bits, encoded as arrays of Numbers.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace Arrays of bits, encoded as arrays of Numbers.
|
||||
*
|
||||
* @description
|
||||
* <p>
|
||||
* These objects are the currency accepted by SJCL's crypto functions.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Most of our crypto primitives operate on arrays of 4-byte words internally,
|
||||
* but many of them can take arguments that are not a multiple of 4 bytes.
|
||||
* This library encodes arrays of bits (whose size need not be a multiple of 8
|
||||
* bits) as arrays of 32-bit words. The bits are packed, big-endian, into an
|
||||
* array of words, 32 bits at a time. Since the words are double-precision
|
||||
* floating point numbers, they fit some extra data. We use this (in a private,
|
||||
* possibly-changing manner) to encode the number of bits actually present
|
||||
* in the last word of the array.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Because bitwise ops clear this out-of-band data, these arrays can be passed
|
||||
* to ciphers like AES which want arrays of words.
|
||||
* </p>
|
||||
*/
|
||||
sjcl.bitArray = {
|
||||
/**
|
||||
* Array slices in units of bits.
|
||||
* @param {bitArray} a The array to slice.
|
||||
* @param {Number} bstart The offset to the start of the slice, in bits.
|
||||
* @param {Number} bend The offset to the end of the slice, in bits. If this is undefined,
|
||||
* slice until the end of the array.
|
||||
* @return {bitArray} The requested slice.
|
||||
*/
|
||||
bitSlice: function (a, bstart, bend) {
|
||||
a = sjcl.bitArray._shiftRight(a.slice(bstart/32), 32 - (bstart & 31)).slice(1);
|
||||
return (bend === undefined) ? a : sjcl.bitArray.clamp(a, bend-bstart);
|
||||
},
|
||||
|
||||
/**
|
||||
* Extract a number packed into a bit array.
|
||||
* @param {bitArray} a The array to slice.
|
||||
* @param {Number} bstart The offset to the start of the slice, in bits.
|
||||
* @param {Number} length The length of the number to extract.
|
||||
* @return {Number} The requested slice.
|
||||
*/
|
||||
extract: function(a, bstart, blength) {
|
||||
// FIXME: this Math.floor is not necessary at all, but for some reason
|
||||
// seems to suppress a bug in the Chromium JIT.
|
||||
var x, sh = Math.floor((-bstart-blength) & 31);
|
||||
if ((bstart + blength - 1 ^ bstart) & -32) {
|
||||
// it crosses a boundary
|
||||
x = (a[bstart/32|0] << (32 - sh)) ^ (a[bstart/32+1|0] >>> sh);
|
||||
} else {
|
||||
// within a single word
|
||||
x = a[bstart/32|0] >>> sh;
|
||||
}
|
||||
return x & ((1<<blength) - 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Concatenate two bit arrays.
|
||||
* @param {bitArray} a1 The first array.
|
||||
* @param {bitArray} a2 The second array.
|
||||
* @return {bitArray} The concatenation of a1 and a2.
|
||||
*/
|
||||
concat: function (a1, a2) {
|
||||
if (a1.length === 0 || a2.length === 0) {
|
||||
return a1.concat(a2);
|
||||
}
|
||||
|
||||
var last = a1[a1.length-1], shift = sjcl.bitArray.getPartial(last);
|
||||
if (shift === 32) {
|
||||
return a1.concat(a2);
|
||||
} else {
|
||||
return sjcl.bitArray._shiftRight(a2, shift, last|0, a1.slice(0,a1.length-1));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the length of an array of bits.
|
||||
* @param {bitArray} a The array.
|
||||
* @return {Number} The length of a, in bits.
|
||||
*/
|
||||
bitLength: function (a) {
|
||||
var l = a.length, x;
|
||||
if (l === 0) { return 0; }
|
||||
x = a[l - 1];
|
||||
return (l-1) * 32 + sjcl.bitArray.getPartial(x);
|
||||
},
|
||||
|
||||
/**
|
||||
* Truncate an array.
|
||||
* @param {bitArray} a The array.
|
||||
* @param {Number} len The length to truncate to, in bits.
|
||||
* @return {bitArray} A new array, truncated to len bits.
|
||||
*/
|
||||
clamp: function (a, len) {
|
||||
if (a.length * 32 < len) { return a; }
|
||||
a = a.slice(0, Math.ceil(len / 32));
|
||||
var l = a.length;
|
||||
len = len & 31;
|
||||
if (l > 0 && len) {
|
||||
a[l-1] = sjcl.bitArray.partial(len, a[l-1] & 0x80000000 >> (len-1), 1);
|
||||
}
|
||||
return a;
|
||||
},
|
||||
|
||||
/**
|
||||
* Make a partial word for a bit array.
|
||||
* @param {Number} len The number of bits in the word.
|
||||
* @param {Number} x The bits.
|
||||
* @param {Number} [0] _end Pass 1 if x has already been shifted to the high side.
|
||||
* @return {Number} The partial word.
|
||||
*/
|
||||
partial: function (len, x, _end) {
|
||||
if (len === 32) { return x; }
|
||||
return (_end ? x|0 : x << (32-len)) + len * 0x10000000000;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the number of bits used by a partial word.
|
||||
* @param {Number} x The partial word.
|
||||
* @return {Number} The number of bits used by the partial word.
|
||||
*/
|
||||
getPartial: function (x) {
|
||||
return Math.round(x/0x10000000000) || 32;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compare two arrays for equality in a predictable amount of time.
|
||||
* @param {bitArray} a The first array.
|
||||
* @param {bitArray} b The second array.
|
||||
* @return {boolean} true if a == b; false otherwise.
|
||||
*/
|
||||
equal: function (a, b) {
|
||||
if (sjcl.bitArray.bitLength(a) !== sjcl.bitArray.bitLength(b)) {
|
||||
return false;
|
||||
}
|
||||
var x = 0, i;
|
||||
for (i=0; i<a.length; i++) {
|
||||
x |= a[i]^b[i];
|
||||
}
|
||||
return (x === 0);
|
||||
},
|
||||
|
||||
/** Shift an array right.
|
||||
* @param {bitArray} a The array to shift.
|
||||
* @param {Number} shift The number of bits to shift.
|
||||
* @param {Number} [carry=0] A byte to carry in
|
||||
* @param {bitArray} [out=[]] An array to prepend to the output.
|
||||
* @private
|
||||
*/
|
||||
_shiftRight: function (a, shift, carry, out) {
|
||||
var i, last2=0, shift2;
|
||||
if (out === undefined) { out = []; }
|
||||
|
||||
for (; shift >= 32; shift -= 32) {
|
||||
out.push(carry);
|
||||
carry = 0;
|
||||
}
|
||||
if (shift === 0) {
|
||||
return out.concat(a);
|
||||
}
|
||||
|
||||
for (i=0; i<a.length; i++) {
|
||||
out.push(carry | a[i]>>>shift);
|
||||
carry = a[i] << (32-shift);
|
||||
}
|
||||
last2 = a.length ? a[a.length-1] : 0;
|
||||
shift2 = sjcl.bitArray.getPartial(last2);
|
||||
out.push(sjcl.bitArray.partial(shift+shift2 & 31, (shift + shift2 > 32) ? carry : out.pop(),1));
|
||||
return out;
|
||||
},
|
||||
|
||||
/** xor a block of 4 words together.
|
||||
* @private
|
||||
*/
|
||||
_xor4: function(x,y) {
|
||||
return [x[0]^y[0],x[1]^y[1],x[2]^y[2],x[3]^y[3]];
|
||||
}
|
||||
};
|
||||
/** @fileOverview Bit array codec implementations.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace UTF-8 strings */
|
||||
sjcl.codec.utf8String = {
|
||||
/** Convert from a bitArray to a UTF-8 string. */
|
||||
fromBits: function (arr) {
|
||||
var out = "", bl = sjcl.bitArray.bitLength(arr), i, tmp;
|
||||
for (i=0; i<bl/8; i++) {
|
||||
if ((i&3) === 0) {
|
||||
tmp = arr[i/4];
|
||||
}
|
||||
out += String.fromCharCode(tmp >>> 24);
|
||||
tmp <<= 8;
|
||||
}
|
||||
return decodeURIComponent(escape(out));
|
||||
},
|
||||
|
||||
/** Convert from a UTF-8 string to a bitArray. */
|
||||
toBits: function (str) {
|
||||
str = unescape(encodeURIComponent(str));
|
||||
var out = [], i, tmp=0;
|
||||
for (i=0; i<str.length; i++) {
|
||||
tmp = tmp << 8 | str.charCodeAt(i);
|
||||
if ((i&3) === 3) {
|
||||
out.push(tmp);
|
||||
tmp = 0;
|
||||
}
|
||||
}
|
||||
if (i&3) {
|
||||
out.push(sjcl.bitArray.partial(8*(i&3), tmp));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
/** @fileOverview Bit array codec implementations.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** @namespace Hexadecimal */
|
||||
sjcl.codec.hex = {
|
||||
/** Convert from a bitArray to a hex string. */
|
||||
fromBits: function (arr) {
|
||||
var out = "", i;
|
||||
for (i=0; i<arr.length; i++) {
|
||||
out += ((arr[i]|0)+0xF00000000000).toString(16).substr(4);
|
||||
}
|
||||
return out.substr(0, sjcl.bitArray.bitLength(arr)/4);//.replace(/(.{8})/g, "$1 ");
|
||||
},
|
||||
/** Convert from a hex string to a bitArray. */
|
||||
toBits: function (str) {
|
||||
var i, out=[], len;
|
||||
str = str.replace(/\s|0x/g, "");
|
||||
len = str.length;
|
||||
str = str + "00000000";
|
||||
for (i=0; i<str.length; i+=8) {
|
||||
out.push(parseInt(str.substr(i,8),16)^0);
|
||||
}
|
||||
return sjcl.bitArray.clamp(out, len*4);
|
||||
}
|
||||
};
|
||||
|
||||
/** @fileOverview Javascript SHA-256 implementation.
|
||||
*
|
||||
* An older version of this implementation is available in the public
|
||||
* domain, but this one is (c) Emily Stark, Mike Hamburg, Dan Boneh,
|
||||
* Stanford University 2008-2010 and BSD-licensed for liability
|
||||
* reasons.
|
||||
*
|
||||
* Special thanks to Aldo Cortesi for pointing out several bugs in
|
||||
* this code.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/**
|
||||
* Context for a SHA-256 operation in progress.
|
||||
* @constructor
|
||||
* @class Secure Hash Algorithm, 256 bits.
|
||||
*/
|
||||
sjcl.hash.sha256 = function (hash) {
|
||||
if (!this._key[0]) { this._precompute(); }
|
||||
if (hash) {
|
||||
this._h = hash._h.slice(0);
|
||||
this._buffer = hash._buffer.slice(0);
|
||||
this._length = hash._length;
|
||||
} else {
|
||||
this.reset();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hash a string or an array of words.
|
||||
* @static
|
||||
* @param {bitArray|String} data the data to hash.
|
||||
* @return {bitArray} The hash value, an array of 16 big-endian words.
|
||||
*/
|
||||
sjcl.hash.sha256.hash = function (data) {
|
||||
return (new sjcl.hash.sha256()).update(data).finalize();
|
||||
};
|
||||
|
||||
sjcl.hash.sha256.prototype = {
|
||||
/**
|
||||
* The hash's block size, in bits.
|
||||
* @constant
|
||||
*/
|
||||
blockSize: 512,
|
||||
|
||||
/**
|
||||
* Reset the hash state.
|
||||
* @return this
|
||||
*/
|
||||
reset:function () {
|
||||
this._h = this._init.slice(0);
|
||||
this._buffer = [];
|
||||
this._length = 0;
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Input several words to the hash.
|
||||
* @param {bitArray|String} data the data to hash.
|
||||
* @return this
|
||||
*/
|
||||
update: function (data) {
|
||||
if (typeof data === "string") {
|
||||
data = sjcl.codec.utf8String.toBits(data);
|
||||
}
|
||||
var i, b = this._buffer = sjcl.bitArray.concat(this._buffer, data),
|
||||
ol = this._length,
|
||||
nl = this._length = ol + sjcl.bitArray.bitLength(data);
|
||||
for (i = 512+ol & -512; i <= nl; i+= 512) {
|
||||
this._block(b.splice(0,16));
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Complete hashing and output the hash value.
|
||||
* @return {bitArray} The hash value, an array of 8 big-endian words.
|
||||
*/
|
||||
finalize:function () {
|
||||
var i, b = this._buffer, h = this._h;
|
||||
|
||||
// Round out and push the buffer
|
||||
b = sjcl.bitArray.concat(b, [sjcl.bitArray.partial(1,1)]);
|
||||
|
||||
// Round out the buffer to a multiple of 16 words, less the 2 length words.
|
||||
for (i = b.length + 2; i & 15; i++) {
|
||||
b.push(0);
|
||||
}
|
||||
|
||||
// append the length
|
||||
b.push(Math.floor(this._length / 0x100000000));
|
||||
b.push(this._length | 0);
|
||||
|
||||
while (b.length) {
|
||||
this._block(b.splice(0,16));
|
||||
}
|
||||
|
||||
this.reset();
|
||||
return h;
|
||||
},
|
||||
|
||||
/**
|
||||
* The SHA-256 initialization vector, to be precomputed.
|
||||
* @private
|
||||
*/
|
||||
_init:[],
|
||||
/*
|
||||
_init:[0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19],
|
||||
*/
|
||||
|
||||
/**
|
||||
* The SHA-256 hash key, to be precomputed.
|
||||
* @private
|
||||
*/
|
||||
_key:[],
|
||||
/*
|
||||
_key:
|
||||
[0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2],
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Function to precompute _init and _key.
|
||||
* @private
|
||||
*/
|
||||
_precompute: function () {
|
||||
var i = 0, prime = 2, factor;
|
||||
|
||||
function frac(x) { return (x-Math.floor(x)) * 0x100000000 | 0; }
|
||||
|
||||
outer: for (; i<64; prime++) {
|
||||
for (factor=2; factor*factor <= prime; factor++) {
|
||||
if (prime % factor === 0) {
|
||||
// not a prime
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
if (i<8) {
|
||||
this._init[i] = frac(Math.pow(prime, 1/2));
|
||||
}
|
||||
this._key[i] = frac(Math.pow(prime, 1/3));
|
||||
i++;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform one cycle of SHA-256.
|
||||
* @param {bitArray} words one block of words.
|
||||
* @private
|
||||
*/
|
||||
_block:function (words) {
|
||||
var i, tmp, a, b,
|
||||
w = words.slice(0),
|
||||
h = this._h,
|
||||
k = this._key,
|
||||
h0 = h[0], h1 = h[1], h2 = h[2], h3 = h[3],
|
||||
h4 = h[4], h5 = h[5], h6 = h[6], h7 = h[7];
|
||||
|
||||
/* Rationale for placement of |0 :
|
||||
* If a value can overflow is original 32 bits by a factor of more than a few
|
||||
* million (2^23 ish), there is a possibility that it might overflow the
|
||||
* 53-bit mantissa and lose precision.
|
||||
*
|
||||
* To avoid this, we clamp back to 32 bits by |'ing with 0 on any value that
|
||||
* propagates around the loop, and on the hash state h[]. I don't believe
|
||||
* that the clamps on h4 and on h0 are strictly necessary, but it's close
|
||||
* (for h4 anyway), and better safe than sorry.
|
||||
*
|
||||
* The clamps on h[] are necessary for the output to be correct even in the
|
||||
* common case and for short inputs.
|
||||
*/
|
||||
for (i=0; i<64; i++) {
|
||||
// load up the input word for this round
|
||||
if (i<16) {
|
||||
tmp = w[i];
|
||||
} else {
|
||||
a = w[(i+1 ) & 15];
|
||||
b = w[(i+14) & 15];
|
||||
tmp = w[i&15] = ((a>>>7 ^ a>>>18 ^ a>>>3 ^ a<<25 ^ a<<14) +
|
||||
(b>>>17 ^ b>>>19 ^ b>>>10 ^ b<<15 ^ b<<13) +
|
||||
w[i&15] + w[(i+9) & 15]) | 0;
|
||||
}
|
||||
|
||||
tmp = (tmp + h7 + (h4>>>6 ^ h4>>>11 ^ h4>>>25 ^ h4<<26 ^ h4<<21 ^ h4<<7) + (h6 ^ h4&(h5^h6)) + k[i]); // | 0;
|
||||
|
||||
// shift register
|
||||
h7 = h6; h6 = h5; h5 = h4;
|
||||
h4 = h3 + tmp | 0;
|
||||
h3 = h2; h2 = h1; h1 = h0;
|
||||
|
||||
h0 = (tmp + ((h1&h2) ^ (h3&(h1^h2))) + (h1>>>2 ^ h1>>>13 ^ h1>>>22 ^ h1<<30 ^ h1<<19 ^ h1<<10)) | 0;
|
||||
}
|
||||
|
||||
h[0] = h[0]+h0 | 0;
|
||||
h[1] = h[1]+h1 | 0;
|
||||
h[2] = h[2]+h2 | 0;
|
||||
h[3] = h[3]+h3 | 0;
|
||||
h[4] = h[4]+h4 | 0;
|
||||
h[5] = h[5]+h5 | 0;
|
||||
h[6] = h[6]+h6 | 0;
|
||||
h[7] = h[7]+h7 | 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @fileOverview HMAC implementation.
|
||||
*
|
||||
* @author Emily Stark
|
||||
* @author Mike Hamburg
|
||||
* @author Dan Boneh
|
||||
*/
|
||||
|
||||
/** HMAC with the specified hash function.
|
||||
* @constructor
|
||||
* @param {bitArray} key the key for HMAC.
|
||||
* @param {Object} [hash=sjcl.hash.sha256] The hash function to use.
|
||||
*/
|
||||
sjcl.misc.hmac = function (key, Hash) {
|
||||
this._hash = Hash = Hash || sjcl.hash.sha256;
|
||||
var exKey = [[],[]], i,
|
||||
bs = Hash.prototype.blockSize / 32;
|
||||
this._baseHash = [new Hash(), new Hash()];
|
||||
|
||||
if (key.length > bs) {
|
||||
key = Hash.hash(key);
|
||||
}
|
||||
|
||||
for (i=0; i<bs; i++) {
|
||||
exKey[0][i] = key[i]^0x36363636;
|
||||
exKey[1][i] = key[i]^0x5C5C5C5C;
|
||||
}
|
||||
|
||||
this._baseHash[0].update(exKey[0]);
|
||||
this._baseHash[1].update(exKey[1]);
|
||||
this._resultHash = new Hash(this._baseHash[0]);
|
||||
};
|
||||
|
||||
/** HMAC with the specified hash function. Also called encrypt since it's a prf.
|
||||
* @param {bitArray|String} data The data to mac.
|
||||
*/
|
||||
sjcl.misc.hmac.prototype.encrypt = sjcl.misc.hmac.prototype.mac = function (data) {
|
||||
if (!this._updated) {
|
||||
this.update(data);
|
||||
return this.digest(data);
|
||||
} else {
|
||||
throw new sjcl.exception.invalid("encrypt on already updated hmac called!");
|
||||
}
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.reset = function () {
|
||||
this._resultHash = new this._hash(this._baseHash[0]);
|
||||
this._updated = false;
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.update = function (data) {
|
||||
this._updated = true;
|
||||
this._resultHash.update(data);
|
||||
};
|
||||
|
||||
sjcl.misc.hmac.prototype.digest = function () {
|
||||
var w = this._resultHash.finalize(), result = new (this._hash)(this._baseHash[1]).update(w).finalize();
|
||||
|
||||
this.reset();
|
||||
|
||||
return result;
|
||||
};
|
78
browser/components/loop/content/shared/libs/token.js
Normal file
@ -0,0 +1,78 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
'use strict';
|
||||
|
||||
var PREFIX_NAME = 'identity.mozilla.com/picl/v1/';
|
||||
var bitSlice = sjcl.bitArray.bitSlice;
|
||||
var salt = sjcl.codec.hex.toBits('');
|
||||
|
||||
/**
|
||||
* hkdf - The HMAC-based Key Derivation Function
|
||||
* based on https://github.com/mozilla/node-hkdf
|
||||
*
|
||||
* @class hkdf
|
||||
* @param {bitArray} ikm Initial keying material
|
||||
* @param {bitArray} info Key derivation data
|
||||
* @param {bitArray} salt Salt
|
||||
* @param {integer} length Length of the derived key in bytes
|
||||
* @return promise object- It will resolve with `output` data
|
||||
*/
|
||||
function hkdf(ikm, info, salt, length, callback) {
|
||||
var mac = new sjcl.misc.hmac(salt, sjcl.hash.sha256);
|
||||
mac.update(ikm);
|
||||
|
||||
// compute the PRK
|
||||
var prk = mac.digest();
|
||||
|
||||
// hash length is 32 because only sjcl.hash.sha256 is used at this moment
|
||||
var hashLength = 32;
|
||||
var num_blocks = Math.ceil(length / hashLength);
|
||||
var prev = sjcl.codec.hex.toBits('');
|
||||
var output = '';
|
||||
|
||||
for (var i = 0; i < num_blocks; i++) {
|
||||
var hmac = new sjcl.misc.hmac(prk, sjcl.hash.sha256);
|
||||
|
||||
var input = sjcl.bitArray.concat(
|
||||
sjcl.bitArray.concat(prev, info),
|
||||
sjcl.codec.utf8String.toBits((String.fromCharCode(i + 1)))
|
||||
);
|
||||
|
||||
hmac.update(input);
|
||||
|
||||
prev = hmac.digest();
|
||||
output += sjcl.codec.hex.fromBits(prev);
|
||||
}
|
||||
|
||||
var truncated = sjcl.bitArray.clamp(sjcl.codec.hex.toBits(output), length * 8);
|
||||
|
||||
callback(truncated);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @class hawkCredentials
|
||||
* @method deriveHawkCredentials
|
||||
* @param {String} tokenHex
|
||||
* @param {String} context
|
||||
* @param {int} size
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function deriveHawkCredentials(tokenHex, context, size, callback) {
|
||||
var token = sjcl.codec.hex.toBits(tokenHex);
|
||||
var info = sjcl.codec.utf8String.toBits(PREFIX_NAME + context);
|
||||
|
||||
hkdf(token, info, salt, size || 3 * 32, function(out) {
|
||||
var authKey = bitSlice(out, 8 * 32, 8 * 64);
|
||||
var bundleKey = bitSlice(out, 8 * 64);
|
||||
callback({
|
||||
algorithm: 'sha256',
|
||||
id: sjcl.codec.hex.fromBits(bitSlice(out, 0, 8 * 32)),
|
||||
key: sjcl.codec.hex.fromBits(authKey),
|
||||
bundleKey: bundleKey
|
||||
});
|
||||
});
|
||||
}
|
||||
|
1157
browser/components/loop/content/shared/libs/webl10n-20130617.js
Normal file
44
browser/components/loop/jar.mn
Normal file
@ -0,0 +1,44 @@
|
||||
# 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/.
|
||||
|
||||
browser.jar:
|
||||
content/browser/loop/conversation.html (content/conversation.html)
|
||||
content/browser/loop/panel.html (content/panel.html)
|
||||
content/browser/loop/shared/css/common.css (content/shared/css/common.css)
|
||||
content/browser/loop/shared/css/panel.css (content/shared/css/panel.css)
|
||||
content/browser/loop/shared/css/conversation.css (content/shared/css/conversation.css)
|
||||
content/browser/loop/shared/img/icon_32.png (content/shared/img/icon_32.png)
|
||||
content/browser/loop/shared/img/icon_64.png (content/shared/img/icon_64.png)
|
||||
content/browser/loop/shared/img/loading-icon.gif (content/shared/img/loading-icon.gif)
|
||||
content/browser/loop/shared/js/client.js (content/shared/js/client.js)
|
||||
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
|
||||
content/browser/loop/shared/js/router.js (content/shared/js/router.js)
|
||||
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
|
||||
content/browser/loop/shared/libs/lodash-2.4.1.js (content/shared/libs/lodash-2.4.1.js)
|
||||
content/browser/loop/shared/libs/jquery-2.1.0.js (content/shared/libs/jquery-2.1.0.js)
|
||||
content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
|
||||
content/browser/loop/shared/libs/sjcl-dev20140604.js (content/shared/libs/sjcl-dev20140604.js)
|
||||
content/browser/loop/shared/libs/token.js (content/shared/libs/token.js)
|
||||
content/browser/loop/shared/libs/hawk-browser-2.2.1.js (content/shared/libs/hawk-browser-2.2.1.js)
|
||||
content/browser/loop/libs/l10n.js (content/libs/l10n.js)
|
||||
content/browser/loop/js/desktopRouter.js (content/js/desktopRouter.js)
|
||||
content/browser/loop/js/conversation.js (content/js/conversation.js)
|
||||
content/browser/loop/js/panel.js (content/js/panel.js)
|
||||
# Partner SDK assets
|
||||
content/browser/loop/shared/libs/sdk.js (content/shared/libs/sdk.js)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/css/ot.min.css (content/shared/libs/otcdn/webrtc/v2.2.5/css/ot.min.css)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js (content/shared/libs/otcdn/webrtc/v2.2.5/js/dynamic_config.min.js)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-chrome.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-chrome.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-copy-firefox.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-copy-firefox.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-denied-firefox.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-denied-firefox.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-predenied-chrome.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-predenied-chrome.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/access-prompt-chrome.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/access-prompt-chrome.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/audioonly-publisher.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/audioonly-publisher.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/audioonly-subscriber.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/audioonly-subscriber.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/buttons.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/buttons.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/loader.gif (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/loader.gif)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/mic-off.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/mic-off.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/mic-on.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/mic-on.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/speaker-off.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/speaker-off.png)
|
||||
content/browser/loop/otcdn/webrtc/v2.2.5/images/rtc/speaker-on.png (content/shared/libs/otcdn/webrtc/v2.2.5/images/rtc/speaker-on.png)
|
22
browser/components/loop/manifest.ini
Normal file
@ -0,0 +1,22 @@
|
||||
; This file is in the main loop directory, and not the test directory as we need
|
||||
; to include the content/ files in what is packaged as test support files.
|
||||
;
|
||||
; Although the content/ files would normally be tested from within the browser,
|
||||
; the issue is that they are seen to be loaded in a "chrome" privilaged context,
|
||||
; and hence are not allowed to be loaded via the content files that run the
|
||||
; tests.
|
||||
;
|
||||
; Hence we duplicate the same files as support files, so that they can be loaded
|
||||
; in a content context.
|
||||
;
|
||||
; This might get messy if we start having pre-processed files, but for now it
|
||||
; seems to work.
|
||||
|
||||
[DEFAULT]
|
||||
b2g = false
|
||||
browser = true
|
||||
qemu = false
|
||||
|
||||
[test/shared/test_shared_all.py]
|
||||
[test/desktop-local/test_desktop_all.py]
|
||||
[test/standalone/test_standalone_all.py]
|
16
browser/components/loop/moz.build
Normal file
@ -0,0 +1,16 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
JS_MODULES_PATH = 'modules/loop'
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'MozLoopAPI.jsm',
|
||||
'MozLoopService.jsm',
|
||||
]
|
2
browser/components/loop/standalone/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
*.pyc
|
4
browser/components/loop/standalone/.jshintignore
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
content/shared/libs
|
||||
test/shared/vendor
|
||||
|
43
browser/components/loop/standalone/.jshintrc
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"browser" : true, // Web Browser (window, document, etc)
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"devel" : true, // Development/debugging (alert, confirm, etc)
|
||||
"immed" : false, // true: Require immediate invocations to be wrapped
|
||||
// in parens e.g. `(function () { } ());`
|
||||
"indent" : 2,
|
||||
"jquery": true,
|
||||
"latedef" : true, // true: Require variables to be defined before use
|
||||
// (helps avoid hoisting bugs)
|
||||
|
||||
"maxlen" : 80, // {int} Max number of characters per line
|
||||
"node" : true, // Node.js
|
||||
"strict": true, // Strict mode
|
||||
"trailing" : false, // true: Prohibit trailing whitespaces
|
||||
"undef" : true, // true: Require all non-global variables to be
|
||||
// declared (prevents global leaks)
|
||||
"unused" : "vars", // vars: Require all defined variables be used, but not
|
||||
// function parameters
|
||||
|
||||
"globals": {
|
||||
// chai global
|
||||
"chai": false,
|
||||
|
||||
// mocha globals
|
||||
"after": false,
|
||||
"afterEach": false,
|
||||
"before": false,
|
||||
"beforeEach": false,
|
||||
"describe": false,
|
||||
"it": false,
|
||||
|
||||
// firefox stuff not covered by "browser" option
|
||||
"indexedDB": true,
|
||||
"mozRTCIceCandidate": false,
|
||||
"mozRTCPeerConnection": false,
|
||||
"mozRTCSessionDescription": false,
|
||||
|
||||
"Backbone": false,
|
||||
|
||||
"_": false
|
||||
}
|
||||
}
|
24
browser/components/loop/standalone/Makefile
Normal file
@ -0,0 +1,24 @@
|
||||
# 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/.
|
||||
|
||||
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
|
||||
NODE_LOCAL_BIN=./node_modules/.bin
|
||||
|
||||
install:
|
||||
@npm install
|
||||
|
||||
test:
|
||||
@echo "Not implemented yet."
|
||||
|
||||
lint:
|
||||
@$(NODE_LOCAL_BIN)/jshint *.js content test
|
||||
|
||||
runserver: config
|
||||
@node server.js
|
||||
|
||||
frontend:
|
||||
@echo "Not implemented yet."
|
||||
|
||||
config:
|
||||
@echo "var loop = loop || {};\nloop.config = {serverUrl: '`echo $(LOOP_SERVER_URL)`'};" > content/config.js
|
50
browser/components/loop/standalone/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
Loop Client
|
||||
===========
|
||||
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
NodeJS and npm installed.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
$ make install
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
You will need to generate a configuration file, you can do so with:
|
||||
|
||||
$ make config
|
||||
|
||||
It will read the configuration from the `LOOP_SERVER_URL` env variable and
|
||||
generate the appropriate configuration file. This setting defines the root url
|
||||
of the loop server, without trailing slash.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
For development, run a local static file server:
|
||||
|
||||
$ make runserver
|
||||
|
||||
Then point your browser at:
|
||||
|
||||
- `http://localhost:3000/content/` for all public webapp contents,
|
||||
- `http://localhost:3000/test/` for tests.
|
||||
|
||||
**Note:** the provided static file server for web contents is **not** intended
|
||||
for production use.
|
||||
|
||||
Code linting
|
||||
------------
|
||||
|
||||
$ make lint
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
The Loop server code is released under the terms of the
|
||||
[Mozilla Public License v2.0](http://www.mozilla.org/MPL/2.0/). See the
|
||||
`LICENSE` file at the root of the repository.
|
11
browser/components/loop/standalone/content/css/webapp.css
Normal file
@ -0,0 +1,11 @@
|
||||
/* 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/. */
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
/* prevent the video convsersation elements to occupy the whole available
|
||||
width hence the height while keeping aspect ratio */
|
||||
max-width: 730px;
|
||||
margin: 0 auto;
|
||||
}
|
47
browser/components/loop/standalone/content/index.html
Normal file
@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Loop</title>
|
||||
<link rel="stylesheet" type="text/css" href="shared/css/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/css/conversation.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/webapp.css">
|
||||
<link rel="prefetch" type="application/l10n" href="l10n/data.ini">
|
||||
</head>
|
||||
<body onload="loop.webapp.init();">
|
||||
|
||||
<header>
|
||||
<h1>Loop</h1>
|
||||
</header>
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<div id="main"></div>
|
||||
|
||||
<!-- libs -->
|
||||
<script src="https://static.opentok.com/webrtc/v2.2/js/opentok.min.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/webl10n-20130617.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/jquery-2.1.0.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/lodash-2.4.1.js"></script>
|
||||
<script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
|
||||
|
||||
<!-- app scripts -->
|
||||
<script type="text/javascript" src="config.js"></script>
|
||||
<script type="text/javascript" src="shared/js/client.js"></script>
|
||||
<script type="text/javascript" src="shared/js/models.js"></script>
|
||||
<script type="text/javascript" src="shared/js/views.js"></script>
|
||||
<script type="text/javascript" src="shared/js/router.js"></script>
|
||||
<script type="text/javascript" src="js/webapp.js"></script>
|
||||
|
||||
<script>
|
||||
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
|
||||
window.addEventListener('localized', function() {
|
||||
document.documentElement.lang = document.webL10n.getLanguage();
|
||||
document.documentElement.dir = document.webL10n.getDirection();
|
||||
}, false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
230
browser/components/loop/standalone/content/js/webapp.js
Normal file
@ -0,0 +1,230 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.webapp = (function($, _, OT) {
|
||||
"use strict";
|
||||
|
||||
loop.config = loop.config || {};
|
||||
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
|
||||
|
||||
var sharedModels = loop.shared.models,
|
||||
sharedViews = loop.shared.views,
|
||||
baseServerUrl = loop.config.serverUrl;
|
||||
|
||||
/**
|
||||
* App router.
|
||||
* @type {loop.webapp.WebappRouter}
|
||||
*/
|
||||
var router;
|
||||
|
||||
/**
|
||||
* Homepage view.
|
||||
*/
|
||||
var HomeView = sharedViews.BaseView.extend({
|
||||
template: _.template('<p data-l10n-id="welcome"></p>')
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation launcher view. A ConversationModel is associated and attached
|
||||
* as a `model` property.
|
||||
*/
|
||||
var ConversationFormView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<form>',
|
||||
' <p>',
|
||||
' <button class="btn btn-success" data-l10n-id="start_call"></button>',
|
||||
' </p>',
|
||||
'</form>'
|
||||
].join("")),
|
||||
|
||||
events: {
|
||||
"submit": "initiate"
|
||||
},
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Required options:
|
||||
* - {loop.shared.model.ConversationModel} model Conversation model.
|
||||
* - {loop.shared.views.NotificationListView} notifier Notifier component.
|
||||
*
|
||||
* @param {Object} options Options object.
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
|
||||
if (!options.model) {
|
||||
throw new Error("missing required model");
|
||||
}
|
||||
this.model = options.model;
|
||||
|
||||
if (!options.notifier) {
|
||||
throw new Error("missing required notifier");
|
||||
}
|
||||
this.notifier = options.notifier;
|
||||
|
||||
this.listenTo(this.model, "session:error", this._onSessionError);
|
||||
},
|
||||
|
||||
_onSessionError: function(error) {
|
||||
console.error(error);
|
||||
this.notifier.errorL10n("unable_retrieve_call_info");
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables this form to prevent multiple submissions.
|
||||
*
|
||||
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=991126
|
||||
*/
|
||||
disableForm: function() {
|
||||
this.$("button").attr("disabled", "disabled");
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates the call.
|
||||
*
|
||||
* @param {SubmitEvent} event
|
||||
*/
|
||||
initiate: function(event) {
|
||||
event.preventDefault();
|
||||
this.model.initiate({
|
||||
baseServerUrl: baseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
this.disableForm();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Webapp Router.
|
||||
*/
|
||||
var WebappRouter = loop.shared.router.BaseConversationRouter.extend({
|
||||
routes: {
|
||||
"": "home",
|
||||
"unsupportedDevice": "unsupportedDevice",
|
||||
"unsupportedBrowser": "unsupportedBrowser",
|
||||
"call/ongoing/:token": "loadConversation",
|
||||
"call/:token": "initiate"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
// Load default view
|
||||
this.loadView(new HomeView());
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {loop.shared.router.BaseConversationRouter.startCall}
|
||||
*/
|
||||
startCall: function() {
|
||||
if (!this._conversation.get("loopToken")) {
|
||||
this._notifier.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
} else {
|
||||
this.navigate("call/ongoing/" + this._conversation.get("loopToken"), {
|
||||
trigger: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @override {loop.shared.router.BaseConversationRouter.endCall}
|
||||
*/
|
||||
endCall: function() {
|
||||
var route = "home";
|
||||
if (this._conversation.get("loopToken")) {
|
||||
route = "call/" + this._conversation.get("loopToken");
|
||||
}
|
||||
this.navigate(route, {trigger: true});
|
||||
},
|
||||
|
||||
/**
|
||||
* Default entry point.
|
||||
*/
|
||||
home: function() {
|
||||
this.loadView(new HomeView());
|
||||
},
|
||||
|
||||
unsupportedDevice: function() {
|
||||
this.loadView(new sharedViews.UnsupportedDeviceView());
|
||||
},
|
||||
|
||||
unsupportedBrowser: function() {
|
||||
this.loadView(new sharedViews.UnsupportedBrowserView());
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads conversation launcher view, setting the received conversation token
|
||||
* to the current conversation model. If a session is currently established,
|
||||
* terminates it first.
|
||||
*
|
||||
* @param {String} loopToken Loop conversation token.
|
||||
*/
|
||||
initiate: function(loopToken) {
|
||||
// Check if a session is ongoing; if so, terminate it
|
||||
if (this._conversation.get("ongoing")) {
|
||||
this._conversation.endSession();
|
||||
}
|
||||
this._conversation.set("loopToken", loopToken);
|
||||
this.loadView(new ConversationFormView({
|
||||
model: this._conversation,
|
||||
notifier: this._notifier
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads conversation establishment view.
|
||||
*
|
||||
*/
|
||||
loadConversation: function(loopToken) {
|
||||
if (!this._conversation.isSessionReady()) {
|
||||
// User has loaded this url directly, actually setup the call.
|
||||
return this.navigate("call/" + loopToken, {trigger: true});
|
||||
}
|
||||
this.loadView(new sharedViews.ConversationView({
|
||||
sdk: OT,
|
||||
model: this._conversation
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Local helpers.
|
||||
*/
|
||||
function WebappHelper() {
|
||||
this._iOSRegex = /^(iPad|iPhone|iPod)/;
|
||||
}
|
||||
|
||||
WebappHelper.prototype.isIOS = function isIOS(platform) {
|
||||
return this._iOSRegex.test(platform);
|
||||
};
|
||||
|
||||
/**
|
||||
* App initialization.
|
||||
*/
|
||||
function init() {
|
||||
var helper = new WebappHelper();
|
||||
router = new WebappRouter({
|
||||
conversation: new sharedModels.ConversationModel({}, {sdk: OT}),
|
||||
notifier: new sharedViews.NotificationListView({el: "#messages"})
|
||||
});
|
||||
Backbone.history.start();
|
||||
if (helper.isIOS(navigator.platform)) {
|
||||
router.navigate("unsupportedDevice", {trigger: true});
|
||||
} else if (!OT.checkSystemRequirements()) {
|
||||
router.navigate("unsupportedBrowser", {trigger: true});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
baseServerUrl: baseServerUrl,
|
||||
ConversationFormView: ConversationFormView,
|
||||
HomeView: HomeView,
|
||||
WebappHelper: WebappHelper,
|
||||
init: init,
|
||||
WebappRouter: WebappRouter
|
||||
};
|
||||
})(jQuery, _, window.OT);
|
31
browser/components/loop/standalone/content/l10n/data.ini
Normal file
@ -0,0 +1,31 @@
|
||||
[en]
|
||||
call_has_ended=Your call has ended.
|
||||
missing_conversation_info=Missing conversation information.
|
||||
network_disconnected=The network connection terminated abruptly.
|
||||
peer_ended_conversation=Your peer ended the conversation.
|
||||
unable_retrieve_call_info=Unable to retrieve conversation information.
|
||||
stop=Stop
|
||||
start_call=Start the call
|
||||
welcome=Welcome to the Loop web client.
|
||||
incompatible_browser=Incompatible Browser
|
||||
powered_by_webrtc=The audio and video components of Loop are powered by WebRTC.
|
||||
use_latest_firefox.innerHTML=To use Loop, please use the latest version of <a href="{{ff_url}}">Firefox</a>.
|
||||
incompatible_device=Incompatible device
|
||||
sorry_device_unsupported=Sorry, Loop does not currently support your device.
|
||||
use_firefox_windows_mac_linux=Please open this page using the latest Firefox on Windows, Android, Mac or Linux.
|
||||
|
||||
[fr]
|
||||
call_has_ended=L'appel est terminé.
|
||||
missing_conversation_info=Informations de communication manquantes.
|
||||
network_disconnected=La connexion réseau semble avoir été interrompue.
|
||||
peer_ended_conversation=Votre correspondant a mis fin à la communication.
|
||||
unable_retrieve_call_info=Impossible de récupérer les informations liées à cet appel.
|
||||
stop=Arrêter
|
||||
start_call=Démarrer l'appel
|
||||
welcome=Bienvenue sur Loop.
|
||||
incompatible_browser=Navigateur non supporté
|
||||
powered_by_webrtc=Les fonctionnalités audio et vidéo de Loop utilisent WebRTC.
|
||||
use_latest_firefox.innerHTML=Pour utiliser Loop, merci d'utiliser la dernière version de <a href="{{ff_url}}">Firefox</a>.
|
||||
incompatible_device=Plateforme non supportée
|
||||
sorry_device_unsupported=Désolé, Loop ne fonctionne actuellement pas sur votre appareil.
|
||||
use_firefox_windows_mac_linux=Merci d'ouvrir cette page avec une version récente de Firefox pour Windows, Android, Mac ou Linux.
|
19
browser/components/loop/standalone/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Loop-client",
|
||||
"description": "Video conferencing app powered by WebRTC.",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"node": "0.10.x",
|
||||
"npm":"1.3.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "3.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jshint": "2.x"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make test",
|
||||
"start": "make runserver"
|
||||
}
|
||||
}
|
17
browser/components/loop/standalone/server.js
Normal file
@ -0,0 +1,17 @@
|
||||
/* 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 express = require('express');
|
||||
var app = express();
|
||||
|
||||
// This lets /test/ be mapped to the right place for running tests
|
||||
app.use(express.static(__dirname + '/../'));
|
||||
// This lets /content/ be mappy right for the static contents.
|
||||
app.use(express.static(__dirname + '/'));
|
||||
|
||||
app.listen(3000);
|
||||
console.log("Serving repository root over HTTP at http://localhost:3000/");
|
||||
console.log("Static contents are available at http://localhost:3000/content/");
|
||||
console.log("Tests are viewable at http://localhost:3000/test/");
|
||||
console.log("Use this for development only.");
|
21
browser/components/loop/test/desktop-local/README.md
Normal file
@ -0,0 +1,21 @@
|
||||
== Mocha unit tests ==
|
||||
|
||||
These unit tests use the browser build of the [Mocha test framework][1]
|
||||
and the Chai Assertion Library's [BDD interface][2].
|
||||
|
||||
[1]: http://visionmedia.github.io/mocha/
|
||||
[2]: http://chaijs.com/api/bdd/
|
||||
|
||||
Aim your browser at the index.html in this directory on your localhost using
|
||||
a file: or HTTP URL to run the tests. Alternately, from the top-level of your
|
||||
Gecko source directory, execute:
|
||||
|
||||
```
|
||||
./mach marionette-test browser/components/loop/test/manifest.ini
|
||||
```
|
||||
|
||||
Next steps:
|
||||
|
||||
* run using JS http server so the property security context for DOM elements
|
||||
is used
|
||||
|
215
browser/components/loop/test/desktop-local/conversation_test.js
Normal file
@ -0,0 +1,215 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop, sinon */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.conversation", function() {
|
||||
"use strict";
|
||||
|
||||
var ConversationRouter = loop.conversation.ConversationRouter,
|
||||
sandbox,
|
||||
notifier;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
notifier = {
|
||||
notify: sandbox.spy(),
|
||||
warn: sandbox.spy(),
|
||||
warnL10n: sandbox.spy(),
|
||||
error: sandbox.spy(),
|
||||
errorL10n: sandbox.spy()
|
||||
};
|
||||
|
||||
window.navigator.mozLoop = {
|
||||
get serverUrl() {
|
||||
return "http://example.com";
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete window.navigator.mozLoop;
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("ConversationRouter", function() {
|
||||
var conversation;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new loop.shared.models.ConversationModel({}, {sdk: {}});
|
||||
sandbox.stub(conversation, "initiate");
|
||||
});
|
||||
|
||||
describe("Routes", function() {
|
||||
var router;
|
||||
|
||||
beforeEach(function() {
|
||||
router = new ConversationRouter({
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
sandbox.stub(router, "loadView");
|
||||
});
|
||||
|
||||
describe("#incoming", function() {
|
||||
it("should set the loopVersion on the conversation model", function() {
|
||||
router.incoming("fakeVersion");
|
||||
|
||||
expect(conversation.get("loopVersion")).to.equal("fakeVersion");
|
||||
});
|
||||
|
||||
it("should display the incoming call view", function() {
|
||||
router.incoming("fakeVersion");
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWithExactly(router.loadView,
|
||||
sinon.match.instanceOf(loop.conversation.IncomingCallView));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#accept", function() {
|
||||
it("should initiate the conversation", function() {
|
||||
router.accept();
|
||||
|
||||
sinon.assert.calledOnce(conversation.initiate);
|
||||
sinon.assert.calledWithExactly(conversation.initiate, {
|
||||
baseServerUrl: "http://example.com",
|
||||
outgoing: false
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#conversation", function() {
|
||||
it("should load the ConversationView if session is set", function() {
|
||||
sandbox.stub(loop.shared.views.ConversationView.prototype,
|
||||
"initialize");
|
||||
conversation.set("sessionId", "fakeSessionId");
|
||||
|
||||
router.conversation();
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWith(router.loadView,
|
||||
sinon.match.instanceOf(loop.shared.views.ConversationView));
|
||||
});
|
||||
|
||||
it("should not load the ConversationView if session is not set",
|
||||
function() {
|
||||
router.conversation();
|
||||
|
||||
sinon.assert.notCalled(router.loadView);
|
||||
});
|
||||
|
||||
it("should notify the user when session is not set",
|
||||
function() {
|
||||
router.conversation();
|
||||
|
||||
sinon.assert.calledOnce(router._notifier.errorL10n);
|
||||
sinon.assert.calledWithExactly(router._notifier.errorL10n,
|
||||
"cannot_start_call_session_not_ready");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#ended", function() {
|
||||
// XXX When the call is ended gracefully, we should check that we
|
||||
// close connections nicely
|
||||
it("should close the window");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
var router, fakeSessionData;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeSessionData = {
|
||||
sessionId: "sessionId",
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey"
|
||||
};
|
||||
sandbox.stub(loop.conversation.ConversationRouter.prototype,
|
||||
"navigate");
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
router = new loop.conversation.ConversationRouter({
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
});
|
||||
|
||||
it("should navigate to call/ongoing once the call session is ready",
|
||||
function() {
|
||||
conversation.setReady(fakeSessionData);
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ongoing");
|
||||
});
|
||||
|
||||
it("should navigate to call/ended when the call session ends",
|
||||
function() {
|
||||
conversation.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
});
|
||||
|
||||
it("should navigate to call/ended when peer hangs up", function() {
|
||||
conversation.trigger("session:peer-hungup");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
});
|
||||
|
||||
it("should navigate to call/{token} when network disconnects",
|
||||
function() {
|
||||
conversation.trigger("session:network-disconnected");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EndedCallView", function() {
|
||||
describe("#closeWindow", function() {
|
||||
it("should close the conversation window", function() {
|
||||
sandbox.stub(window, "close");
|
||||
var view = new loop.conversation.EndedCallView();
|
||||
|
||||
view.closeWindow({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(window.close);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("IncomingCallView", function() {
|
||||
var conversation, view;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new loop.shared.models.ConversationModel({}, {sdk: {}});
|
||||
view = new loop.conversation.IncomingCallView({model: conversation});
|
||||
});
|
||||
|
||||
describe("#handleAccept", function() {
|
||||
it("should trigger an 'accept' conversation model event" ,
|
||||
function(done) {
|
||||
conversation.once("accept", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
view.handleAccept({preventDefault: sandbox.spy()});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#handleDecline", function() {
|
||||
it("should close the window", function() {
|
||||
sandbox.stub(window, "close");
|
||||
|
||||
view.handleDecline({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(window.close);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
51
browser/components/loop/test/desktop-local/index.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Loop desktop-local mocha tests</title>
|
||||
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha">
|
||||
<p><a href="../">Index</a></p>
|
||||
</div>
|
||||
<div id="messages"></div>
|
||||
<div id="fixtures"></div>
|
||||
<!-- libs -->
|
||||
<script src="../../content/libs/l10n.js"></script>
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<script src="../shared/vendor/mocha-1.17.1.js"></script>
|
||||
<script src="../shared/vendor/chai-1.9.0.js"></script>
|
||||
<script src="../shared/vendor/sinon-1.9.0.js"></script>
|
||||
<script>
|
||||
/*global chai,mocha */
|
||||
chai.Assertion.includeStack = true;
|
||||
mocha.setup('bdd');
|
||||
</script>
|
||||
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/client.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/js/desktopRouter.js"></script>
|
||||
<script src="../../content/js/conversation.js"></script>
|
||||
<script src="../../content/js/panel.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="conversation_test.js"></script>
|
||||
<script src="panel_test.js"></script>
|
||||
<script>
|
||||
mocha.run(function () {
|
||||
$("#mocha").append("<p id='complete'>Complete.</p>");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
327
browser/components/loop/test/desktop-local/panel_test.js
Normal file
@ -0,0 +1,327 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*global loop, sinon */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.panel", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox, notifier, fakeXHR, requests = [], savedMozLoop;
|
||||
|
||||
function createTestRouter(fakeDocument) {
|
||||
return new loop.panel.PanelRouter({
|
||||
notifier: notifier,
|
||||
document: fakeDocument
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
notifier = {
|
||||
clear: sandbox.spy(),
|
||||
notify: sandbox.spy(),
|
||||
warn: sandbox.spy(),
|
||||
warnL10n: sandbox.spy(),
|
||||
error: sandbox.spy(),
|
||||
errorL10n: sandbox.spy()
|
||||
};
|
||||
|
||||
navigator.mozLoop = {
|
||||
doNotDisturb: true,
|
||||
get serverUrl() {
|
||||
return "http://example.com";
|
||||
},
|
||||
getStrings: function() {
|
||||
return "{}";
|
||||
},
|
||||
get locale() {
|
||||
return "en-US";
|
||||
}
|
||||
};
|
||||
document.mozL10n.initialize(navigator.mozLoop);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
delete navigator.mozLoop;
|
||||
$("#fixtures").empty();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("loop.panel.PanelRouter", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a notifier", function() {
|
||||
expect(function() {
|
||||
new loop.panel.PanelRouter();
|
||||
}).to.Throw(Error, /missing required notifier/);
|
||||
});
|
||||
|
||||
it("should require a document", function() {
|
||||
expect(function() {
|
||||
new loop.panel.PanelRouter({notifier: notifier});
|
||||
}).to.Throw(Error, /missing required document/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
var router;
|
||||
|
||||
beforeEach(function() {
|
||||
router = createTestRouter({
|
||||
hidden: true,
|
||||
addEventListener: sandbox.spy()
|
||||
});
|
||||
|
||||
sandbox.stub(router, "loadView");
|
||||
});
|
||||
|
||||
describe("#home", function() {
|
||||
it("should reset the PanelView", function() {
|
||||
sandbox.stub(router, "reset");
|
||||
|
||||
router.home();
|
||||
|
||||
sinon.assert.calledOnce(router.reset);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#reset", function() {
|
||||
it("should clear all pending notifications", function() {
|
||||
router.reset();
|
||||
|
||||
sinon.assert.calledOnce(notifier.clear);
|
||||
});
|
||||
|
||||
it("should load the home view", function() {
|
||||
router.reset();
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWithExactly(router.loadView,
|
||||
sinon.match.instanceOf(loop.panel.PanelView));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
it("should listen to document visibility changes", function() {
|
||||
var fakeDocument = {
|
||||
hidden: true,
|
||||
addEventListener: sandbox.spy()
|
||||
};
|
||||
|
||||
var router = createTestRouter(fakeDocument);
|
||||
|
||||
sinon.assert.calledOnce(fakeDocument.addEventListener);
|
||||
sinon.assert.calledWith(fakeDocument.addEventListener,
|
||||
"visibilitychange");
|
||||
});
|
||||
|
||||
it("should trigger panel:open when the panel document is visible",
|
||||
function(done) {
|
||||
var router = createTestRouter({
|
||||
hidden: false,
|
||||
addEventListener: function(name, cb) {
|
||||
setTimeout(function() {
|
||||
cb({currentTarget: {hidden: false}});
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
router.once("panel:open", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should trigger panel:closed when the panel document is hidden",
|
||||
function(done) {
|
||||
var router = createTestRouter({
|
||||
addEventListener: function(name, cb) {
|
||||
hidden: true,
|
||||
setTimeout(function() {
|
||||
cb({currentTarget: {hidden: true}});
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
router.once("panel:closed", function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop.panel.DoNotDisturbView", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
$("#fixtures").append('<div id="dnd-view"></div>');
|
||||
view = new loop.panel.DoNotDisturbView({el: $("#dnd-view")});
|
||||
});
|
||||
|
||||
describe("#toggle", function() {
|
||||
beforeEach(function() {
|
||||
navigator.mozLoop.doNotDisturb = false;
|
||||
});
|
||||
|
||||
it("should toggle the value of mozLoop.doNotDisturb", function() {
|
||||
view.toggle();
|
||||
|
||||
expect(navigator.mozLoop.doNotDisturb).eql(true);
|
||||
});
|
||||
|
||||
it("should update the DnD checkbox value", function() {
|
||||
view.toggle();
|
||||
|
||||
expect(view.$("input").is(":checked")).eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("render", function() {
|
||||
it("should check the dnd checkbox when dnd is enabled", function() {
|
||||
navigator.mozLoop.doNotDisturb = false;
|
||||
|
||||
view.render();
|
||||
|
||||
expect(view.$("input").is(":checked")).eql(false);
|
||||
});
|
||||
|
||||
it("should uncheck the dnd checkbox when dnd is disabled", function() {
|
||||
navigator.mozLoop.doNotDisturb = true;
|
||||
|
||||
view.render();
|
||||
|
||||
expect(view.$("input").is(":checked")).eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop.panel.PanelView", function() {
|
||||
beforeEach(function() {
|
||||
$("#fixtures").append('<div id="messages"></div><div id="main"></div>');
|
||||
});
|
||||
|
||||
describe("#getCallUrl", function() {
|
||||
it("should reset all pending notifications", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallUrl");
|
||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(view.notifier.clear, "clear");
|
||||
});
|
||||
|
||||
it("should request a call url to the server", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallUrl");
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
sandbox.stub(view, "getNickname").returns("foo");
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(requestCallUrl);
|
||||
sinon.assert.calledWith(requestCallUrl, "foo");
|
||||
});
|
||||
|
||||
it("should set the call url form in a pending state", function() {
|
||||
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallUrl");
|
||||
sandbox.stub(loop.panel.PanelView.prototype, "setPending");
|
||||
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(view.setPending);
|
||||
});
|
||||
|
||||
it("should clear the pending state when a response is received",
|
||||
function() {
|
||||
sandbox.stub(loop.panel.PanelView.prototype,
|
||||
"clearPending");
|
||||
var requestCallUrl = sandbox.stub(
|
||||
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
cb("fake error");
|
||||
});
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(view.clearPending);
|
||||
});
|
||||
|
||||
it("should notify the user when the operation failed", function() {
|
||||
var requestCallUrl = sandbox.stub(
|
||||
loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
|
||||
cb("fake error");
|
||||
});
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
|
||||
view.getCallUrl({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(view.notifier.errorL10n);
|
||||
sinon.assert.calledWithExactly(view.notifier.errorL10n,
|
||||
"unable_retrieve_url");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onCallUrlReceived", function() {
|
||||
var callUrlData;
|
||||
|
||||
beforeEach(function() {
|
||||
callUrlData = {
|
||||
call_url: "http://call.me/",
|
||||
expiresAt: 1000
|
||||
};
|
||||
});
|
||||
|
||||
it("should update the text field with the call url", function() {
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
view.render();
|
||||
|
||||
view.onCallUrlReceived(callUrlData);
|
||||
|
||||
expect(view.$("#call-url").val()).eql("http://call.me/");
|
||||
});
|
||||
|
||||
it("should reset all pending notifications", function() {
|
||||
var view = new loop.panel.PanelView({notifier: notifier}).render();
|
||||
|
||||
view.onCallUrlReceived(callUrlData);
|
||||
|
||||
sinon.assert.calledOnce(view.notifier.clear);
|
||||
});
|
||||
});
|
||||
|
||||
describe("events", function() {
|
||||
describe("goBack", function() {
|
||||
it("should update the button state");
|
||||
});
|
||||
|
||||
describe("changeButtonState", function() {
|
||||
it("should do set the disabled state if there is no text");
|
||||
it("should do set the enabled state if there is text");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#render", function() {
|
||||
it("should render a DoNotDisturbView", function() {
|
||||
var renderDnD = sandbox.stub(loop.panel.DoNotDisturbView.prototype,
|
||||
"render");
|
||||
var view = new loop.panel.PanelView({notifier: notifier});
|
||||
|
||||
view.render();
|
||||
|
||||
sinon.assert.calledOnce(renderDnD);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,16 @@
|
||||
# need to get this dir in the path so that we make the import work
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'shared'))
|
||||
|
||||
from frontend_tester import BaseTestFrontendUnits
|
||||
|
||||
|
||||
class TestDesktopUnits(BaseTestFrontendUnits):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDesktopUnits, self).setUp()
|
||||
self.set_server_prefix("../desktop-local/")
|
||||
|
||||
def test_units(self):
|
||||
self.check_page("index.html")
|
18
browser/components/loop/test/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Loop test index</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Loop tests</h1>
|
||||
<ul>
|
||||
<li><a href="shared/">Shared tests</a></li>
|
||||
<li><a href="desktop-local/">Local tests</a></li>
|
||||
<li><a href="standalone/">Standalone tests</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
291
browser/components/loop/test/shared/client_test.js
Normal file
@ -0,0 +1,291 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/*global loop, sinon, it, beforeEach, afterEach, describe, hawk */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.shared.Client", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox,
|
||||
fakeXHR,
|
||||
requests = [],
|
||||
callback,
|
||||
mozLoop,
|
||||
fakeToken;
|
||||
|
||||
var fakeErrorRes = JSON.stringify({
|
||||
status: "errors",
|
||||
errors: [{
|
||||
location: "url",
|
||||
name: "token",
|
||||
description: "invalid token"
|
||||
}]
|
||||
});
|
||||
|
||||
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.shared.Client", function() {
|
||||
describe("#constructor", function() {
|
||||
it("should require a baseServerUrl setting", function() {
|
||||
expect(function() {
|
||||
new loop.shared.Client();
|
||||
}).to.Throw(Error, /required/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallUrl", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
window.navigator.mozLoop = {
|
||||
ensureRegistered: sinon.stub().callsArgWith(0, null),
|
||||
noteCallUrlExpiry: sinon.spy(),
|
||||
getLoopCharPref: sandbox.stub()
|
||||
.returns(null)
|
||||
.withArgs("hawk-session-token")
|
||||
.returns(fakeToken)
|
||||
};
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: window.navigator.mozLoop}
|
||||
);
|
||||
});
|
||||
|
||||
it("should ensure loop is registered", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.ensureRegistered);
|
||||
});
|
||||
|
||||
it("should send an error when registration fails", function() {
|
||||
navigator.mozLoop.ensureRegistered.callsArgWith(0, "offline");
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
sinon.assert.calledOnce(callback);
|
||||
sinon.assert.calledWithExactly(callback, "offline");
|
||||
});
|
||||
|
||||
it("should post to /call-url/", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].method).to.be.equal("POST");
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/call-url/");
|
||||
expect(requests[0].requestBody).to.be.equal('callerId=foo');
|
||||
});
|
||||
|
||||
it("should set the XHR Authorization header", function() {
|
||||
sandbox.stub(hawk.client, "header").returns( {field: fakeToken} );
|
||||
client._credentials = {
|
||||
// XXX we probably really want to stub out external module calls
|
||||
// eg deriveHawkCredentials, rather supplying them with valid arguments
|
||||
// like we're doing here:
|
||||
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
|
||||
algorithm: 'sha256',
|
||||
user: 'Steve'
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests[0].requestHeaders.Authorization).to.equal(fakeToken);
|
||||
});
|
||||
|
||||
it("should request a call url", function() {
|
||||
var callUrlData = {
|
||||
"call_url": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(callUrlData));
|
||||
|
||||
sinon.assert.calledWithExactly(callback, null, callUrlData);
|
||||
});
|
||||
|
||||
it("should note the call url expiry", function() {
|
||||
var callUrlData = {
|
||||
"call_url": "fakeCallUrl",
|
||||
"expiresAt": 60
|
||||
};
|
||||
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
JSON.stringify(callUrlData));
|
||||
|
||||
// expiresAt is in hours, and noteCallUrlExpiry wants seconds.
|
||||
sinon.assert.calledWithExactly(navigator.mozLoop.noteCallUrlExpiry,
|
||||
60 * 60 * 60);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallUrl("foo", callback);
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": {}}');
|
||||
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#requestCallsInfo", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
mozLoop = {
|
||||
getLoopCharPref: sandbox.stub()
|
||||
.returns(null)
|
||||
.withArgs("hawk-session-token")
|
||||
.returns(fakeToken)
|
||||
};
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: mozLoop}
|
||||
);
|
||||
});
|
||||
|
||||
it("should prevent launching a conversation when version is missing",
|
||||
function() {
|
||||
expect(function() {
|
||||
client.requestCallsInfo();
|
||||
}).to.Throw(Error, /missing required parameter version/);
|
||||
});
|
||||
|
||||
it("should request data for all calls", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
expect(requests).to.have.length.of(1);
|
||||
expect(requests[0].url).to.be.equal("http://fake.api/calls?version=42");
|
||||
expect(requests[0].method).to.be.equal("GET");
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"calls": [{"apiKey": "fake"}]}');
|
||||
sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]);
|
||||
});
|
||||
|
||||
it("should set the XHR Authorization header", function() {
|
||||
sandbox.stub(hawk.client, "header").returns( {field: fakeToken} );
|
||||
// XXX we probably really want to stub out external module calls
|
||||
// eg deriveHawkCredentials, rather supplying them with valid arguments
|
||||
// like we're doing here:
|
||||
client._credentials = {
|
||||
key: 'werxhqb98rpaxn39848xrunpaw3489ruxnpa98w4rxn',
|
||||
algorithm: 'sha256',
|
||||
user: 'Steve'
|
||||
};
|
||||
|
||||
client.requestCallsInfo("foo", callback);
|
||||
|
||||
expect(requests[0].requestHeaders.Authorization).to.equal(fakeToken);
|
||||
});
|
||||
|
||||
it("should send an error when the request fails", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallsInfo(42, callback);
|
||||
|
||||
requests[0].respond(200, {"Content-Type": "application/json"},
|
||||
'{"bad": {}}');
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /Invalid data received/.test(err.message);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("requestCallInfo", function() {
|
||||
var client;
|
||||
|
||||
beforeEach(function() {
|
||||
client = new loop.shared.Client(
|
||||
{baseServerUrl: "http://fake.api", mozLoop: undefined}
|
||||
);
|
||||
});
|
||||
|
||||
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", 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");
|
||||
});
|
||||
|
||||
it("should receive call data for the given call", function() {
|
||||
client.requestCallInfo("fake", 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", callback);
|
||||
|
||||
requests[0].respond(400, {"Content-Type": "application/json"},
|
||||
fakeErrorRes);
|
||||
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
|
||||
return /400.*invalid token/.test(err.message);
|
||||
}));
|
||||
});
|
||||
|
||||
it("should send an error if the data is not valid", function() {
|
||||
client.requestCallInfo("fake", 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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
117
browser/components/loop/test/shared/frontend_tester.py
Normal file
@ -0,0 +1,117 @@
|
||||
from marionette_test import MarionetteTestCase
|
||||
import threading
|
||||
import SimpleHTTPServer
|
||||
import SocketServer
|
||||
import BaseHTTPServer
|
||||
import socket
|
||||
import urllib
|
||||
import urlparse
|
||||
import os
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
# XXX Once we're on a branch with bug 993478 landed, we may want to get
|
||||
# rid of this HTTP server and just use the built-in one from Marionette,
|
||||
# since there will less code to maintain, and it will be faster. We'll
|
||||
# need to consider whether this code wants to be shared with WebDriver tests
|
||||
# for other browsers, though.
|
||||
#
|
||||
class ThreadingSimpleServer(SocketServer.ThreadingMixIn,
|
||||
BaseHTTPServer.HTTPServer):
|
||||
pass
|
||||
|
||||
|
||||
class QuietHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class BaseTestFrontendUnits(MarionetteTestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseTestFrontendUnits, cls).setUpClass()
|
||||
|
||||
if DEBUG:
|
||||
handler = SimpleHTTPServer.SimpleHTTPRequestHandler
|
||||
else:
|
||||
handler = QuietHttpRequestHandler
|
||||
|
||||
# Port 0 means to select an arbitrary unused port
|
||||
cls.server = ThreadingSimpleServer(('', 0), handler)
|
||||
cls.ip, cls.port = cls.server.server_address
|
||||
|
||||
cls.server_thread = threading.Thread(target=cls.server.serve_forever)
|
||||
cls.server_thread.daemon = False
|
||||
cls.server_thread.start()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.server.shutdown()
|
||||
cls.server_thread.join()
|
||||
|
||||
# make sure everything gets GCed so it doesn't interfere with the next
|
||||
# test class. Even though this is class-static, each subclass gets
|
||||
# its own instance of this stuff.
|
||||
cls.server_thread = None
|
||||
cls.server = None
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTestFrontendUnits, self).setUp()
|
||||
|
||||
# This extends the timeout for find_element to 10 seconds.
|
||||
# We need this as the tests take an amount of time to run after loading,
|
||||
# which we have to wait for.
|
||||
self.marionette.set_search_timeout(10000)
|
||||
|
||||
# srcdir_path should be the directory relative to this file.
|
||||
def set_server_prefix(self, srcdir_path):
|
||||
# We may be run from a different path than topsrcdir, e.g. in the case
|
||||
# of packaged tests. If so, then we have to work out the right directory
|
||||
# for the local server.
|
||||
|
||||
# First find the top of the working directory.
|
||||
commonPath = os.path.commonprefix([__file__, os.getcwd()])
|
||||
|
||||
# Now get the relative path between the two
|
||||
relPath = os.path.relpath(os.path.dirname(__file__), commonPath)
|
||||
|
||||
relPath = urllib.pathname2url(os.path.join(relPath, srcdir_path))
|
||||
|
||||
# Finally join the relative path with the given src path
|
||||
self.server_prefix = urlparse.urljoin("http://localhost:" + str(self.port),
|
||||
relPath)
|
||||
|
||||
def check_page(self, page):
|
||||
|
||||
self.marionette.navigate(urlparse.urljoin(self.server_prefix, page))
|
||||
self.marionette.find_element("id", 'complete')
|
||||
|
||||
fail_node = self.marionette.find_element("css selector",
|
||||
'.failures > em')
|
||||
if fail_node.text == "0":
|
||||
return
|
||||
|
||||
# This may want to be in a more general place triggerable by an env
|
||||
# var some day if it ends up being something we need often:
|
||||
#
|
||||
# If you have browser-based unit tests which work when loaded manually
|
||||
# but not from marionette, uncomment the two lines below to break
|
||||
# on failing tests, so that the browsers won't be torn down, and you
|
||||
# can use the browser debugging facilities to see what's going on.
|
||||
#from ipdb import set_trace
|
||||
#set_trace()
|
||||
|
||||
raise AssertionError(self.get_failure_details())
|
||||
|
||||
def get_failure_details(self):
|
||||
fail_nodes = self.marionette.find_elements("css selector",
|
||||
'.test.fail')
|
||||
details = ["%d failure(s) encountered:" % len(fail_nodes)]
|
||||
for node in fail_nodes:
|
||||
details.append(
|
||||
node.find_element("tag name", 'h2').text.split("\n")[0])
|
||||
details.append(
|
||||
node.find_element("css selector", '.error').text)
|
||||
return "\n".join(details)
|
54
browser/components/loop/test/shared/index.html
Normal file
@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Loop shared mocha tests</title>
|
||||
<link rel="stylesheet" media="all" href="vendor/mocha-1.17.1.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha">
|
||||
<p><a href="../">Index</a></p>
|
||||
</div>
|
||||
<div id="messages"></div>
|
||||
<div id="fixtures"></div>
|
||||
|
||||
<!-- libs -->
|
||||
<script src="../../content/shared/libs/webl10n-20130617.js"></script>
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
<script src="../../content/shared/libs/sjcl-dev20140604.js"></script>
|
||||
<script src="../../content/shared/libs/token.js"></script>
|
||||
<script src="../../content/shared/libs/hawk-browser-2.2.1.js"></script>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<script src="vendor/mocha-1.17.1.js"></script>
|
||||
<script src="vendor/chai-1.9.0.js"></script>
|
||||
<script src="vendor/sinon-1.9.0.js"></script>
|
||||
<script>
|
||||
/*global chai, mocha */
|
||||
chai.Assertion.includeStack = true;
|
||||
mocha.setup('bdd');
|
||||
</script>
|
||||
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/client.js"></script>
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
|
||||
<!-- Test scripts -->
|
||||
<script src="client_test.js"></script>
|
||||
<script src="models_test.js"></script>
|
||||
<script src="views_test.js"></script>
|
||||
<script src="router_test.js"></script>
|
||||
<script>
|
||||
mocha.run(function () {
|
||||
$("#mocha").append("<p id='complete'>Complete.</p>");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
268
browser/components/loop/test/shared/models_test.js
Normal file
@ -0,0 +1,268 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop, sinon */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.shared.models", function() {
|
||||
"use strict";
|
||||
|
||||
var sharedModels = loop.shared.models,
|
||||
sandbox, fakeXHR, requests = [], fakeSDK, fakeSession, fakeSessionData;
|
||||
|
||||
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);
|
||||
};
|
||||
fakeSessionData = {
|
||||
sessionId: "sessionId",
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey"
|
||||
};
|
||||
fakeSession = _.extend({
|
||||
connect: sandbox.spy(),
|
||||
disconnect: sandbox.spy(),
|
||||
unpublish: sandbox.spy()
|
||||
}, Backbone.Events);
|
||||
fakeSDK = {
|
||||
initPublisher: sandbox.spy(),
|
||||
initSession: sandbox.stub().returns(fakeSession)
|
||||
};
|
||||
});
|
||||
|
||||
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, reqCallInfoStub, reqCallsInfoStub, fakeBaseServerUrl;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {sdk: fakeSDK});
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
fakeBaseServerUrl = "http://fakeBaseServerUrl";
|
||||
reqCallInfoStub = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallInfo");
|
||||
reqCallsInfoStub = sandbox.stub(loop.shared.Client.prototype,
|
||||
"requestCallsInfo");
|
||||
});
|
||||
|
||||
describe("#initiate", function() {
|
||||
it("call requestCallInfo on the client for outgoing calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(reqCallInfoStub);
|
||||
sinon.assert.calledWith(reqCallInfoStub, "fakeToken");
|
||||
});
|
||||
|
||||
it("should not call requestCallsInfo on the client for outgoing calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(reqCallsInfoStub);
|
||||
});
|
||||
|
||||
it("call requestCallsInfo on the client for incoming calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(reqCallsInfoStub);
|
||||
sinon.assert.calledWith(reqCallsInfoStub);
|
||||
});
|
||||
|
||||
it("should not call requestCallInfo on the client for incoming calls",
|
||||
function() {
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(reqCallInfoStub);
|
||||
});
|
||||
|
||||
it("should update conversation session information from server data",
|
||||
function() {
|
||||
sandbox.stub(conversation, "setReady");
|
||||
reqCallInfoStub.callsArgWith(1, null, fakeSessionData);
|
||||
|
||||
conversation.initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(conversation.setReady);
|
||||
sinon.assert.calledWith(conversation.setReady, fakeSessionData);
|
||||
});
|
||||
|
||||
it("should trigger a `session:error` on failure", function(done) {
|
||||
reqCallInfoStub.callsArgWith(1,
|
||||
new Error("failed: HTTP 400 Bad Request; fake"));
|
||||
|
||||
conversation.on("session:error", function(err) {
|
||||
expect(err.message).to.match(/failed: HTTP 400 Bad Request; fake/);
|
||||
done();
|
||||
}).initiate({
|
||||
baseServerUrl: fakeBaseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setReady", function() {
|
||||
it("should update conversation session information", function() {
|
||||
conversation.setReady(fakeSessionData);
|
||||
|
||||
expect(conversation.get("sessionId")).eql("sessionId");
|
||||
expect(conversation.get("sessionToken")).eql("sessionToken");
|
||||
expect(conversation.get("apiKey")).eql("apiKey");
|
||||
});
|
||||
|
||||
it("should trigger a `session:ready` event", function(done) {
|
||||
conversation.on("session:ready", function() {
|
||||
done();
|
||||
}).setReady(fakeSessionData);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#startSession", function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK
|
||||
});
|
||||
model.startSession();
|
||||
});
|
||||
|
||||
it("should start a session", function() {
|
||||
sinon.assert.calledOnce(fakeSDK.initSession);
|
||||
});
|
||||
|
||||
describe("Session events", function() {
|
||||
it("should trigger a session:connected event on sessionConnected",
|
||||
function(done) {
|
||||
model.once("session:connected", function(){ done(); });
|
||||
|
||||
fakeSession.trigger("sessionConnected");
|
||||
});
|
||||
|
||||
it("should trigger a session:ended event on sessionDisconnected",
|
||||
function(done) {
|
||||
model.once("session:ended", function(){ done(); });
|
||||
|
||||
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
|
||||
});
|
||||
|
||||
it("should set the ongoing attribute to false on sessionDisconnected",
|
||||
function(done) {
|
||||
model.once("session:ended", function() {
|
||||
expect(model.get("ongoing")).eql(false);
|
||||
done();
|
||||
});
|
||||
|
||||
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
|
||||
});
|
||||
|
||||
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.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("networkDisconnected event received", function() {
|
||||
it("should trigger a session:network-disconnected event",
|
||||
function(done) {
|
||||
model.once("session:network-disconnected", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
fakeSession.trigger("networkDisconnected");
|
||||
});
|
||||
|
||||
it("should terminate the session", function() {
|
||||
sandbox.stub(model, "endSession");
|
||||
|
||||
fakeSession.trigger("networkDisconnected", {reason: "ko"});
|
||||
|
||||
sinon.assert.calledOnce(model.endSession);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#endSession", function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK
|
||||
});
|
||||
model.startSession();
|
||||
});
|
||||
|
||||
it("should disconnect current session", function() {
|
||||
model.endSession();
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.disconnect);
|
||||
});
|
||||
|
||||
it("should set the ongoing attribute to false", function() {
|
||||
model.endSession();
|
||||
|
||||
expect(model.get("ongoing")).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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
178
browser/components/loop/test/shared/router_test.js
Normal file
@ -0,0 +1,178 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop, sinon */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.shared.router", function() {
|
||||
"use strict";
|
||||
|
||||
var sandbox, notifier;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
notifier = {
|
||||
notify: sandbox.spy(),
|
||||
warn: sandbox.spy(),
|
||||
warnL10n: sandbox.spy(),
|
||||
error: sandbox.spy(),
|
||||
errorL10n: sandbox.spy()
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("BaseRouter", function() {
|
||||
beforeEach(function() {
|
||||
$("#fixtures").html('<div id="main"></div>');
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
$("#fixtures").empty();
|
||||
});
|
||||
|
||||
describe("#constructor", function() {
|
||||
it("should require a notifier", function() {
|
||||
expect(function() {
|
||||
new loop.shared.router.BaseRouter();
|
||||
}).to.Throw(Error, /missing required notifier/);
|
||||
});
|
||||
|
||||
describe("inherited", function() {
|
||||
var ExtendedRouter = loop.shared.router.BaseRouter.extend({});
|
||||
|
||||
it("should require a notifier", function() {
|
||||
expect(function() {
|
||||
new ExtendedRouter();
|
||||
}).to.Throw(Error, /missing required notifier/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
var router, view, TestRouter;
|
||||
|
||||
beforeEach(function() {
|
||||
TestRouter = loop.shared.router.BaseRouter.extend({});
|
||||
var TestView = loop.shared.views.BaseView.extend({
|
||||
template: _.template("<p>plop</p>")
|
||||
});
|
||||
view = new TestView();
|
||||
router = new TestRouter({notifier: notifier});
|
||||
});
|
||||
|
||||
describe("#loadView", function() {
|
||||
it("should set the active view", function() {
|
||||
router.loadView(view);
|
||||
|
||||
expect(router._activeView).eql(view);
|
||||
});
|
||||
|
||||
it("should load and render the passed view", function() {
|
||||
router.loadView(view);
|
||||
|
||||
expect($("#main p").text()).eql("plop");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#updateView", function() {
|
||||
it("should update the main element with provided contents", function() {
|
||||
router.updateView($("<p>plip</p>"));
|
||||
|
||||
expect($("#main p").text()).eql("plip");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("BaseConversationRouter", function() {
|
||||
var conversation, TestRouter;
|
||||
|
||||
beforeEach(function() {
|
||||
TestRouter = loop.shared.router.BaseConversationRouter.extend({
|
||||
startCall: sandbox.spy(),
|
||||
endCall: sandbox.spy()
|
||||
});
|
||||
conversation = new loop.shared.models.ConversationModel({
|
||||
loopToken: "fakeToken"
|
||||
}, {sdk: {}});
|
||||
});
|
||||
|
||||
describe("#constructor", function() {
|
||||
it("should require a ConversationModel instance", function() {
|
||||
expect(function() {
|
||||
new TestRouter();
|
||||
}).to.Throw(Error, /missing required conversation/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
var router, fakeSessionData;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeSessionData = {
|
||||
sessionId: "sessionId",
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey"
|
||||
};
|
||||
router = new TestRouter({
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
});
|
||||
|
||||
it("should call startCall() once the call session is ready", function() {
|
||||
conversation.trigger("session:ready");
|
||||
|
||||
sinon.assert.calledOnce(router.startCall);
|
||||
});
|
||||
|
||||
it("should call endCall() when conversation ended", function() {
|
||||
conversation.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(router.endCall);
|
||||
});
|
||||
|
||||
it("should warn the user that the session has ended", function() {
|
||||
conversation.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(notifier.warnL10n);
|
||||
sinon.assert.calledWithExactly(notifier.warnL10n,
|
||||
"call_has_ended");
|
||||
});
|
||||
|
||||
it("should warn the user when peer hangs up", function() {
|
||||
conversation.trigger("session:peer-hungup");
|
||||
|
||||
sinon.assert.calledOnce(notifier.warnL10n);
|
||||
sinon.assert.calledWithExactly(notifier.warnL10n,
|
||||
"peer_ended_conversation");
|
||||
|
||||
});
|
||||
|
||||
it("should call endCall() when peer hangs up", function() {
|
||||
conversation.trigger("session:peer-hungup");
|
||||
|
||||
sinon.assert.calledOnce(router.endCall);
|
||||
});
|
||||
|
||||
it("should warn the user when network disconnects", function() {
|
||||
conversation.trigger("session:network-disconnected");
|
||||
|
||||
sinon.assert.calledOnce(notifier.warnL10n);
|
||||
sinon.assert.calledWithExactly(notifier.warnL10n,
|
||||
"network_disconnected");
|
||||
});
|
||||
|
||||
it("should call endCall() when network disconnects", function() {
|
||||
conversation.trigger("session:network-disconnected");
|
||||
|
||||
sinon.assert.calledOnce(router.endCall);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
22
browser/components/loop/test/shared/sdk_mock.js
Normal file
@ -0,0 +1,22 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This file mocks the functions from the OT sdk that we use. This is to provide
|
||||
* an interface that tests can mock out, without needing to maintain a copy of
|
||||
* the sdk or load one from the network.
|
||||
*/
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
if (!window.OT) {
|
||||
window.OT = {};
|
||||
}
|
||||
|
||||
window.OT.checkSystemRequirements = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
})(window);
|
||||
|
16
browser/components/loop/test/shared/test_shared_all.py
Normal file
@ -0,0 +1,16 @@
|
||||
# need to get this dir in the path so that we make the import work
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
from frontend_tester import BaseTestFrontendUnits
|
||||
|
||||
|
||||
class TestSharedUnits(BaseTestFrontendUnits):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSharedUnits, self).setUp()
|
||||
self.set_server_prefix(".")
|
||||
|
||||
def test_units(self):
|
||||
self.check_page("index.html")
|
4696
browser/components/loop/test/shared/vendor/chai-1.9.0.js
vendored
Normal file
270
browser/components/loop/test/shared/vendor/mocha-1.17.1.css
vendored
Normal file
@ -0,0 +1,270 @@
|
||||
@charset "utf-8";
|
||||
|
||||
body {
|
||||
margin:0;
|
||||
}
|
||||
|
||||
#mocha {
|
||||
font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
margin: 60px 50px;
|
||||
}
|
||||
|
||||
#mocha ul,
|
||||
#mocha li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#mocha ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#mocha h1,
|
||||
#mocha h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mocha h1 {
|
||||
margin-top: 15px;
|
||||
font-size: 1em;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
#mocha h1 a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#mocha h1 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#mocha .suite .suite h1 {
|
||||
margin-top: 0;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
#mocha .hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mocha h2 {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#mocha .suite {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
#mocha .test {
|
||||
margin-left: 15px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#mocha .test.pending:hover h2::after {
|
||||
content: '(pending)';
|
||||
font-family: arial, sans-serif;
|
||||
}
|
||||
|
||||
#mocha .test.pass.medium .duration {
|
||||
background: #c09853;
|
||||
}
|
||||
|
||||
#mocha .test.pass.slow .duration {
|
||||
background: #b94a48;
|
||||
}
|
||||
|
||||
#mocha .test.pass::before {
|
||||
content: '✓';
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
color: #00d6b2;
|
||||
}
|
||||
|
||||
#mocha .test.pass .duration {
|
||||
font-size: 9px;
|
||||
margin-left: 5px;
|
||||
padding: 2px 5px;
|
||||
color: #fff;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
-moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
|
||||
-webkit-border-radius: 5px;
|
||||
-moz-border-radius: 5px;
|
||||
-ms-border-radius: 5px;
|
||||
-o-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mocha .test.pass.fast .duration {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mocha .test.pending {
|
||||
color: #0b97c4;
|
||||
}
|
||||
|
||||
#mocha .test.pending::before {
|
||||
content: '◦';
|
||||
color: #0b97c4;
|
||||
}
|
||||
|
||||
#mocha .test.fail {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test.fail pre {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#mocha .test.fail::before {
|
||||
content: '✖';
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
#mocha .test pre.error {
|
||||
color: #c00;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* (1): approximate for browsers not supporting calc
|
||||
* (2): 42 = 2*15 + 2*10 + 2*1 (padding + margin + border)
|
||||
* ^^ seriously
|
||||
*/
|
||||
#mocha .test pre {
|
||||
display: block;
|
||||
float: left;
|
||||
clear: left;
|
||||
font: 12px/1.5 monaco, monospace;
|
||||
margin: 5px;
|
||||
padding: 15px;
|
||||
border: 1px solid #eee;
|
||||
max-width: 85%; /*(1)*/
|
||||
max-width: calc(100% - 42px); /*(2)*/
|
||||
word-wrap: break-word;
|
||||
border-bottom-color: #ddd;
|
||||
-webkit-border-radius: 3px;
|
||||
-webkit-box-shadow: 0 1px 3px #eee;
|
||||
-moz-border-radius: 3px;
|
||||
-moz-box-shadow: 0 1px 3px #eee;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#mocha .test h2 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#mocha .test a.replay {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 0;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
background: #eee;
|
||||
font-size: 15px;
|
||||
-moz-border-radius: 15px;
|
||||
border-radius: 15px;
|
||||
-webkit-transition: opacity 200ms;
|
||||
-moz-transition: opacity 200ms;
|
||||
transition: opacity 200ms;
|
||||
opacity: 0.3;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
#mocha .test:hover a.replay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#mocha-report.pass .test.fail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mocha-report.fail .test.pass {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#mocha-report.pending .test.pass,
|
||||
#mocha-report.pending .test.fail {
|
||||
display: none;
|
||||
}
|
||||
#mocha-report.pending .test.pass.pending {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#mocha-error {
|
||||
color: #c00;
|
||||
font-size: 1.5em;
|
||||
font-weight: 100;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
#mocha-stats {
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
right: 10px;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
color: #888;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#mocha-stats .progress {
|
||||
float: right;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#mocha-stats em {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#mocha-stats a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#mocha-stats a:hover {
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
#mocha-stats li {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
list-style: none;
|
||||
padding-top: 11px;
|
||||
}
|
||||
|
||||
#mocha-stats canvas {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
#mocha code .comment { color: #ddd; }
|
||||
#mocha code .init { color: #2f6fad; }
|
||||
#mocha code .string { color: #5890ad; }
|
||||
#mocha code .keyword { color: #8a6343; }
|
||||
#mocha code .number { color: #2f6fad; }
|
||||
|
||||
@media screen and (max-device-width: 480px) {
|
||||
#mocha {
|
||||
margin: 60px 0px;
|
||||
}
|
||||
|
||||
#mocha #stats {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
5812
browser/components/loop/test/shared/vendor/mocha-1.17.1.js
vendored
Normal file
4794
browser/components/loop/test/shared/vendor/sinon-1.9.0.js
vendored
Normal file
420
browser/components/loop/test/shared/views_test.js
Normal file
@ -0,0 +1,420 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop, sinon */
|
||||
|
||||
var expect = chai.expect;
|
||||
var l10n = document.webL10n || document.mozL10n;
|
||||
|
||||
describe("loop.shared.views", function() {
|
||||
"use strict";
|
||||
|
||||
var sharedModels = loop.shared.models,
|
||||
sharedViews = loop.shared.views,
|
||||
sandbox;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
$("#fixtures").empty();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("L10nView", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(l10n, "translate");
|
||||
});
|
||||
|
||||
it("should translate generated contents on render()", function() {
|
||||
var TestView = loop.shared.views.L10nView.extend();
|
||||
|
||||
var view = new TestView();
|
||||
view.render();
|
||||
|
||||
sinon.assert.calledOnce(l10n.translate);
|
||||
sinon.assert.calledWithExactly(l10n.translate, view.el);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConversationView", function() {
|
||||
var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model;
|
||||
|
||||
beforeEach(function() {
|
||||
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 = {
|
||||
on: sandbox.spy(),
|
||||
off: sandbox.spy()
|
||||
};
|
||||
fakeSDK = {
|
||||
initPublisher: sandbox.stub().returns(fakePublisher),
|
||||
initSession: sandbox.stub().returns(fakeSession)
|
||||
};
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK
|
||||
});
|
||||
});
|
||||
|
||||
describe("#initialize", function() {
|
||||
it("should require a sdk object", function() {
|
||||
expect(function() {
|
||||
new sharedViews.ConversationView();
|
||||
}).to.Throw(Error, /sdk/);
|
||||
});
|
||||
|
||||
it("should start a session", function() {
|
||||
sandbox.stub(model, "startSession");
|
||||
|
||||
new sharedViews.ConversationView({sdk: fakeSDK, model: model});
|
||||
|
||||
sinon.assert.calledOnce(model.startSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
describe("#hangup", function() {
|
||||
it("should disconnect the session", function() {
|
||||
var view = new sharedViews.ConversationView({
|
||||
sdk: fakeSDK,
|
||||
model: model
|
||||
});
|
||||
sandbox.stub(model, "endSession");
|
||||
view.publish();
|
||||
|
||||
view.hangup({preventDefault: function() {}});
|
||||
|
||||
sinon.assert.calledOnce(model.endSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#publish", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
view = new sharedViews.ConversationView({
|
||||
sdk: fakeSDK,
|
||||
model: model
|
||||
});
|
||||
});
|
||||
|
||||
it("should publish local stream", function() {
|
||||
view.publish();
|
||||
|
||||
sinon.assert.calledOnce(fakeSDK.initPublisher);
|
||||
sinon.assert.calledOnce(fakeSession.publish);
|
||||
});
|
||||
|
||||
it("should start listening to OT publisher accessDialogOpened and " +
|
||||
" accessDenied events",
|
||||
function() {
|
||||
view.publish();
|
||||
|
||||
sinon.assert.calledTwice(fakePublisher.on);
|
||||
sinon.assert.calledWith(fakePublisher.on, "accessDialogOpened");
|
||||
sinon.assert.calledWith(fakePublisher.on, "accessDenied");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#unpublish", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
view = new sharedViews.ConversationView({
|
||||
sdk: fakeSDK,
|
||||
model: model
|
||||
});
|
||||
view.publish();
|
||||
});
|
||||
|
||||
it("should unpublish local stream", function() {
|
||||
view.unpublish();
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.unpublish);
|
||||
});
|
||||
|
||||
it("should unsubscribe from accessDialogOpened and accessDenied events",
|
||||
function() {
|
||||
view.unpublish();
|
||||
|
||||
sinon.assert.calledTwice(fakePublisher.off);
|
||||
sinon.assert.calledWith(fakePublisher.off, "accessDialogOpened");
|
||||
sinon.assert.calledWith(fakePublisher.off, "accessDenied");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Model events", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(sharedViews.ConversationView.prototype, "publish");
|
||||
sandbox.stub(sharedViews.ConversationView.prototype, "unpublish");
|
||||
view = new sharedViews.ConversationView({sdk: fakeSDK, model: model});
|
||||
});
|
||||
|
||||
it("should publish local stream on session:connected", function() {
|
||||
model.trigger("session:connected");
|
||||
|
||||
sinon.assert.calledOnce(view.publish);
|
||||
});
|
||||
|
||||
it("should publish remote streams on session:stream-created",
|
||||
function() {
|
||||
var s1 = {connection: {connectionId: 42}};
|
||||
var s2 = {connection: {connectionId: 43}};
|
||||
|
||||
model.trigger("session:stream-created", {streams: [s1, s2]});
|
||||
|
||||
sinon.assert.calledOnce(fakeSession.subscribe);
|
||||
sinon.assert.calledWith(fakeSession.subscribe, s2);
|
||||
});
|
||||
|
||||
it("should unpublish local stream on session:ended", function() {
|
||||
model.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(view.unpublish);
|
||||
});
|
||||
|
||||
it("should unpublish local stream on session:peer-hungup", function() {
|
||||
model.trigger("session:peer-hungup");
|
||||
|
||||
sinon.assert.calledOnce(view.unpublish);
|
||||
});
|
||||
|
||||
it("should unpublish local stream on session:network-disconnected",
|
||||
function() {
|
||||
model.trigger("session:network-disconnected");
|
||||
|
||||
sinon.assert.calledOnce(view.unpublish);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationView", function() {
|
||||
var collection, model, view;
|
||||
|
||||
beforeEach(function() {
|
||||
$("#fixtures").append('<div id="test-notif"></div>');
|
||||
model = new sharedModels.NotificationModel({
|
||||
level: "error",
|
||||
message: "plop"
|
||||
});
|
||||
collection = new sharedModels.NotificationCollection([model]);
|
||||
view = new sharedViews.NotificationView({
|
||||
el: $("#test-notif"),
|
||||
collection: collection,
|
||||
model: model
|
||||
});
|
||||
});
|
||||
|
||||
describe("#dismiss", function() {
|
||||
it("should automatically dismiss notification after 500ms", function() {
|
||||
view.render().dismiss({preventDefault: sandbox.spy()});
|
||||
|
||||
expect(view.$(".message").text()).eql("plop");
|
||||
|
||||
sandbox.clock.tick(500);
|
||||
|
||||
expect(collection).to.have.length.of(0);
|
||||
expect($("#test-notif").html()).eql(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#render", function() {
|
||||
it("should render template with model attribute values", function() {
|
||||
view.render();
|
||||
|
||||
expect(view.$(".message").text()).eql("plop");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationListView", function() {
|
||||
var coll, notifData, testNotif;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(l10n, "get", function(x) {
|
||||
return "translated:" + x;
|
||||
});
|
||||
notifData = {level: "error", message: "plop"};
|
||||
testNotif = new sharedModels.NotificationModel(notifData);
|
||||
coll = new sharedModels.NotificationCollection();
|
||||
});
|
||||
|
||||
describe("#initialize", function() {
|
||||
it("should accept a collection option", function() {
|
||||
var view = new sharedViews.NotificationListView({collection: coll});
|
||||
|
||||
expect(view.collection).to.be.an.instanceOf(
|
||||
sharedModels.NotificationCollection);
|
||||
});
|
||||
|
||||
it("should set a default collection when none is passed", function() {
|
||||
var view = new sharedViews.NotificationListView();
|
||||
|
||||
expect(view.collection).to.be.an.instanceOf(
|
||||
sharedModels.NotificationCollection);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#clear", function() {
|
||||
it("should clear all notifications from the collection", function() {
|
||||
var view = new sharedViews.NotificationListView();
|
||||
view.notify(testNotif);
|
||||
|
||||
view.clear();
|
||||
|
||||
expect(coll).to.have.length.of(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#notify", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
view = new sharedViews.NotificationListView({collection: coll});
|
||||
});
|
||||
|
||||
describe("adds a new notification to the stack", function() {
|
||||
it("using a plain object", function() {
|
||||
view.notify(notifData);
|
||||
|
||||
expect(coll).to.have.length.of(1);
|
||||
});
|
||||
|
||||
it("using a NotificationModel instance", function() {
|
||||
view.notify(testNotif);
|
||||
|
||||
expect(coll).to.have.length.of(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#notifyL10n", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
view = new sharedViews.NotificationListView({collection: coll});
|
||||
});
|
||||
|
||||
it("should translate a message string identifier", function() {
|
||||
view.notifyL10n("fakeId", "warning");
|
||||
|
||||
sinon.assert.calledOnce(l10n.get);
|
||||
sinon.assert.calledWithExactly(l10n.get, "fakeId");
|
||||
});
|
||||
|
||||
it("should notify end user with the provided message", function() {
|
||||
sandbox.stub(view, "notify");
|
||||
|
||||
view.notifyL10n("fakeId", "warning");
|
||||
|
||||
sinon.assert.calledOnce(view.notify);
|
||||
sinon.assert.calledWithExactly(view.notify, {
|
||||
message: "translated:fakeId",
|
||||
level: "warning"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#warn", function() {
|
||||
it("should add a warning notification to the stack", function() {
|
||||
var view = new sharedViews.NotificationListView({collection: coll});
|
||||
|
||||
view.warn("watch out");
|
||||
|
||||
expect(coll).to.have.length.of(1);
|
||||
expect(coll.at(0).get("level")).eql("warning");
|
||||
expect(coll.at(0).get("message")).eql("watch out");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#warnL10n", function() {
|
||||
it("should warn using a l10n string id", function() {
|
||||
var view = new sharedViews.NotificationListView({collection: coll});
|
||||
sandbox.stub(view, "notify");
|
||||
|
||||
view.warnL10n("fakeId");
|
||||
|
||||
sinon.assert.called(view.notify);
|
||||
sinon.assert.calledWithExactly(view.notify, {
|
||||
message: "translated:fakeId",
|
||||
level: "warning"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#error", function() {
|
||||
it("should add an error notification to the stack", function() {
|
||||
var view = new sharedViews.NotificationListView({collection: coll});
|
||||
|
||||
view.error("wrong");
|
||||
|
||||
expect(coll).to.have.length.of(1);
|
||||
expect(coll.at(0).get("level")).eql("error");
|
||||
expect(coll.at(0).get("message")).eql("wrong");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#errorL10n", function() {
|
||||
it("should notify an error using a l10n string id", function() {
|
||||
var view = new sharedViews.NotificationListView({collection: coll});
|
||||
sandbox.stub(view, "notify");
|
||||
|
||||
view.errorL10n("fakeId");
|
||||
|
||||
sinon.assert.called(view.notify);
|
||||
sinon.assert.calledWithExactly(view.notify, {
|
||||
message: "translated:fakeId",
|
||||
level: "error"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Collection events", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(sharedViews.NotificationListView.prototype, "render");
|
||||
view = new sharedViews.NotificationListView({collection: coll});
|
||||
});
|
||||
|
||||
it("should render when a notification is added to the collection",
|
||||
function() {
|
||||
coll.add(testNotif);
|
||||
|
||||
sinon.assert.calledOnce(view.render);
|
||||
});
|
||||
|
||||
it("should render when a notification is removed from the collection",
|
||||
function() {
|
||||
coll.add(testNotif);
|
||||
coll.remove(testNotif);
|
||||
|
||||
sinon.assert.calledTwice(view.render);
|
||||
});
|
||||
|
||||
it("should render when the collection is reset", function() {
|
||||
coll.reset();
|
||||
|
||||
sinon.assert.calledOnce(view.render);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
45
browser/components/loop/test/standalone/index.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Loop mocha tests</title>
|
||||
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha">
|
||||
<p><a href="../">Index</a></p>
|
||||
<p><a href="../shared/">Shared Tests</a></p>
|
||||
</div>
|
||||
<div id="messages"></div>
|
||||
<div id="fixtures"></div>
|
||||
<!-- libs -->
|
||||
<script src="../../content/shared/libs/webl10n-20130617.js"></script>
|
||||
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
|
||||
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
|
||||
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
|
||||
<!-- test dependencies -->
|
||||
<script src="../shared/vendor/mocha-1.17.1.js"></script>
|
||||
<script src="../shared/vendor/chai-1.9.0.js"></script>
|
||||
<script src="../shared/vendor/sinon-1.9.0.js"></script>
|
||||
<script src="../shared/sdk_mock.js"></script>
|
||||
<script>
|
||||
chai.Assertion.includeStack = true;
|
||||
mocha.setup('bdd');
|
||||
</script>
|
||||
<!-- App scripts -->
|
||||
<script src="../../content/shared/js/models.js"></script>
|
||||
<script src="../../content/shared/js/views.js"></script>
|
||||
<script src="../../content/shared/js/router.js"></script>
|
||||
<script src="../../standalone/content/js/webapp.js"></script>
|
||||
<!-- Test scripts -->
|
||||
<script src="webapp_test.js"></script>
|
||||
<script>
|
||||
mocha.run(function () {
|
||||
$("#mocha").append("<p id='complete'>Complete.</p>");
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,16 @@
|
||||
# need to get this dir in the path so that we make the import work
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'shared'))
|
||||
|
||||
from frontend_tester import BaseTestFrontendUnits
|
||||
|
||||
|
||||
class TestDesktopUnits(BaseTestFrontendUnits):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDesktopUnits, self).setUp()
|
||||
self.set_server_prefix("../standalone/")
|
||||
|
||||
def test_units(self):
|
||||
self.check_page("index.html")
|
344
browser/components/loop/test/standalone/webapp_test.js
Normal file
@ -0,0 +1,344 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop, sinon */
|
||||
|
||||
var expect = chai.expect;
|
||||
|
||||
describe("loop.webapp", function() {
|
||||
"use strict";
|
||||
|
||||
var sharedModels = loop.shared.models,
|
||||
sharedViews = loop.shared.views,
|
||||
sandbox,
|
||||
notifier;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
notifier = {
|
||||
notify: sandbox.spy(),
|
||||
warn: sandbox.spy(),
|
||||
warnL10n: sandbox.spy(),
|
||||
error: sandbox.spy(),
|
||||
errorL10n: sandbox.spy(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("#init", function() {
|
||||
var WebappRouter;
|
||||
|
||||
beforeEach(function() {
|
||||
WebappRouter = loop.webapp.WebappRouter;
|
||||
sandbox.stub(WebappRouter.prototype, "navigate");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
Backbone.history.stop();
|
||||
});
|
||||
|
||||
it("should navigate to the unsupportedDevice route if the sdk detects " +
|
||||
"the device is running iOS", function() {
|
||||
sandbox.stub(loop.webapp.WebappHelper.prototype, "isIOS").returns(true);
|
||||
|
||||
loop.webapp.init();
|
||||
|
||||
sinon.assert.calledOnce(WebappRouter.prototype.navigate);
|
||||
sinon.assert.calledWithExactly(WebappRouter.prototype.navigate,
|
||||
"unsupportedDevice", {trigger: true});
|
||||
});
|
||||
|
||||
it("should navigate to the unsupportedBrowser route if the sdk detects " +
|
||||
"the browser is unsupported", function() {
|
||||
sandbox.stub(loop.webapp.WebappHelper.prototype, "isIOS").returns(false);
|
||||
sandbox.stub(window.OT, "checkSystemRequirements").returns(false);
|
||||
|
||||
loop.webapp.init();
|
||||
|
||||
sinon.assert.calledOnce(WebappRouter.prototype.navigate);
|
||||
sinon.assert.calledWithExactly(WebappRouter.prototype.navigate,
|
||||
"unsupportedBrowser", {trigger: true});
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebappRouter", function() {
|
||||
var router, conversation;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
|
||||
router = new loop.webapp.WebappRouter({
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
sandbox.stub(router, "loadView");
|
||||
sandbox.stub(router, "navigate");
|
||||
});
|
||||
|
||||
describe("#startCall", function() {
|
||||
it("should navigate back home if session token is missing", function() {
|
||||
router.startCall();
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "home");
|
||||
});
|
||||
|
||||
it("should notify the user if session token is missing", function() {
|
||||
router.startCall();
|
||||
|
||||
sinon.assert.calledOnce(notifier.errorL10n);
|
||||
sinon.assert.calledWithExactly(notifier.errorL10n,
|
||||
"missing_conversation_info");
|
||||
});
|
||||
|
||||
it("should navigate to call/ongoing/:token if session token is available",
|
||||
function() {
|
||||
conversation.set("loopToken", "fake");
|
||||
|
||||
router.startCall();
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#endCall", function() {
|
||||
it("should navigate to home if session token is unset", function() {
|
||||
router.endCall();
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "home");
|
||||
});
|
||||
|
||||
it("should navigate to call/:token if session token is set", function() {
|
||||
conversation.set("loopToken", "fake");
|
||||
|
||||
router.endCall();
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/fake");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Routes", function() {
|
||||
describe("#home", function() {
|
||||
it("should load the HomeView", function() {
|
||||
router.home();
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWith(router.loadView,
|
||||
sinon.match.instanceOf(loop.webapp.HomeView));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#initiate", function() {
|
||||
it("should set the token on the conversation model", function() {
|
||||
router.initiate("fakeToken");
|
||||
|
||||
expect(conversation.get("loopToken")).eql("fakeToken");
|
||||
});
|
||||
|
||||
it("should load the ConversationFormView", function() {
|
||||
router.initiate("fakeToken");
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWith(router.loadView,
|
||||
sinon.match.instanceOf(loop.webapp.ConversationFormView));
|
||||
});
|
||||
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=991118
|
||||
it("should terminate any ongoing call session", function() {
|
||||
sinon.stub(conversation, "endSession");
|
||||
conversation.set("ongoing", true);
|
||||
|
||||
router.initiate("fakeToken");
|
||||
|
||||
sinon.assert.calledOnce(conversation.endSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#loadConversation", function() {
|
||||
it("should load the ConversationView if session is set", function() {
|
||||
sandbox.stub(sharedViews.ConversationView.prototype, "initialize");
|
||||
conversation.set("sessionId", "fakeSessionId");
|
||||
|
||||
router.loadConversation();
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWith(router.loadView,
|
||||
sinon.match.instanceOf(sharedViews.ConversationView));
|
||||
});
|
||||
|
||||
it("should navigate to #call/{token} if session isn't ready",
|
||||
function() {
|
||||
router.loadConversation("fakeToken");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#unsupportedDevice", function() {
|
||||
it("should load the UnsupportedDeviceView", function() {
|
||||
router.unsupportedDevice();
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWith(router.loadView,
|
||||
sinon.match.instanceOf(sharedViews.UnsupportedDeviceView));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#unsupportedBrowser", function() {
|
||||
it("should load the UnsupportedBrowserView", function() {
|
||||
router.unsupportedBrowser();
|
||||
|
||||
sinon.assert.calledOnce(router.loadView);
|
||||
sinon.assert.calledWith(router.loadView,
|
||||
sinon.match.instanceOf(sharedViews.UnsupportedBrowserView));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
var fakeSessionData;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeSessionData = {
|
||||
sessionId: "sessionId",
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey"
|
||||
};
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
});
|
||||
|
||||
it("should navigate to call/ongoing/:token once call session is ready",
|
||||
function() {
|
||||
conversation.trigger("session:ready");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ongoing/fakeToken");
|
||||
});
|
||||
|
||||
it("should navigate to call/{token} when conversation ended", function() {
|
||||
conversation.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
|
||||
});
|
||||
|
||||
it("should navigate to call/{token} when peer hangs up", function() {
|
||||
conversation.trigger("session:peer-hungup");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
|
||||
});
|
||||
|
||||
it("should navigate to call/{token} when network disconnects",
|
||||
function() {
|
||||
conversation.trigger("session:network-disconnected");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConversationFormView", function() {
|
||||
var conversation;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
|
||||
});
|
||||
|
||||
describe("#initialize", function() {
|
||||
it("should require a conversation option", function() {
|
||||
expect(function() {
|
||||
new loop.webapp.WebappRouter();
|
||||
}).to.Throw(Error, /missing required conversation/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#initiate", function() {
|
||||
var conversation, initiate, view, fakeSubmitEvent;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {sdk: {}});
|
||||
view = new loop.webapp.ConversationFormView({
|
||||
model: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
fakeSubmitEvent = {preventDefault: sinon.spy()};
|
||||
initiate = sinon.stub(conversation, "initiate");
|
||||
});
|
||||
|
||||
it("should start the conversation establishment process", function() {
|
||||
conversation.set("loopToken", "fake");
|
||||
|
||||
view.initiate(fakeSubmitEvent);
|
||||
|
||||
sinon.assert.calledOnce(fakeSubmitEvent.preventDefault);
|
||||
sinon.assert.calledOnce(initiate);
|
||||
sinon.assert.calledWith(initiate, {
|
||||
baseServerUrl: loop.webapp.baseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable current form once session is initiated", function() {
|
||||
sandbox.stub(view, "disableForm");
|
||||
conversation.set("loopToken", "fake");
|
||||
|
||||
view.initiate(fakeSubmitEvent);
|
||||
|
||||
sinon.assert.calledOnce(view.disableForm);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
var conversation, view;
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({
|
||||
loopToken: "fake"
|
||||
}, {sdk: {}});
|
||||
view = new loop.webapp.ConversationFormView({
|
||||
model: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
});
|
||||
|
||||
it("should trigger a notication when a session:error model event is " +
|
||||
" received", function() {
|
||||
conversation.trigger("session:error", "tech error");
|
||||
|
||||
sinon.assert.calledOnce(notifier.errorL10n);
|
||||
sinon.assert.calledWithExactly(notifier.errorL10n,
|
||||
"unable_retrieve_call_info");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebappHelper", function() {
|
||||
var helper;
|
||||
|
||||
beforeEach(function() {
|
||||
helper = new loop.webapp.WebappHelper();
|
||||
});
|
||||
|
||||
describe("#isIOS", function() {
|
||||
it("should detect iOS", function() {
|
||||
expect(helper.isIOS("iPad")).eql(true);
|
||||
expect(helper.isIOS("iPod")).eql(true);
|
||||
expect(helper.isIOS("iPhone")).eql(true);
|
||||
expect(helper.isIOS("iPhone Simulator")).eql(true);
|
||||
});
|
||||
|
||||
it("shouldn't detect iOS with other platforms", function() {
|
||||
expect(helper.isIOS("MacIntel")).eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|