Bug 1273671 - Land version 1.4.0 of the Loop system add-on in mozilla-central, rs=Standard8 for already reviewed code.

This commit is contained in:
Mark Banner
2016-05-17 22:46:17 +01:00
parent ac8d0ba8df
commit dde7a6afc5
125 changed files with 32747 additions and 33097 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +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/. */
"use strict";
"use strict";var _Components =
const { utils: Cu } = Components;
Components;var Cu = _Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
const { MozLoopService, LOOP_SESSION_TYPE } =
Cu.import("chrome://loop/content/modules/MozLoopService.jsm", {});
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
Cu.import("resource://gre/modules/Task.jsm");var _Cu$import =
Cu.import("chrome://loop/content/modules/MozLoopService.jsm", {});var MozLoopService = _Cu$import.MozLoopService;var LOOP_SESSION_TYPE = _Cu$import.LOOP_SESSION_TYPE;
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
this.EXPORTED_SYMBOLS = ["LoopRoomsCache"];
const LOOP_ROOMS_CACHE_FILENAME = "loopRoomsCache.json";
var LOOP_ROOMS_CACHE_FILENAME = "loopRoomsCache.json";
XPCOMUtils.defineConstant(this, "LOOP_ROOMS_CACHE_FILENAME", LOOP_ROOMS_CACHE_FILENAME);
/**
@@ -47,26 +47,25 @@ function LoopRoomsCache(options) {
this.baseDir = options.baseDir || OS.Constants.Path.profileDir;
this.path = OS.Path.join(
this.baseDir,
options.filename || LOOP_ROOMS_CACHE_FILENAME
);
this._cache = null;
}
this.baseDir,
options.filename || LOOP_ROOMS_CACHE_FILENAME);
LoopRoomsCache.prototype = {
this._cache = null;}
LoopRoomsCache.prototype = {
/**
* Updates the local copy of the cache and saves it to disk.
*
* @param {Object} contents An object to be saved in json format.
* @return {Promise} A promise that is resolved once the save is complete.
*/
_setCache: function(contents) {
_setCache: function _setCache(contents) {var _this = this;
this._cache = contents;
return OS.File.makeDir(this.baseDir, { ignoreExisting: true }).then(() => {
return CommonUtils.writeJSON(contents, this.path);
});
},
return OS.File.makeDir(this.baseDir, { ignoreExisting: true }).then(function () {return (
CommonUtils.writeJSON(contents, _this.path));});},
/**
* Returns the local copy of the cache if there is one, otherwise it reads
@@ -76,28 +75,28 @@ LoopRoomsCache.prototype = {
*/
_getCache: Task.async(function* () {
if (this._cache) {
return this._cache;
}
return this._cache;}
try {
return (this._cache = yield CommonUtils.readJSON(this.path));
} catch (error) {
return this._cache = yield CommonUtils.readJSON(this.path);}
catch (error) {
if (!error.becauseNoSuchFile) {
MozLoopService.log.debug("Error reading the cache:", error);
}
return (this._cache = {});
}
}),
MozLoopService.log.debug("Error reading the cache:", error);}
return this._cache = {};}}),
/**
* Function for testability purposes. Clears the cache.
*
* @return {Promise} A promise that is resolved once the clear is complete.
*/
clear: function() {
clear: function clear() {
this._cache = null;
return OS.File.remove(this.path);
},
return OS.File.remove(this.path);},
/**
* Gets a room key from the cache.
@@ -109,16 +108,16 @@ LoopRoomsCache.prototype = {
*/
getKey: Task.async(function* (sessionType, roomToken) {
if (sessionType != LOOP_SESSION_TYPE.FXA) {
return null;
}
return null;}
let sessionData = (yield this._getCache())[sessionType];
var sessionData = (yield this._getCache())[sessionType];
if (!sessionData || !sessionData[roomToken]) {
return null;
}
return sessionData[roomToken].key;
}),
return null;}
return sessionData[roomToken].key;}),
/**
* Stores a room key into the cache. Note, if the key has not changed,
@@ -131,30 +130,28 @@ LoopRoomsCache.prototype = {
*/
setKey: Task.async(function* (sessionType, roomToken, roomKey) {
if (sessionType != LOOP_SESSION_TYPE.FXA) {
return Promise.resolve();
}
return Promise.resolve();}
let cache = yield this._getCache();
var cache = yield this._getCache();
// Create these objects if they don't exist.
// We aim to do this creation and setting of the room key in a
// forwards-compatible way so that if new fields are added to rooms later
// then we don't mess them up (if there's no keys).
if (!cache[sessionType]) {
cache[sessionType] = {};
}
cache[sessionType] = {};}
if (!cache[sessionType][roomToken]) {
cache[sessionType][roomToken] = {};
}
cache[sessionType][roomToken] = {};}
// Only save it if there's no key, or it is different.
if (!cache[sessionType][roomToken].key ||
cache[sessionType][roomToken].key != roomKey) {
if (!cache[sessionType][roomToken].key ||
cache[sessionType][roomToken].key != roomKey) {
cache[sessionType][roomToken].key = roomKey;
return yield this._setCache(cache);
}
return yield this._setCache(cache);}
return Promise.resolve();
})
};
return Promise.resolve();}) };

File diff suppressed because it is too large Load Diff

View File

@@ -6,14 +6,14 @@
* A worker dedicated to loop-report sanitation and writing for MozLoopService.
*/
"use strict";
"use strict";var _slicedToArray = function () {function sliceIterator(arr, i) {var _arr = [];var _n = true;var _d = false;var _e = undefined;try {for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {_arr.push(_s.value);if (i && _arr.length === i) break;}} catch (err) {_d = true;_e = err;} finally {try {if (!_n && _i["return"]) _i["return"]();} finally {if (_d) throw _e;}}return _arr;}return function (arr, i) {if (Array.isArray(arr)) {return arr;} else if (Symbol.iterator in Object(arr)) {return sliceIterator(arr, i);} else {throw new TypeError("Invalid attempt to destructure non-iterable instance");}};}();
importScripts("resource://gre/modules/osfile.jsm");
var Encoder = new TextEncoder();
var Counter = 0;
const MAX_LOOP_LOGS = 5;
var MAX_LOOP_LOGS = 5;
/**
* Communications with the controller.
*
@@ -25,17 +25,17 @@ const MAX_LOOP_LOGS = 5;
* { fail: serialized_form_of_OS.File.Error }
*/
onmessage = function(e) {
onmessage = function onmessage(e) {
if (++Counter > MAX_LOOP_LOGS) {
postMessage({
fail: "Maximum " + MAX_LOOP_LOGS + "loop reports reached for this session"
});
return;
}
postMessage({
fail: "Maximum " + MAX_LOOP_LOGS + "loop reports reached for this session" });
let directory = e.data.directory;
let filename = e.data.filename;
let ping = e.data.ping;
return;}
var directory = e.data.directory;
var filename = e.data.filename;
var ping = e.data.ping;
// Anonymize data
resetIpMask();
@@ -43,27 +43,27 @@ onmessage = function(e) {
ping.payload.remoteSdp = redactSdp(ping.payload.remoteSdp);
ping.payload.log = sanitizeLogs(ping.payload.log);
let pingStr = anonymizeIPv4(sanitizeUrls(JSON.stringify(ping)));
var pingStr = anonymizeIPv4(sanitizeUrls(JSON.stringify(ping)));
// Save to disk
let array = Encoder.encode(pingStr);
var array = Encoder.encode(pingStr);
try {
OS.File.makeDir(directory, {
unixMode: OS.Constants.S_IRWXU,
ignoreExisting: true
});
OS.File.makeDir(directory, {
unixMode: OS.Constants.S_IRWXU,
ignoreExisting: true });
OS.File.writeAtomic(OS.Path.join(directory, filename), array);
postMessage({ ok: true });
} catch (ex) {
postMessage({ ok: true });}
catch (ex) {
// Instances of OS.File.Error know how to serialize themselves
if (ex instanceof OS.File.Error) {
postMessage({ fail: OS.File.Error.toMsg(ex) });
}
else {
throw ex;
}
}
};
postMessage({ fail: OS.File.Error.toMsg(ex) });} else
{
throw ex;}}};
/**
* Mask upper 24-bits of ip address with fake numbers. Call resetIpMask() first.
@@ -74,8 +74,8 @@ var IpCount = 0;
function resetIpMask() {
IpMap = {};
IpCount = Math.floor(Math.random() * 16777215) + 1;
}
IpCount = Math.floor(Math.random() * 16777215) + 1;}
/**
* Masks upper 24-bits of ip address with fake numbers. Grunt function.
@@ -83,32 +83,32 @@ function resetIpMask() {
* @param {DOMString} ip address
*/
function maskIp(ip) {
let isInvalidOrRfc1918or3927 = function(p1, p2, p3, p4) {
let invalid = octet => octet < 0 || octet > 255;
return invalid(p1) || invalid(p2) || invalid(p3) || invalid(p4) ||
(p1 == 10) ||
(p1 == 172 && p2 >= 16 && p2 <= 31) ||
(p1 == 192 && p2 == 168) ||
(p1 == 169 && p2 == 254);
};
var isInvalidOrRfc1918or3927 = function isInvalidOrRfc1918or3927(p1, p2, p3, p4) {
var invalid = function invalid(octet) {return octet < 0 || octet > 255;};
return invalid(p1) || invalid(p2) || invalid(p3) || invalid(p4) ||
p1 == 10 ||
p1 == 172 && p2 >= 16 && p2 <= 31 ||
p1 == 192 && p2 == 168 ||
p1 == 169 && p2 == 254;};var _ip$split =
let [p1, p2, p3, p4] = ip.split(".");
ip.split(".");var _ip$split2 = _slicedToArray(_ip$split, 4);var p1 = _ip$split2[0];var p2 = _ip$split2[1];var p3 = _ip$split2[2];var p4 = _ip$split2[3];
if (isInvalidOrRfc1918or3927(p1, p2, p3, p4)) {
return ip;
}
let key = [p1, p2, p3].join();
return ip;}
var key = [p1, p2, p3].join();
if (!IpMap[key]) {
do {
IpCount = (IpCount + 1049039) % 16777216; // + prime % 2^24
p1 = (IpCount >> 16) % 256;
p2 = (IpCount >> 8) % 256;
p3 = IpCount % 256;
} while (isInvalidOrRfc1918or3927(p1, p2, p3, p4));
IpMap[key] = p1 + "." + p2 + "." + p3;
}
return IpMap[key] + "." + p4;
}
p3 = IpCount % 256;} while (
isInvalidOrRfc1918or3927(p1, p2, p3, p4));
IpMap[key] = p1 + "." + p2 + "." + p3;}
return IpMap[key] + "." + p4;}
/**
* Partially masks ip numbers in input text.
@@ -116,9 +116,9 @@ function maskIp(ip) {
* @param {DOMString} text Input text containing IP numbers as words.
*/
function anonymizeIPv4(text) {
return text.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
maskIp.bind(this));
}
return text.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
maskIp.bind(this));}
/**
* Sanitizes any urls of session information, like
@@ -135,12 +135,12 @@ function anonymizeIPv4(text) {
* @param {DOMString} text The text.
*/
function sanitizeUrls(text) {
let trimUrl = url => url.replace(/(#call|#incoming|#).*/g,
(match, type) => type + "/xxxx");
return text.replace(/\(id=(\d+) url=([^\)]+)\)/g,
(match, id, url) =>
"(id=" + id + " url=" + trimUrl(url) + ")");
}
var trimUrl = function trimUrl(url) {return url.replace(/(#call|#incoming|#).*/g,
function (match, type) {return type + "/xxxx";});};
return text.replace(/\(id=(\d+) url=([^\)]+)\)/g,
function (match, id, url) {return (
"(id=" + id + " url=" + trimUrl(url) + ")");});}
/**
* Removes privacy sensitive information from SDP input text outright, like
@@ -152,8 +152,8 @@ function sanitizeUrls(text) {
*
* @param {DOMString} sdp The sdp text.
*/
var redactSdp = sdp => sdp.replace(/\r\na=(fingerprint|identity):.*?\r\n/g,
"\r\n");
var redactSdp = function redactSdp(sdp) {return sdp.replace(/\r\na=(fingerprint|identity):.*?\r\n/g,
"\r\n");};
/**
* Sanitizes log text of sensitive information, like
@@ -164,7 +164,6 @@ var redactSdp = sdp => sdp.replace(/\r\na=(fingerprint|identity):.*?\r\n/g,
* @param {DOMString} log The log text.
*/
function sanitizeLogs(log) {
let rex = /(srflx|relay)\(IP4:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}\/(UDP|TCP)\|[^\)]+\)/g;
var rex = /(srflx|relay)\(IP4:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}\/(UDP|TCP)\|[^\)]+\)/g;
return log.replace(rex, match => match.replace(/\|[^\)]+\)/, "|xxxx.xxx)"));
}
return log.replace(rex, function (match) {return match.replace(/\|[^\)]+\)/, "|xxxx.xxx)");});}

View File

@@ -13,10 +13,8 @@
// Listen for when the title is changed and send a message back to the chrome
// process.
addEventListener("DOMTitleChanged", ({ target }) => {
sendAsyncMessage("loop@mozilla.org:DOMTitleChanged", {
details: "titleChanged"
}, {
target: target
});
});
addEventListener("DOMTitleChanged", function (_ref) {var target = _ref.target;
sendAsyncMessage("loop@mozilla.org:DOMTitleChanged", {
details: "titleChanged" },
{
target: target });});

View File

@@ -23,6 +23,7 @@
<script type="text/javascript" src="panels/js/otconfig.js"></script>
<script type="text/javascript" src="shared/vendor/sdk.js"></script>
<script type="text/javascript" src="shared/vendor/react.js"></script>
<script type="text/javascript" src="shared/vendor/react-dom.js"></script>
<script type="text/javascript" src="shared/vendor/lodash.js"></script>
<script type="text/javascript" src="shared/vendor/backbone.js"></script>
<script type="text/javascript" src="shared/vendor/classnames.js"></script>

View File

@@ -17,6 +17,7 @@
<script type="text/javascript" src="panels/vendor/l10n.js"></script>
<script type="text/javascript" src="shared/vendor/react.js"></script>
<script type="text/javascript" src="shared/vendor/react-dom.js"></script>
<script type="text/javascript" src="panels/js/copy.js"></script>
</body>
</html>

View File

@@ -422,12 +422,6 @@ html[dir="rtl"] .rename-container > .input-group {
top: 0;
}
/* keep the various room-entry row pieces aligned with each other */
.room-list > .room-entry > h2 > button,
.room-list > .room-entry > h2 > span {
vertical-align: middle;
}
/* Room entry context button (edit button) */
.room-entry-context-actions {
display: none;

View File

@@ -1,6 +1,6 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.conversation = function (mozL10n) {
@@ -18,91 +18,94 @@ loop.conversation = function (mozL10n) {
* Master controller view for handling if incoming or outgoing calls are
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({
displayName: "AppControllerView",
var AppControllerView = React.createClass({ displayName: "AppControllerView",
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationAppStore"),
sharedMixins.DocumentTitleMixin,
sharedMixins.WindowCloseMixin],
mixins: [Backbone.Events, loop.store.StoreMixin("conversationAppStore"), sharedMixins.DocumentTitleMixin, sharedMixins.WindowCloseMixin],
propTypes: {
cursorStore: React.PropTypes.instanceOf(loop.store.RemoteCursorStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
},
propTypes: {
cursorStore: React.PropTypes.instanceOf(loop.store.RemoteCursorStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore) },
componentWillMount: function () {
this.listenTo(this.props.cursorStore, "change:remoteCursorPosition", this._onRemoteCursorPositionChange);
this.listenTo(this.props.cursorStore, "change:remoteCursorClick", this._onRemoteCursorClick);
},
_onRemoteCursorPositionChange: function () {
loop.request("AddRemoteCursorOverlay", this.props.cursorStore.getStoreState("remoteCursorPosition"));
},
componentWillMount: function componentWillMount() {
this.listenTo(this.props.cursorStore, "change:remoteCursorPosition",
this._onRemoteCursorPositionChange);
this.listenTo(this.props.cursorStore, "change:remoteCursorClick",
this._onRemoteCursorClick);},
_onRemoteCursorClick: function () {
let click = this.props.cursorStore.getStoreState("remoteCursorClick");
_onRemoteCursorPositionChange: function _onRemoteCursorPositionChange() {
loop.request("AddRemoteCursorOverlay",
this.props.cursorStore.getStoreState("remoteCursorPosition"));},
_onRemoteCursorClick: function _onRemoteCursorClick() {
var click = this.props.cursorStore.getStoreState("remoteCursorClick");
// if the click is 'false', assume it is a storeState reset,
// so don't do anything
if (!click) {
return;
}
return;}
this.props.cursorStore.setStoreState({
remoteCursorClick: false
});
loop.request("ClickRemoteCursor", click);
},
this.props.cursorStore.setStoreState({
remoteCursorClick: false });
getInitialState: function () {
return this.getStoreState();
},
_renderFeedbackForm: function () {
loop.request("ClickRemoteCursor", click);},
getInitialState: function getInitialState() {
return this.getStoreState();},
_renderFeedbackForm: function _renderFeedbackForm() {
this.setTitle(mozL10n.get("conversation_has_ended"));
return React.createElement(FeedbackView, {
onAfterFeedbackReceived: this.closeWindow });
},
return React.createElement(FeedbackView, {
onAfterFeedbackReceived: this.closeWindow });},
/**
* We only show the feedback for once every 6 months, otherwise close
* the window.
*/
handleCallTerminated: function () {
this.props.dispatcher.dispatch(new sharedActions.LeaveConversation());
},
handleCallTerminated: function handleCallTerminated() {
this.props.dispatcher.dispatch(new sharedActions.LeaveConversation());},
render: function () {
render: function render() {
if (this.state.showFeedbackForm) {
return this._renderFeedbackForm();
}
return this._renderFeedbackForm();}
switch (this.state.windowType) {
case "room":
{
return React.createElement(DesktopRoomConversationView, {
chatWindowDetached: this.state.chatWindowDetached,
cursorStore: this.props.cursorStore,
dispatcher: this.props.dispatcher,
facebookEnabled: this.state.facebookEnabled,
onCallTerminated: this.handleCallTerminated,
roomStore: this.props.roomStore });
}
case "failed":
{
return React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: FAILURE_DETAILS.UNKNOWN });
}
default:
{
case "room":{
return React.createElement(DesktopRoomConversationView, {
chatWindowDetached: this.state.chatWindowDetached,
cursorStore: this.props.cursorStore,
dispatcher: this.props.dispatcher,
facebookEnabled: this.state.facebookEnabled,
onCallTerminated: this.handleCallTerminated,
roomStore: this.props.roomStore });}
case "failed":{
return React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: FAILURE_DETAILS.UNKNOWN });}
default:{
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
return null;
}
}
}
});
return null;}}} });
/**
* Conversation initialisation.
@@ -114,11 +117,21 @@ loop.conversation = function (mozL10n) {
var hash = locationHash.match(/#(.*)/);
if (hash) {
windowId = hash[1];
}
windowId = hash[1];}
var requests = [
["GetAllConstants"],
["GetAllStrings"],
["GetLocale"],
["GetLoopPref", "ot.guid"],
["GetLoopPref", "feedback.periodSec"],
["GetLoopPref", "feedback.dateLastSeenSec"],
["GetLoopPref", "facebook.enabled"]];
var prefetch = [
["GetConversationWindowData", windowId]];
var requests = [["GetAllConstants"], ["GetAllStrings"], ["GetLocale"], ["GetLoopPref", "ot.guid"], ["GetLoopPref", "feedback.periodSec"], ["GetLoopPref", "feedback.dateLastSeenSec"], ["GetLoopPref", "facebook.enabled"]];
var prefetch = [["GetConversationWindowData", windowId]];
return loop.requestMulti.apply(null, requests.concat(prefetch)).then(function (results) {
// `requestIdx` is keyed off the order of the `requests` and `prefetch`
@@ -129,101 +142,101 @@ loop.conversation = function (mozL10n) {
// else to ensure the L10n environment is setup correctly.
var stringBundle = results[++requestIdx];
var locale = results[++requestIdx];
mozL10n.initialize({
locale: locale,
getStrings: function (key) {
mozL10n.initialize({
locale: locale,
getStrings: function getStrings(key) {
if (!(key in stringBundle)) {
console.error("No string found for key: ", key);
return "{ textContent: '' }";
}
return "{ textContent: '' }";}
return JSON.stringify({ textContent: stringBundle[key] });} });
return JSON.stringify({ textContent: stringBundle[key] });
}
});
// Plug in an alternate client ID mechanism, as localStorage and cookies
// don't work in the conversation window
var currGuid = results[++requestIdx];
window.OT.overrideGuidStorage({
get: function (callback) {
callback(null, currGuid);
},
set: function (guid, callback) {
window.OT.overrideGuidStorage({
get: function get(callback) {
callback(null, currGuid);},
set: function set(guid, callback) {
// See nsIPrefBranch
var PREF_STRING = 32;
currGuid = guid;
loop.request("SetLoopPref", "ot.guid", guid, PREF_STRING);
callback(null);
}
});
callback(null);} });
var dispatcher = new loop.Dispatcher();
var sdkDriver = new loop.OTSdkDriver({
constants: constants,
isDesktop: true,
useDataChannels: true,
dispatcher: dispatcher,
sdk: OT
});
var sdkDriver = new loop.OTSdkDriver({
constants: constants,
isDesktop: true,
useDataChannels: true,
dispatcher: dispatcher,
sdk: OT });
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
// Create the stores.
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
isDesktop: true,
sdkDriver: sdkDriver
});
var conversationAppStore = new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore,
dispatcher: dispatcher,
feedbackPeriod: results[++requestIdx],
feedbackTimestamp: results[++requestIdx],
facebookEnabled: results[++requestIdx]
});
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
isDesktop: true,
sdkDriver: sdkDriver });
var conversationAppStore = new loop.store.ConversationAppStore(dispatcher, {
activeRoomStore: activeRoomStore,
feedbackPeriod: results[++requestIdx],
feedbackTimestamp: results[++requestIdx],
facebookEnabled: results[++requestIdx] });
prefetch.forEach(function (req) {
req.shift();
loop.storeRequest(req, results[++requestIdx]);
});
loop.storeRequest(req, results[++requestIdx]);});
var roomStore = new loop.store.RoomStore(dispatcher, {
activeRoomStore: activeRoomStore,
constants: constants
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: sdkDriver
});
var remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: sdkDriver
});
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
remoteCursorStore: remoteCursorStore,
textChatStore: textChatStore
});
var roomStore = new loop.store.RoomStore(dispatcher, {
activeRoomStore: activeRoomStore,
constants: constants });
React.render(React.createElement(AppControllerView, {
cursorStore: remoteCursorStore,
dispatcher: dispatcher,
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: sdkDriver });
var remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: sdkDriver });
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
remoteCursorStore: remoteCursorStore,
textChatStore: textChatStore });
ReactDOM.render(
React.createElement(AppControllerView, {
cursorStore: remoteCursorStore,
dispatcher: dispatcher,
roomStore: roomStore }), document.querySelector("#main"));
document.documentElement.setAttribute("lang", mozL10n.language.code);
document.documentElement.setAttribute("dir", mozL10n.language.direction);
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId
}));
dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId }));
loop.request("TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);
});
}
return {
AppControllerView: AppControllerView,
init: init,
loop.request("TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);});}
return {
AppControllerView: AppControllerView,
init: init,
/**
* Exposed for the use of functional tests to be able to check
@@ -231,8 +244,8 @@ loop.conversation = function (mozL10n) {
*
* @type loop.OTSdkDriver
*/
_sdkDriver: null
};
}(document.mozL10n);
_sdkDriver: null };}(
document.mozL10n);
document.addEventListener("DOMContentLoaded", loop.conversation.init);

View File

@@ -1,6 +1,6 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.store = loop.store || {};
@@ -9,7 +9,7 @@ loop.store = loop.store || {};
* Manages the conversation window app controller view. Used to get
* the window data and store the window type.
*/
loop.store.ConversationAppStore = (function() {
loop.store.ConversationAppStore = function () {
"use strict";
/**
@@ -18,89 +18,84 @@ loop.store.ConversationAppStore = (function() {
* @param {Object} options Options for the store. Should contain the
* activeRoomStore and dispatcher objects.
*/
var ConversationAppStore = function(options) {
if (!options.activeRoomStore) {
throw new Error("Missing option activeRoomStore");
}
if (!options.dispatcher) {
throw new Error("Missing option dispatcher");
}
if (!("feedbackPeriod" in options)) {
throw new Error("Missing option feedbackPeriod");
}
if (!("feedbackTimestamp" in options)) {
throw new Error("Missing option feedbackTimestamp");
}
var ConversationAppStore = loop.store.createStore({
initialize: function initialize(options) {
if (!options.activeRoomStore) {
throw new Error("Missing option activeRoomStore");}
this._activeRoomStore = options.activeRoomStore;
this._dispatcher = options.dispatcher;
this._facebookEnabled = options.facebookEnabled;
this._feedbackPeriod = options.feedbackPeriod;
this._feedbackTimestamp = options.feedbackTimestamp;
this._rootObj = ("rootObject" in options) ? options.rootObject : window;
this._storeState = this.getInitialStoreState();
if (!("feedbackPeriod" in options)) {
throw new Error("Missing option feedbackPeriod");}
// Start listening for specific events, coming from the window object.
this._eventHandlers = {};
["unload", "LoopHangupNow", "socialFrameAttached", "socialFrameDetached", "ToggleBrowserSharing"]
.forEach(function(eventName) {
if (!("feedbackTimestamp" in options)) {
throw new Error("Missing option feedbackTimestamp");}
this._activeRoomStore = options.activeRoomStore;
this._facebookEnabled = options.facebookEnabled;
this._feedbackPeriod = options.feedbackPeriod;
this._feedbackTimestamp = options.feedbackTimestamp;
this._rootObj = "rootObject" in options ? options.rootObject : window;
this._storeState = this.getInitialStoreState();
// Start listening for specific events, coming from the window object.
this._eventHandlers = {};
["unload", "LoopHangupNow", "socialFrameAttached", "socialFrameDetached", "ToggleBrowserSharing"].
forEach(function (eventName) {
var handlerName = eventName + "Handler";
this._eventHandlers[eventName] = this[handlerName].bind(this);
this._rootObj.addEventListener(eventName, this._eventHandlers[eventName]);
}.bind(this));
this._rootObj.addEventListener(eventName, this._eventHandlers[eventName]);}.
bind(this));
this._dispatcher.register(this, [
"getWindowData",
"showFeedbackForm",
"leaveConversation"
]);
};
this.dispatcher.register(this, [
"getWindowData",
"showFeedbackForm",
"leaveConversation"]);},
ConversationAppStore.prototype = _.extend({
getInitialStoreState: function() {
return {
chatWindowDetached: false,
facebookEnabled: this._facebookEnabled,
getInitialStoreState: function getInitialStoreState() {
return {
chatWindowDetached: false,
facebookEnabled: this._facebookEnabled,
// How often to display the form. Convert seconds to ms.
feedbackPeriod: this._feedbackPeriod * 1000,
feedbackPeriod: this._feedbackPeriod * 1000,
// Date when the feedback form was last presented. Convert to ms.
feedbackTimestamp: this._feedbackTimestamp * 1000,
showFeedbackForm: false
};
},
feedbackTimestamp: this._feedbackTimestamp * 1000,
showFeedbackForm: false };},
/**
* Retrieves current store state.
*
* @return {Object}
*/
getStoreState: function() {
return this._storeState;
},
getStoreState: function getStoreState() {
return this._storeState;},
/**
* Updates store states and trigger a "change" event.
*
* @param {Object} state The new store state.
*/
setStoreState: function(state) {
setStoreState: function setStoreState(state) {
this._storeState = _.extend({}, this._storeState, state);
this.trigger("change");
},
this.trigger("change");},
/**
* Sets store state which will result in the feedback form rendered.
* Saves a timestamp of when the feedback was last rendered.
*/
showFeedbackForm: function() {
showFeedbackForm: function showFeedbackForm() {
var timestamp = Math.floor(new Date().getTime() / 1000);
loop.request("SetLoopPref", "feedback.dateLastSeenSec", timestamp);
this.setStoreState({
showFeedbackForm: true
});
},
this.setStoreState({
showFeedbackForm: true });},
/**
* Handles the get window data action - obtains the window data,
@@ -108,21 +103,21 @@ loop.store.ConversationAppStore = (function() {
*
* @param {sharedActions.GetWindowData} actionData The action data
*/
getWindowData: function(actionData) {
var windowData = loop.getStoredRequest(["GetConversationWindowData",
actionData.windowId]);
getWindowData: function getWindowData(actionData) {
var windowData = loop.getStoredRequest(["GetConversationWindowData",
actionData.windowId]);
if (!windowData) {
console.error("Failed to get the window data");
this.setStoreState({ windowType: "failed" });
return;
}
return;}
this.setStoreState({ windowType: windowData.type });
this._dispatcher.dispatch(new loop.shared.actions.SetupWindowData(_.extend({
windowId: actionData.windowId }, windowData)));
},
this.dispatcher.dispatch(new loop.shared.actions.SetupWindowData(_.extend({
windowId: actionData.windowId }, windowData)));},
/**
* Event handler; invoked when the 'unload' event is dispatched from the
@@ -130,16 +125,16 @@ loop.store.ConversationAppStore = (function() {
* It will dispatch a 'WindowUnload' action that other stores may listen to
* and will remove all event handlers attached to the window object.
*/
unloadHandler: function() {
this._dispatcher.dispatch(new loop.shared.actions.WindowUnload());
unloadHandler: function unloadHandler() {
this.dispatcher.dispatch(new loop.shared.actions.WindowUnload());
// Unregister event handlers.
var eventNames = Object.getOwnPropertyNames(this._eventHandlers);
eventNames.forEach(function(eventName) {
this._rootObj.removeEventListener(eventName, this._eventHandlers[eventName]);
}.bind(this));
this._eventHandlers = null;
},
eventNames.forEach(function (eventName) {
this._rootObj.removeEventListener(eventName, this._eventHandlers[eventName]);}.
bind(this));
this._eventHandlers = null;},
/**
* Event handler; invoked when the 'LoopHangupNow' event is dispatched from
@@ -147,66 +142,64 @@ loop.store.ConversationAppStore = (function() {
* It'll attempt to gracefully disconnect from an active session, or close
* the window when no session is currently active.
*/
LoopHangupNowHandler: function() {
this._dispatcher.dispatch(new loop.shared.actions.LeaveConversation());
},
LoopHangupNowHandler: function LoopHangupNowHandler() {
this.dispatcher.dispatch(new loop.shared.actions.LeaveConversation());},
/**
* Handles leaving the conversation, displaying the feedback form if it
* is time to.
*/
leaveConversation: function() {
if (this.getStoreState().windowType !== "room" ||
!this._activeRoomStore.getStoreState().used ||
this.getStoreState().showFeedbackForm) {
leaveConversation: function leaveConversation() {
if (this.getStoreState().windowType !== "room" ||
!this._activeRoomStore.getStoreState().used ||
this.getStoreState().showFeedbackForm) {
loop.shared.mixins.WindowCloseMixin.closeWindow();
return;
}
return;}
var delta = new Date() - new Date(this.getStoreState().feedbackTimestamp);
// Show timestamp if feedback period (6 months) passed.
// 0 is default value for pref. Always show feedback form on first use.
if (this.getStoreState().feedbackTimestamp === 0 ||
delta >= this.getStoreState().feedbackPeriod) {
this._dispatcher.dispatch(new loop.shared.actions.LeaveRoom({
windowStayingOpen: true
}));
this._dispatcher.dispatch(new loop.shared.actions.ShowFeedbackForm());
return;
}
if (this.getStoreState().feedbackTimestamp === 0 ||
delta >= this.getStoreState().feedbackPeriod) {
this.dispatcher.dispatch(new loop.shared.actions.LeaveRoom({
windowStayingOpen: true }));
this.dispatcher.dispatch(new loop.shared.actions.ShowFeedbackForm());
return;}
loop.shared.mixins.WindowCloseMixin.closeWindow();},
loop.shared.mixins.WindowCloseMixin.closeWindow();
},
/**
* Event handler; invoked when the 'PauseScreenShare' event is dispatched from
* the window object.
* It'll attempt to pause or resume the screen share as appropriate.
*/
ToggleBrowserSharingHandler: function(actionData) {
this._dispatcher.dispatch(new loop.shared.actions.ToggleBrowserSharing({
enabled: !actionData.detail
}));
},
ToggleBrowserSharingHandler: function ToggleBrowserSharingHandler(actionData) {
this.dispatcher.dispatch(new loop.shared.actions.ToggleBrowserSharing({
enabled: !actionData.detail }));},
/**
* Event handler; invoked when the 'socialFrameAttached' event is dispatched
* from the window object.
*/
socialFrameAttachedHandler: function() {
this.setStoreState({ chatWindowDetached: false });
},
socialFrameAttachedHandler: function socialFrameAttachedHandler() {
this.setStoreState({ chatWindowDetached: false });},
/**
* Event handler; invoked when the 'socialFrameDetached' event is dispatched
* from the window object.
*/
socialFrameDetachedHandler: function() {
this.setStoreState({ chatWindowDetached: true });
}
}, Backbone.Events);
socialFrameDetachedHandler: function socialFrameDetachedHandler() {
this.setStoreState({ chatWindowDetached: true });} });
return ConversationAppStore;
})();
return ConversationAppStore;}();

View File

@@ -1,120 +1,107 @@
/* 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 _slicedToArray = function () {function sliceIterator(arr, i) {var _arr = [];var _n = true;var _d = false;var _e = undefined;try {for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {_arr.push(_s.value);if (i && _arr.length === i) break;}} catch (err) {_d = true;_e = err;} finally {try {if (!_n && _i["return"]) _i["return"]();} finally {if (_d) throw _e;}}return _arr;}return function (arr, i) {if (Array.isArray(arr)) {return arr;} else if (Symbol.iterator in Object(arr)) {return sliceIterator(arr, i);} else {throw new TypeError("Invalid attempt to destructure non-iterable instance");}};}(); /* 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 Components */
let { utils: Cu } = Components;
/* global Components */var _Components =
Components;var Cu = _Components.utils;
var loop = loop || {};
loop.copy = (mozL10n => {
loop.copy = function (mozL10n) {
"use strict";
/**
* Copy panel view.
*/
var CopyView = React.createClass({ displayName: "CopyView",
mixins: [React.addons.PureRenderMixin],
let CopyView = React.createClass({
displayName: "CopyView",
getInitialState: function getInitialState() {
return { checked: false };},
mixins: [React.addons.PureRenderMixin],
getInitialState() {
return { checked: false };
},
/**
* Send a message to chrome/bootstrap to handle copy panel events.
* @param {Boolean} accept True if the user clicked accept.
*/
_dispatch(accept) {
window.dispatchEvent(new CustomEvent("CopyPanelClick", {
detail: {
accept,
stop: this.state.checked
}
}));
},
_dispatch: function _dispatch(accept) {
window.dispatchEvent(new CustomEvent("CopyPanelClick", {
detail: {
accept: accept,
stop: this.state.checked } }));},
handleAccept: function handleAccept() {
this._dispatch(true);},
handleCancel: function handleCancel() {
this._dispatch(false);},
handleToggle: function handleToggle() {
this.setState({ checked: !this.state.checked });},
render: function render() {
return (
React.createElement("div", { className: "copy-content" },
React.createElement("div", { className: "copy-body" },
React.createElement("img", { className: "copy-logo", src: "shared/img/helloicon.svg" }),
React.createElement("h1", { className: "copy-message" },
mozL10n.get("copy_panel_message"),
React.createElement("label", { className: "copy-toggle-label" },
React.createElement("input", { onChange: this.handleToggle, type: "checkbox" }),
mozL10n.get("copy_panel_dont_show_again_label")))),
React.createElement("button", { className: "copy-button", onClick: this.handleCancel },
mozL10n.get("copy_panel_cancel_button_label")),
React.createElement("button", { className: "copy-button", onClick: this.handleAccept },
mozL10n.get("copy_panel_accept_button_label"))));} });
handleAccept() {
this._dispatch(true);
},
handleCancel() {
this._dispatch(false);
},
handleToggle() {
this.setState({ checked: !this.state.checked });
},
render() {
return React.createElement(
"div",
{ className: "copy-content" },
React.createElement(
"div",
{ className: "copy-body" },
React.createElement("img", { className: "copy-logo", src: "shared/img/helloicon.svg" }),
React.createElement(
"h1",
{ className: "copy-message" },
mozL10n.get("copy_panel_message"),
React.createElement(
"label",
{ className: "copy-toggle-label" },
React.createElement("input", { onChange: this.handleToggle, type: "checkbox" }),
mozL10n.get("copy_panel_dont_show_again_label")
)
)
),
React.createElement(
"button",
{ className: "copy-button", onClick: this.handleCancel },
mozL10n.get("copy_panel_cancel_button_label")
),
React.createElement(
"button",
{ className: "copy-button", onClick: this.handleAccept },
mozL10n.get("copy_panel_accept_button_label")
)
);
}
});
/**
* Copy panel initialization.
*/
function init() {
// Wait for all LoopAPI message requests to complete before continuing init.
let { LoopAPI } = Cu.import("chrome://loop/content/modules/MozLoopAPI.jsm", {});
let requests = ["GetAllStrings", "GetLocale", "GetPluralRule"];
return Promise.all(requests.map(name => new Promise(resolve => {
LoopAPI.sendMessageToHandler({ name }, resolve);
}))).then(results => {
var _Cu$import = Cu.import("chrome://loop/content/modules/MozLoopAPI.jsm", {});var LoopAPI = _Cu$import.LoopAPI;
var requests = ["GetAllStrings", "GetLocale", "GetPluralRule"];
return Promise.all(requests.map(function (name) {return new Promise(function (resolve) {
LoopAPI.sendMessageToHandler({ name: name }, resolve);});})).
then(function (results) {
// Extract individual requested values to initialize l10n.
let [stringBundle, locale, pluralRule] = results;
mozL10n.initialize({
getStrings(key) {
var _results = _slicedToArray(results, 3);var stringBundle = _results[0];var locale = _results[1];var pluralRule = _results[2];
mozL10n.initialize({
getStrings: function getStrings(key) {
if (!(key in stringBundle)) {
console.error("No string found for key:", key);
}
return JSON.stringify({ textContent: stringBundle[key] || "" });
},
locale,
pluralRule
});
console.error("No string found for key:", key);}
React.render(React.createElement(CopyView, null), document.querySelector("#main"));
return JSON.stringify({ textContent: stringBundle[key] || "" });},
locale: locale,
pluralRule: pluralRule });
ReactDOM.render(React.createElement(CopyView, null), document.querySelector("#main"));
document.documentElement.setAttribute("dir", mozL10n.language.direction);
document.documentElement.setAttribute("lang", mozL10n.language.code);
});
}
document.documentElement.setAttribute("lang", mozL10n.language.code);});}
return {
CopyView,
init
};
})(document.mozL10n);
return {
CopyView: CopyView,
init: init };}(
document.mozL10n);
document.addEventListener("DOMContentLoaded", loop.copy.init);

View File

@@ -1,6 +1,6 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
@@ -11,333 +11,287 @@ loop.shared.desktopViews = function (mozL10n) {
var sharedMixins = loop.shared.mixins;
var sharedUtils = loop.shared.utils;
var CopyLinkButton = React.createClass({
displayName: "CopyLinkButton",
var CopyLinkButton = React.createClass({ displayName: "CopyLinkButton",
statics: {
TRIGGERED_RESET_DELAY: 2000 },
statics: {
TRIGGERED_RESET_DELAY: 2000
},
mixins: [React.addons.PureRenderMixin],
mixins: [React.addons.PureRenderMixin],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired
},
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired },
getInitialState: function () {
return {
copiedUrl: false
};
},
handleCopyButtonClick: function (event) {
getInitialState: function getInitialState() {
return {
copiedUrl: false };},
handleCopyButtonClick: function handleCopyButtonClick(event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.roomData.roomUrl,
from: this.props.locationForMetrics
}));
this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({
roomUrl: this.props.roomData.roomUrl,
from: this.props.locationForMetrics }));
this.setState({ copiedUrl: true });
setTimeout(this.resetTriggeredButtons, this.constructor.TRIGGERED_RESET_DELAY);
},
setTimeout(this.resetTriggeredButtons, this.constructor.TRIGGERED_RESET_DELAY);},
/**
* Reset state of triggered buttons if necessary
*/
resetTriggeredButtons: function () {
resetTriggeredButtons: function resetTriggeredButtons() {
if (this.state.copiedUrl) {
this.setState({ copiedUrl: false });
this.props.callback && this.props.callback();
}
},
this.props.callback && this.props.callback();}},
render: function () {
render: function render() {
var cx = classNames;
return React.createElement(
"div",
{ className: cx({
"group-item-bottom": true,
"btn": true,
"invite-button": true,
"btn-copy": true,
"triggered": this.state.copiedUrl
}),
onClick: this.handleCopyButtonClick },
mozL10n.get(this.state.copiedUrl ? "invite_copied_link_button" : "invite_copy_link_button")
);
}
});
return (
React.createElement("div", { className: cx({
"group-item-bottom": true,
"btn": true,
"invite-button": true,
"btn-copy": true,
"triggered": this.state.copiedUrl }),
var EmailLinkButton = React.createClass({
displayName: "EmailLinkButton",
onClick: this.handleCopyButtonClick }, mozL10n.get(this.state.copiedUrl ?
"invite_copied_link_button" : "invite_copy_link_button")));} });
mixins: [React.addons.PureRenderMixin],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired
},
handleEmailButtonClick: function (event) {
var EmailLinkButton = React.createClass({ displayName: "EmailLinkButton",
mixins: [React.addons.PureRenderMixin],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired },
handleEmailButtonClick: function handleEmailButtonClick(event) {
event.preventDefault();
var roomData = this.props.roomData;
var contextURL = roomData.roomContextUrls && roomData.roomContextUrls[0];
if (contextURL) {
if (contextURL.location === null) {
contextURL = undefined;
} else {
contextURL = sharedUtils.formatURL(contextURL.location).hostname;
}
}
contextURL = undefined;} else
{
contextURL = sharedUtils.formatURL(contextURL.location).hostname;}}
this.props.dispatcher.dispatch(new sharedActions.EmailRoomUrl({
roomUrl: roomData.roomUrl,
roomDescription: contextURL,
from: this.props.locationForMetrics
}));
this.props.callback && this.props.callback();
},
render: function () {
return React.createElement(
"div",
{ className: "btn-email invite-button",
onClick: this.handleEmailButtonClick },
React.createElement("img", { src: "shared/img/glyph-email-16x16.svg" }),
React.createElement(
"p",
null,
mozL10n.get("invite_email_link_button")
)
);
}
});
this.props.dispatcher.dispatch(
new sharedActions.EmailRoomUrl({
roomUrl: roomData.roomUrl,
roomDescription: contextURL,
from: this.props.locationForMetrics }));
var FacebookShareButton = React.createClass({
displayName: "FacebookShareButton",
mixins: [React.addons.PureRenderMixin],
this.props.callback && this.props.callback();},
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired
},
handleFacebookButtonClick: function (event) {
render: function render() {
return (
React.createElement("div", { className: "btn-email invite-button",
onClick: this.handleEmailButtonClick },
React.createElement("img", { src: "shared/img/glyph-email-16x16.svg" }),
React.createElement("p", null, mozL10n.get("invite_email_link_button"))));} });
var FacebookShareButton = React.createClass({ displayName: "FacebookShareButton",
mixins: [React.addons.PureRenderMixin],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
roomData: React.PropTypes.object.isRequired },
handleFacebookButtonClick: function handleFacebookButtonClick(event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.FacebookShareRoomUrl({
from: this.props.locationForMetrics,
roomUrl: this.props.roomData.roomUrl
}));
this.props.dispatcher.dispatch(new sharedActions.FacebookShareRoomUrl({
from: this.props.locationForMetrics,
roomUrl: this.props.roomData.roomUrl }));
this.props.callback && this.props.callback();
},
render: function () {
return React.createElement(
"div",
{ className: "btn-facebook invite-button",
onClick: this.handleFacebookButtonClick },
React.createElement("img", { src: "shared/img/glyph-facebook-16x16.svg" }),
React.createElement(
"p",
null,
mozL10n.get("invite_facebook_button3")
)
);
}
});
this.props.callback && this.props.callback();},
var SharePanelView = React.createClass({
displayName: "SharePanelView",
mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
render: function render() {
return (
React.createElement("div", { className: "btn-facebook invite-button",
onClick: this.handleFacebookButtonClick },
React.createElement("img", { src: "shared/img/glyph-facebook-16x16.svg" }),
React.createElement("p", null, mozL10n.get("invite_facebook_button3"))));} });
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
facebookEnabled: React.PropTypes.bool.isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
var SharePanelView = React.createClass({ displayName: "SharePanelView",
mixins: [sharedMixins.DropdownMenuMixin(".room-invitation-overlay")],
propTypes: {
callback: React.PropTypes.func,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
facebookEnabled: React.PropTypes.bool.isRequired,
locationForMetrics: React.PropTypes.string.isRequired,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array },
render: function () {
render: function render() {var _this = this;
if (!this.props.show || !this.props.roomData.roomUrl) {
return null;
}
return null;}
var cx = classNames;
return React.createElement(
"div",
{ className: "room-invitation-overlay" },
React.createElement(
"div",
{ className: "room-invitation-content" },
React.createElement(
"div",
{ className: "room-context-header" },
mozL10n.get("invite_header_text_bold2")
),
React.createElement(
"div",
null,
mozL10n.get("invite_header_text4")
)
),
React.createElement(
"div",
{ className: "input-button-group" },
React.createElement(
"div",
{ className: "input-button-group-label" },
mozL10n.get("invite_your_link")
),
React.createElement(
"div",
{ className: "input-button-content" },
React.createElement(
"div",
{ className: "input-group group-item-top" },
React.createElement("input", { readOnly: true, type: "text", value: this.props.roomData.roomUrl })
),
React.createElement(CopyLinkButton, {
callback: this.props.callback,
dispatcher: this.props.dispatcher,
locationForMetrics: this.props.locationForMetrics,
roomData: this.props.roomData })
)
),
React.createElement(
"div",
{ className: cx({
"btn-group": true,
"share-action-group": true
}) },
React.createElement(EmailLinkButton, {
callback: this.props.callback,
dispatcher: this.props.dispatcher,
locationForMetrics: this.props.locationForMetrics,
roomData: this.props.roomData }),
(() => {
if (this.props.facebookEnabled) {
return React.createElement(FacebookShareButton, {
callback: this.props.callback,
dispatcher: this.props.dispatcher,
locationForMetrics: this.props.locationForMetrics,
roomData: this.props.roomData });
}
return null;
})()
),
React.createElement(SocialShareDropdown, {
dispatcher: this.props.dispatcher,
ref: "menu",
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
socialShareProviders: this.props.socialShareProviders })
);
}
});
return (
React.createElement("div", { className: "room-invitation-overlay" },
React.createElement("div", { className: "room-invitation-content" },
React.createElement("div", { className: "room-context-header" }, mozL10n.get("invite_header_text_bold2")),
React.createElement("div", null, mozL10n.get("invite_header_text4"))),
var SocialShareDropdown = React.createClass({
displayName: "SocialShareDropdown",
React.createElement("div", { className: "input-button-group" },
React.createElement("div", { className: "input-button-group-label" }, mozL10n.get("invite_your_link")),
React.createElement("div", { className: "input-button-content" },
React.createElement("div", { className: "input-group group-item-top" },
React.createElement("input", { readOnly: true, type: "text", value: this.props.roomData.roomUrl })),
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomUrl: React.PropTypes.string,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array
},
React.createElement(CopyLinkButton, {
callback: this.props.callback,
dispatcher: this.props.dispatcher,
locationForMetrics: this.props.locationForMetrics,
roomData: this.props.roomData }))),
handleAddServiceClick: function (event) {
React.createElement("div", { className: cx({
"btn-group": true,
"share-action-group": true }) },
React.createElement(EmailLinkButton, {
callback: this.props.callback,
dispatcher: this.props.dispatcher,
locationForMetrics: this.props.locationForMetrics,
roomData: this.props.roomData }),
function () {
if (_this.props.facebookEnabled) {
return React.createElement(FacebookShareButton, {
callback: _this.props.callback,
dispatcher: _this.props.dispatcher,
locationForMetrics: _this.props.locationForMetrics,
roomData: _this.props.roomData });}
return null;}()),
React.createElement(SocialShareDropdown, {
dispatcher: this.props.dispatcher,
ref: "menu",
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
socialShareProviders: this.props.socialShareProviders })));} });
var SocialShareDropdown = React.createClass({ displayName: "SocialShareDropdown",
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomUrl: React.PropTypes.string,
show: React.PropTypes.bool.isRequired,
socialShareProviders: React.PropTypes.array },
handleAddServiceClick: function handleAddServiceClick(event) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());
},
this.props.dispatcher.dispatch(new sharedActions.AddSocialShareProvider());},
handleProviderClick: function (event) {
handleProviderClick: function handleProviderClick(event) {
event.preventDefault();
var origin = event.currentTarget.dataset.provider;
var provider = this.props.socialShareProviders.filter(function (socialProvider) {
return socialProvider.origin === origin;
})[0];
var provider = this.props.socialShareProviders.
filter(function (socialProvider) {
return socialProvider.origin === origin;})[
0];
this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
provider: provider,
roomUrl: this.props.roomUrl,
previews: []
}));
},
this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
provider: provider,
roomUrl: this.props.roomUrl,
previews: [] }));},
render: function () {
render: function render() {
// Don't render a thing when no data has been fetched yet.
if (!this.props.socialShareProviders) {
return null;
}
return null;}
var cx = classNames;
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"visually-hidden": true,
"hide": !this.props.show
});
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"visually-hidden": true,
"hide": !this.props.show });
return (
React.createElement("ul", { className: shareDropdown },
React.createElement("li", { className: "dropdown-menu-item", onClick: this.handleAddServiceClick },
React.createElement("i", { className: "icon icon-add-share-service" }),
React.createElement("span", null, mozL10n.get("share_add_service_button"))),
this.props.socialShareProviders.length ? React.createElement("li", { className: "dropdown-menu-separator" }) : null,
return React.createElement(
"ul",
{ className: shareDropdown },
React.createElement(
"li",
{ className: "dropdown-menu-item", onClick: this.handleAddServiceClick },
React.createElement("i", { className: "icon icon-add-share-service" }),
React.createElement(
"span",
null,
mozL10n.get("share_add_service_button")
)
),
this.props.socialShareProviders.length ? React.createElement("li", { className: "dropdown-menu-separator" }) : null,
this.props.socialShareProviders.map(function (provider, idx) {
return React.createElement(
"li",
{ className: "dropdown-menu-item",
"data-provider": provider.origin,
key: "provider-" + idx,
onClick: this.handleProviderClick },
React.createElement("img", { className: "icon", src: provider.iconURL }),
React.createElement(
"span",
null,
provider.name
)
);
}.bind(this))
);
}
});
return (
React.createElement("li", { className: "dropdown-menu-item",
"data-provider": provider.origin,
key: "provider-" + idx,
onClick: this.handleProviderClick },
React.createElement("img", { className: "icon", src: provider.iconURL }),
React.createElement("span", null, provider.name)));}.
return {
CopyLinkButton: CopyLinkButton,
EmailLinkButton: EmailLinkButton,
FacebookShareButton: FacebookShareButton,
SharePanelView: SharePanelView,
SocialShareDropdown: SocialShareDropdown
};
}(navigator.mozL10n || document.mozL10n);
bind(this))));} });
return {
CopyLinkButton: CopyLinkButton,
EmailLinkButton: EmailLinkButton,
FacebookShareButton: FacebookShareButton,
SharePanelView: SharePanelView,
SocialShareDropdown: SocialShareDropdown };}(
navigator.mozL10n || document.mozL10n);

View File

@@ -1,6 +1,6 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.feedbackViews = function (_, mozL10n) {
@@ -10,52 +10,46 @@ loop.feedbackViews = function (_, mozL10n) {
* Feedback view is displayed once every 6 months (loop.feedback.periodSec)
* after a conversation has ended.
*/
var FeedbackView = React.createClass({ displayName: "FeedbackView",
propTypes: {
onAfterFeedbackReceived: React.PropTypes.func.isRequired },
var FeedbackView = React.createClass({
displayName: "FeedbackView",
propTypes: {
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
/**
* Pressing the button to leave feedback will open the form in a new page
* and close the conversation window.
*/
onFeedbackButtonClick: function () {
loop.requestMulti(["GetLoopPref", "feedback.formURL"], ["GetAddonVersion"]).then(function (results) {
onFeedbackButtonClick: function onFeedbackButtonClick() {
loop.requestMulti(
["GetLoopPref", "feedback.formURL"],
["GetAddonVersion"]).
then(function (results) {
if (results[0] && results[1]) {
var finalURL = results[0].replace("%APP_VERSION%", results[1]);
loop.request("OpenURL", finalURL).then(this.props.onAfterFeedbackReceived);
}
}.bind(this));
},
loop.request("OpenURL", finalURL).then(this.props.onAfterFeedbackReceived);}}.
render: function () {
return React.createElement(
"div",
{ className: "feedback-view-container" },
React.createElement(
"h2",
{ className: "feedback-heading" },
mozL10n.get("feedback_window_heading")
),
React.createElement("div", { className: "feedback-hello-logo" }),
React.createElement(
"div",
{ className: "feedback-button-container" },
React.createElement(
"button",
{ onClick: this.onFeedbackButtonClick,
ref: "feedbackFormBtn" },
mozL10n.get("feedback_request_button")
)
)
);
}
});
bind(this));},
return {
FeedbackView: FeedbackView
};
}(_, navigator.mozL10n || document.mozL10n);
render: function render() {
return (
React.createElement("div", { className: "feedback-view-container" },
React.createElement("h2", { className: "feedback-heading" },
mozL10n.get("feedback_window_heading")),
React.createElement("div", { className: "feedback-hello-logo" }),
React.createElement("div", { className: "feedback-button-container" },
React.createElement("button", { onClick: this.onFeedbackButtonClick,
ref: "feedbackFormBtn" },
mozL10n.get("feedback_request_button")))));} });
return {
FeedbackView: FeedbackView };}(
_, navigator.mozL10n || document.mozL10n);

View File

@@ -0,0 +1,32 @@
"use strict"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.panel = loop.panel || {};
loop.panel.models = function () {
"use strict";
/**
* Notification model.
*/
var NotificationModel = Backbone.Model.extend({
defaults: {
details: "",
detailsButtonLabel: "",
detailsButtonCallback: null,
level: "info",
message: "" } });
/**
* Notification collection
*/
var NotificationCollection = Backbone.Collection.extend({
model: NotificationModel });
return {
NotificationCollection: NotificationCollection,
NotificationModel: NotificationModel };}();

View File

@@ -1,10 +1,10 @@
/* 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"; /* 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/. */
window.OTProperties = {
cdnURL: "" };
window.OTProperties = {
cdnURL: ""
};
window.OTProperties.assetURL = window.OTProperties.cdnURL + "sdk-content/";
window.OTProperties.configURL = window.OTProperties.assetURL + "js/dynamic_config.min.js";

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +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/. */
"use strict"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.store = loop.store || {};
(function(mozL10n) {
(function (mozL10n) {
"use strict";
/**
@@ -26,28 +26,28 @@ loop.store = loop.store || {};
* Room validation schema. See validate.js.
* @type {Object}
*/
var roomSchema = {
roomToken: String,
roomUrl: String,
var roomSchema = {
roomToken: String,
roomUrl: String,
// roomName: String - Optional.
// roomKey: String - Optional.
maxSize: Number,
participants: Array,
ctime: Number
};
maxSize: Number,
participants: Array,
ctime: Number };
/**
* Room type. Basically acts as a typed object constructor.
*
* @param {Object} values Room property values.
*/
function Room(values) {
var validatedData = new loop.validate.Validator(roomSchema || {})
.validate(values || {});
for (var prop in validatedData) {
this[prop] = validatedData[prop];
}
}
function Room(values) {var _this = this;
var validatedData = new loop.validate.Validator(roomSchema || {}).
validate(values || {});
Object.keys(validatedData).forEach(function (prop) {
_this[prop] = validatedData[prop];});}
loop.store.Room = Room;
@@ -63,43 +63,43 @@ loop.store = loop.store || {};
* - {Object} constants A set of constants that are used
* throughout the store.
*/
loop.store.RoomStore = loop.store.createStore({
loop.store.RoomStore = loop.store.createStore({
/**
* Maximum size given to createRoom; only 2 is supported (and is
* always passed) because that's what the user-experience is currently
* designed and tested to handle.
* @type {Number}
*/
maxRoomCreationSize: MAX_ROOM_CREATION_SIZE,
maxRoomCreationSize: MAX_ROOM_CREATION_SIZE,
/**
* Registered actions.
* @type {Array}
*/
actions: [
"addSocialShareProvider",
"createRoom",
"createdRoom",
"createRoomError",
"copyRoomUrl",
"deleteRoom",
"deleteRoomError",
"emailRoomUrl",
"facebookShareRoomUrl",
"getAllRooms",
"getAllRoomsError",
"openRoom",
"shareRoomUrl",
"updateRoomContext",
"updateRoomContextDone",
"updateRoomContextError",
"updateRoomList"
],
"addSocialShareProvider",
"createRoom",
"createdRoom",
"createRoomError",
"copyRoomUrl",
"deleteRoom",
"deleteRoomError",
"emailRoomUrl",
"facebookShareRoomUrl",
"getAllRooms",
"getAllRoomsError",
"openRoom",
"shareRoomUrl",
"updateRoomContext",
"updateRoomContextDone",
"updateRoomContextError",
"updateRoomList"],
initialize: function(options) {
initialize: function initialize(options) {
if (!options.constants) {
throw new Error("Missing option constants");
}
throw new Error("Missing option constants");}
this._notifications = options.notifications;
this._constants = options.constants;
@@ -107,132 +107,132 @@ loop.store = loop.store || {};
if (options.activeRoomStore) {
this.activeRoomStore = options.activeRoomStore;
this.activeRoomStore.on("change",
this._onActiveRoomStoreChange.bind(this));
}
},
this.activeRoomStore.on("change",
this._onActiveRoomStoreChange.bind(this));}},
getInitialStoreState: function getInitialStoreState() {
return {
activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
closingNewRoom: false,
error: null,
lastCreatedRoom: null,
openedRoom: null,
pendingCreation: false,
pendingInitialRetrieval: true,
rooms: [],
savingContext: false };},
getInitialStoreState: function() {
return {
activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
closingNewRoom: false,
error: null,
lastCreatedRoom: null,
openedRoom: null,
pendingCreation: false,
pendingInitialRetrieval: true,
rooms: [],
savingContext: false
};
},
/**
* Registers Loop API rooms events.
*/
startListeningToRoomEvents: function() {
startListeningToRoomEvents: function startListeningToRoomEvents() {
// Rooms event registration
loop.request("Rooms:PushSubscription", ["add", "close", "delete", "open",
"refresh", "update"]);
loop.request("Rooms:PushSubscription", ["add", "close", "delete", "open",
"refresh", "update"]);
loop.subscribe("Rooms:Add", this._onRoomAdded.bind(this));
loop.subscribe("Rooms:Close", this._onRoomClose.bind(this));
loop.subscribe("Rooms:Open", this._onRoomOpen.bind(this));
loop.subscribe("Rooms:Update", this._onRoomUpdated.bind(this));
loop.subscribe("Rooms:Delete", this._onRoomRemoved.bind(this));
loop.subscribe("Rooms:Refresh", this._onRoomsRefresh.bind(this));
},
loop.subscribe("Rooms:Refresh", this._onRoomsRefresh.bind(this));},
/**
* Updates active room store state.
*/
_onActiveRoomStoreChange: function() {
this.setStoreState({ activeRoom: this.activeRoomStore.getStoreState() });
},
_onActiveRoomStoreChange: function _onActiveRoomStoreChange() {
this.setStoreState({ activeRoom: this.activeRoomStore.getStoreState() });},
/**
* Updates current room list when a new room is available.
*
* @param {Object} addedRoomData The added room data.
*/
_onRoomAdded: function(addedRoomData) {
_onRoomAdded: function _onRoomAdded(addedRoomData) {
addedRoomData.participants = addedRoomData.participants || [];
addedRoomData.ctime = addedRoomData.ctime || new Date().getTime();
this.dispatchAction(new sharedActions.UpdateRoomList({
this.dispatchAction(new sharedActions.UpdateRoomList({
// Ensure the room isn't part of the list already, then add it.
roomList: this._storeState.rooms.filter(function(room) {
return addedRoomData.roomToken !== room.roomToken;
}).concat(new Room(addedRoomData))
}));
},
roomList: this._storeState.rooms.filter(function (room) {
return addedRoomData.roomToken !== room.roomToken;}).
concat(new Room(addedRoomData)) }));},
/**
* Clears the current active room.
*/
_onRoomClose: function() {
let state = this.getStoreState();
_onRoomClose: function _onRoomClose() {
var state = this.getStoreState();
// If the room getting closed has been just created, then open the panel.
if (state.lastCreatedRoom && state.openedRoom === state.lastCreatedRoom) {
this.setStoreState({
closingNewRoom: true
});
loop.request("SetNameNewRoom");
}
this.setStoreState({
closingNewRoom: true });
loop.request("SetNameNewRoom");}
// reset state for closed room
this.setStoreState({
closingNewRoom: false,
lastCreatedRoom: null,
openedRoom: null
});
},
this.setStoreState({
closingNewRoom: false,
lastCreatedRoom: null,
openedRoom: null });},
/**
* Updates the current active room.
*
* @param {String} roomToken Identifier of the room.
*/
_onRoomOpen: function(roomToken) {
this.setStoreState({
openedRoom: roomToken
});
},
_onRoomOpen: function _onRoomOpen(roomToken) {
this.setStoreState({
openedRoom: roomToken });},
/**
* Executed when a room is updated.
*
* @param {Object} updatedRoomData The updated room data.
*/
_onRoomUpdated: function(updatedRoomData) {
this.dispatchAction(new sharedActions.UpdateRoomList({
roomList: this._storeState.rooms.map(function(room) {
return room.roomToken === updatedRoomData.roomToken ?
updatedRoomData : room;
})
}));
},
_onRoomUpdated: function _onRoomUpdated(updatedRoomData) {
this.dispatchAction(new sharedActions.UpdateRoomList({
roomList: this._storeState.rooms.map(function (room) {
return room.roomToken === updatedRoomData.roomToken ?
updatedRoomData : room;}) }));},
/**
* Executed when a room is deleted.
*
* @param {Object} removedRoomData The removed room data.
*/
_onRoomRemoved: function(removedRoomData) {
this.dispatchAction(new sharedActions.UpdateRoomList({
roomList: this._storeState.rooms.filter(function(room) {
return room.roomToken !== removedRoomData.roomToken;
})
}));
},
_onRoomRemoved: function _onRoomRemoved(removedRoomData) {
this.dispatchAction(new sharedActions.UpdateRoomList({
roomList: this._storeState.rooms.filter(function (room) {
return room.roomToken !== removedRoomData.roomToken;}) }));},
/**
* Executed when the user switches accounts.
*/
_onRoomsRefresh: function() {
this.dispatchAction(new sharedActions.UpdateRoomList({
roomList: []
}));
},
_onRoomsRefresh: function _onRoomsRefresh() {
this.dispatchAction(new sharedActions.UpdateRoomList({
roomList: [] }));},
/**
* Maps and sorts the raw room list received from the Loop API.
@@ -240,189 +240,189 @@ loop.store = loop.store || {};
* @param {Array} rawRoomList Raw room list.
* @return {Array}
*/
_processRoomList: function(rawRoomList) {
_processRoomList: function _processRoomList(rawRoomList) {
if (!rawRoomList) {
return [];
}
return rawRoomList
.map(function(rawRoom) {
return new Room(rawRoom);
})
.slice()
.sort(function(a, b) {
return b.ctime - a.ctime;
});
},
return [];}
return rawRoomList.
map(function (rawRoom) {
return new Room(rawRoom);}).
slice().
sort(function (a, b) {
return b.ctime - a.ctime;});},
/**
* Creates a new room.
*
* @param {sharedActions.CreateRoom} actionData The new room information.
*/
createRoom: function(actionData) {
this.setStoreState({
pendingCreation: true,
error: null
});
createRoom: function createRoom(actionData) {
this.setStoreState({
pendingCreation: true,
error: null });
var roomCreationData = {
decryptedContext: {},
maxSize: this.maxRoomCreationSize };
var roomCreationData = {
decryptedContext: {},
maxSize: this.maxRoomCreationSize
};
if ("urls" in actionData) {
roomCreationData.decryptedContext.urls = actionData.urls;
}
roomCreationData.decryptedContext.urls = actionData.urls;}
this._notifications.remove("create-room-error");
loop.request("Rooms:Create", roomCreationData).then(function(result) {
loop.request("Rooms:Create", roomCreationData).then(function (result) {
var buckets = this._constants.ROOM_CREATE;
if (!result || result.isError) {
loop.request("TelemetryAddValue", "LOOP_ROOM_CREATE", buckets.CREATE_FAIL);
this.dispatchAction(new sharedActions.CreateRoomError({
error: result ? result : new Error("no result")
}));
return;
}
this.dispatchAction(new sharedActions.CreateRoomError({
error: result ? result : new Error("no result") }));
return;}
// Keep the token for the last created room.
this.setStoreState({
lastCreatedRoom: result.roomToken
});
this.setStoreState({
lastCreatedRoom: result.roomToken });
this.dispatchAction(new sharedActions.CreatedRoom({
decryptedContext: result.decryptedContext,
roomToken: result.roomToken,
roomUrl: result.roomUrl }));
loop.request("TelemetryAddValue", "LOOP_ROOM_CREATE", buckets.CREATE_SUCCESS);}.
bind(this));},
this.dispatchAction(new sharedActions.CreatedRoom({
decryptedContext: result.decryptedContext,
roomToken: result.roomToken,
roomUrl: result.roomUrl
}));
loop.request("TelemetryAddValue", "LOOP_ROOM_CREATE", buckets.CREATE_SUCCESS);
}.bind(this));
},
/**
* Executed when a room has been created
*/
createdRoom: function(actionData) {
this.setStoreState({
activeRoom: {
decryptedContext: actionData.decryptedContext,
roomToken: actionData.roomToken,
roomUrl: actionData.roomUrl
},
pendingCreation: false
});
},
createdRoom: function createdRoom(actionData) {
this.setStoreState({
activeRoom: {
decryptedContext: actionData.decryptedContext,
roomToken: actionData.roomToken,
roomUrl: actionData.roomUrl },
pendingCreation: false });},
/**
* Executed when a room creation error occurs.
*
* @param {sharedActions.CreateRoomError} actionData The action data.
*/
createRoomError: function(actionData) {
this.setStoreState({
error: actionData.error,
pendingCreation: false
});
createRoomError: function createRoomError(actionData) {
this.setStoreState({
error: actionData.error,
pendingCreation: false });
// XXX Needs a more descriptive error - bug 1109151.
this._notifications.set({
id: "create-room-error",
level: "error",
message: mozL10n.get("generic_failure_message")
});
},
this._notifications.set({
id: "create-room-error",
level: "error",
message: mozL10n.get("generic_failure_message") });},
/**
* Copy a room url.
*
* @param {sharedActions.CopyRoomUrl} actionData The action data.
*/
copyRoomUrl: function(actionData) {
copyRoomUrl: function copyRoomUrl(actionData) {
loop.requestMulti(
["CopyString", actionData.roomUrl],
["NotifyUITour", "Loop:RoomURLCopied"]);
["CopyString", actionData.roomUrl],
["NotifyUITour", "Loop:RoomURLCopied"]);
var from = actionData.from;
var bucket = this._constants.SHARING_ROOM_URL["COPY_FROM_" + from.toUpperCase()];
if (typeof bucket === "undefined") {
console.error("No URL sharing type bucket found for '" + from + "'");
return;
}
return;}
loop.requestMulti(
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
},
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]);},
/**
* Emails a room url.
*
* @param {sharedActions.EmailRoomUrl} actionData The action data.
*/
emailRoomUrl: function(actionData) {
emailRoomUrl: function emailRoomUrl(actionData) {
var from = actionData.from;
loop.shared.utils.composeCallUrlEmail(actionData.roomUrl, null,
actionData.roomDescription);
loop.shared.utils.composeCallUrlEmail(actionData.roomUrl, null,
actionData.roomDescription);
var bucket = this._constants.SHARING_ROOM_URL[
"EMAIL_FROM_" + (from || "").toUpperCase()
];
"EMAIL_FROM_" + (from || "").toUpperCase()];
if (typeof bucket === "undefined") {
console.error("No URL sharing type bucket found for '" + from + "'");
return;
}
return;}
loop.requestMulti(
["NotifyUITour", "Loop:RoomURLEmailed"],
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
},
["NotifyUITour", "Loop:RoomURLEmailed"],
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]);},
/**
* Share a room url with Facebook
*
* @param {sharedActions.FacebookShareRoomUrl} actionData The action data.
*/
facebookShareRoomUrl: function(actionData) {
facebookShareRoomUrl: function facebookShareRoomUrl(actionData) {
var encodedRoom = encodeURIComponent(actionData.roomUrl);
loop.requestMulti(
["GetLoopPref", "facebook.appId"],
["GetLoopPref", "facebook.fallbackUrl"],
["GetLoopPref", "facebook.shareUrl"]
).then(results => {
["GetLoopPref", "facebook.appId"],
["GetLoopPref", "facebook.fallbackUrl"],
["GetLoopPref", "facebook.shareUrl"]).
then(function (results) {
var app_id = results[0];
var fallback_url = results[1];
var redirect_url = encodeURIComponent(actionData.originUrl ||
fallback_url);
var redirect_url = encodeURIComponent(actionData.originUrl ||
fallback_url);
var finalURL = results[2].replace("%ROOM_URL%", encodedRoom)
.replace("%APP_ID%", app_id)
.replace("%REDIRECT_URI%", redirect_url);
var finalURL = results[2].replace("%ROOM_URL%", encodedRoom).
replace("%APP_ID%", app_id).
replace("%REDIRECT_URI%", redirect_url);
return loop.request("OpenURL", finalURL);}).
then(function () {
loop.request("NotifyUITour", "Loop:RoomURLShared");});
return loop.request("OpenURL", finalURL);
}).then(() => {
loop.request("NotifyUITour", "Loop:RoomURLShared");
});
var from = actionData.from;
var bucket = this._constants.SHARING_ROOM_URL["FACEBOOK_FROM_" + from.toUpperCase()];
if (typeof bucket === "undefined") {
console.error("No URL sharing type bucket found for '" + from + "'");
return;
}
return;}
loop.requestMulti(
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]
);
},
["TelemetryAddValue", "LOOP_SHARING_ROOM_URL", bucket],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_SHARE]);},
/**
* Share a room url.
*
* @param {sharedActions.ShareRoomUrl} actionData The action data.
*/
shareRoomUrl: function(actionData) {
shareRoomUrl: function shareRoomUrl(actionData) {
var providerOrigin = new URL(actionData.provider.origin).hostname;
var shareTitle = "";
var shareBody = null;
@@ -430,81 +430,81 @@ loop.store = loop.store || {};
switch (providerOrigin) {
case "mail.google.com":
shareTitle = mozL10n.get("share_email_subject7");
shareBody = mozL10n.get("share_email_body7", {
callUrl: actionData.roomUrl
});
shareBody = mozL10n.get("share_email_body7", {
callUrl: actionData.roomUrl });
shareBody += mozL10n.get("share_email_footer2");
break;
case "twitter.com":
default:
shareTitle = mozL10n.get("share_tweet", {
clientShortname2: mozL10n.get("clientShortname2")
});
break;
}
shareTitle = mozL10n.get("share_tweet", {
clientShortname2: mozL10n.get("clientShortname2") });
break;}
loop.requestMulti(
["SocialShareRoom", actionData.provider.origin, actionData.roomUrl,
shareTitle, shareBody],
["NotifyUITour", "Loop:RoomURLShared"]);
},
["SocialShareRoom", actionData.provider.origin, actionData.roomUrl,
shareTitle, shareBody],
["NotifyUITour", "Loop:RoomURLShared"]);},
/**
* Open the share panel to add a Social share provider.
*/
addSocialShareProvider: function() {
loop.request("AddSocialShareProvider");
},
addSocialShareProvider: function addSocialShareProvider() {
loop.request("AddSocialShareProvider");},
/**
* Creates a new room.
*
* @param {sharedActions.DeleteRoom} actionData The action data.
*/
deleteRoom: function(actionData) {
loop.request("Rooms:Delete", actionData.roomToken).then(function(result) {
var isError = (result && result.isError);
deleteRoom: function deleteRoom(actionData) {
loop.request("Rooms:Delete", actionData.roomToken).then(function (result) {
var isError = result && result.isError;
if (isError) {
this.dispatchAction(new sharedActions.DeleteRoomError({ error: result }));
}
this.dispatchAction(new sharedActions.DeleteRoomError({ error: result }));}
var buckets = this._constants.ROOM_DELETE;
loop.requestMulti(
["TelemetryAddValue", "LOOP_ROOM_DELETE", buckets[isError ?
"DELETE_FAIL" : "DELETE_SUCCESS"]],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_DELETE]
);
}.bind(this));
},
["TelemetryAddValue", "LOOP_ROOM_DELETE", buckets[isError ?
"DELETE_FAIL" : "DELETE_SUCCESS"]],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_DELETE]);}.
bind(this));},
/**
* Executed when a room deletion error occurs.
*
* @param {sharedActions.DeleteRoomError} actionData The action data.
*/
deleteRoomError: function(actionData) {
this.setStoreState({ error: actionData.error });
},
deleteRoomError: function deleteRoomError(actionData) {
this.setStoreState({ error: actionData.error });},
/**
* Gather the list of all available rooms from the Loop API.
*/
getAllRooms: function() {
getAllRooms: function getAllRooms() {
// XXX Ideally, we'd have a specific command to "start up" the room store
// to get the rooms. We should address this alongside bug 1074665.
if (this._gotAllRooms) {
return;
}
return;}
loop.request("Rooms:GetAll", null).then(function(result) {
loop.request("Rooms:GetAll", null).then(function (result) {
var action;
this.setStoreState({ pendingInitialRetrieval: false });
if (result && result.isError) {
action = new sharedActions.GetAllRoomsError({ error: result });
} else {
action = new sharedActions.UpdateRoomList({ roomList: result });
}
action = new sharedActions.GetAllRoomsError({ error: result });} else
{
action = new sharedActions.UpdateRoomList({ roomList: result });}
this.dispatchAction(action);
@@ -512,74 +512,74 @@ loop.store = loop.store || {};
// We can only start listening to room events after getAll() has been
// called executed first.
this.startListeningToRoomEvents();
}.bind(this));
},
this.startListeningToRoomEvents();}.
bind(this));},
/**
* Updates current error state in case getAllRooms failed.
*
* @param {sharedActions.GetAllRoomsError} actionData The action data.
*/
getAllRoomsError: function(actionData) {
this.setStoreState({ error: actionData.error });
},
getAllRoomsError: function getAllRoomsError(actionData) {
this.setStoreState({ error: actionData.error });},
/**
* Updates current room list.
*
* @param {sharedActions.UpdateRoomList} actionData The action data.
*/
updateRoomList: function(actionData) {
this.setStoreState({
error: undefined,
rooms: this._processRoomList(actionData.roomList)
});
},
updateRoomList: function updateRoomList(actionData) {
this.setStoreState({
error: undefined,
rooms: this._processRoomList(actionData.roomList) });},
/**
* Opens a room
*
* @param {sharedActions.OpenRoom} actionData The action data.
*/
openRoom: function(actionData) {
openRoom: function openRoom(actionData) {
loop.requestMulti(
["Rooms:Open", actionData.roomToken],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_OPEN]
);
},
["Rooms:Open", actionData.roomToken],
["TelemetryAddValue", "LOOP_ACTIVITY_COUNTER", this._constants.LOOP_MAU_TYPE.ROOM_OPEN]);},
/**
* Updates the context data attached to a room.
*
* @param {sharedActions.UpdateRoomContext} actionData
*/
updateRoomContext: function(actionData) {
updateRoomContext: function updateRoomContext(actionData) {
this.setStoreState({ savingContext: true });
loop.request("Rooms:Get", actionData.roomToken).then(function(result) {
loop.request("Rooms:Get", actionData.roomToken).then(function (result) {
if (result.isError) {
this.dispatchAction(new sharedActions.UpdateRoomContextError({
error: result
}));
return;
}
this.dispatchAction(new sharedActions.UpdateRoomContextError({
error: result }));
return;}
var roomData = {};
var context = result.decryptedContext;
var oldRoomName = context.roomName;
var newRoomName = (actionData.newRoomName || "").trim();
if (newRoomName && oldRoomName !== newRoomName) {
roomData.roomName = newRoomName;
}
roomData.roomName = newRoomName;}
var oldRoomURLs = context.urls;
var oldRoomURL = oldRoomURLs && oldRoomURLs[0];
// Since we want to prevent storing falsy (i.e. empty) values for context
// data, there's no need to send that to the server as an update.
var newRoomURL = loop.shared.utils.stripFalsyValues({
location: (actionData.newRoomURL || "").trim(),
thumbnail: (actionData.newRoomThumbnail || "").trim(),
description: (actionData.newRoomDescription || "").trim()
});
var newRoomURL = loop.shared.utils.stripFalsyValues({
location: (actionData.newRoomURL || "").trim(),
thumbnail: (actionData.newRoomThumbnail || "").trim(),
description: (actionData.newRoomDescription || "").trim() });
// Only attach a context to the room when
// 1) there was already a URL set,
// 2) a new URL is provided as of now,
@@ -589,14 +589,14 @@ loop.store = loop.store || {};
newRoomURL = _.extend(oldRoomURL || {}, newRoomURL);
var isValidURL = false;
try {
isValidURL = new URL(newRoomURL.location);
} catch (ex) {
isValidURL = new URL(newRoomURL.location);}
catch (ex) {
// URL may throw, default to false;
}
if (isValidURL) {
roomData.urls = [newRoomURL];
}
}
roomData.urls = [newRoomURL];}}
// TODO: there currently is no clear UX defined on what to do when all
// context data was cleared, e.g. when diff.removed contains all the
// context properties. Until then, we can't deal with context removal here.
@@ -606,40 +606,40 @@ loop.store = loop.store || {};
if (!Object.getOwnPropertyNames(roomData).length) {
// Ensure async actions so that we get separate setStoreState events
// that React components won't miss.
setTimeout(function() {
this.dispatchAction(new sharedActions.UpdateRoomContextDone());
}.bind(this), 0);
return;
}
setTimeout(function () {
this.dispatchAction(new sharedActions.UpdateRoomContextDone());}.
bind(this), 0);
return;}
this.setStoreState({ error: null });
loop.request("Rooms:Update", actionData.roomToken, roomData).then(function(result2) {
var isError = (result2 && result2.isError);
var action = isError ?
new sharedActions.UpdateRoomContextError({ error: result2 }) :
new sharedActions.UpdateRoomContextDone();
this.dispatchAction(action);
}.bind(this));
}.bind(this));
},
loop.request("Rooms:Update", actionData.roomToken, roomData).then(function (result2) {
var isError = result2 && result2.isError;
var action = isError ?
new sharedActions.UpdateRoomContextError({ error: result2 }) :
new sharedActions.UpdateRoomContextDone();
this.dispatchAction(action);}.
bind(this));}.
bind(this));},
/**
* Handles the updateRoomContextDone action.
*/
updateRoomContextDone: function() {
this.setStoreState({ savingContext: false });
},
updateRoomContextDone: function updateRoomContextDone() {
this.setStoreState({ savingContext: false });},
/**
* Updating the context data attached to a room error.
*
* @param {sharedActions.UpdateRoomContextError} actionData
*/
updateRoomContextError: function(actionData) {
this.setStoreState({
error: actionData.error,
savingContext: false
});
}
});
})(document.mozL10n || navigator.mozL10n);
updateRoomContextError: function updateRoomContextError(actionData) {
this.setStoreState({
error: actionData.error,
savingContext: false });} });})(
document.mozL10n || navigator.mozL10n);

View File

@@ -1,6 +1,6 @@
/* 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 _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {return typeof obj;} : function (obj) {return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj;}; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.roomViews = function (mozL10n) {
@@ -17,221 +17,221 @@ loop.roomViews = function (mozL10n) {
* ActiveRoomStore mixin.
* @type {Object}
*/
var ActiveRoomStoreMixin = {
mixins: [Backbone.Events],
var ActiveRoomStoreMixin = {
mixins: [Backbone.Events],
propTypes: {
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
propTypes: {
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired },
componentWillMount: function () {
this.listenTo(this.props.roomStore, "change:activeRoom", this._onActiveRoomStateChanged);
this.listenTo(this.props.roomStore, "change:error", this._onRoomError);
this.listenTo(this.props.roomStore, "change:savingContext", this._onRoomSavingContext);
},
componentWillUnmount: function () {
this.stopListening(this.props.roomStore);
},
componentWillMount: function componentWillMount() {
this.listenTo(this.props.roomStore, "change:activeRoom",
this._onActiveRoomStateChanged);
this.listenTo(this.props.roomStore, "change:error",
this._onRoomError);
this.listenTo(this.props.roomStore, "change:savingContext",
this._onRoomSavingContext);},
_onActiveRoomStateChanged: function () {
componentWillUnmount: function componentWillUnmount() {
this.stopListening(this.props.roomStore);},
_onActiveRoomStateChanged: function _onActiveRoomStateChanged() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState(this.props.roomStore.getStoreState("activeRoom"));
}
},
this.setState(this.props.roomStore.getStoreState("activeRoom"));}},
_onRoomError: function () {
_onRoomError: function _onRoomError() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({ error: this.props.roomStore.getStoreState("error") });
}
},
this.setState({ error: this.props.roomStore.getStoreState("error") });}},
_onRoomSavingContext: function () {
_onRoomSavingContext: function _onRoomSavingContext() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({ savingContext: this.props.roomStore.getStoreState("savingContext") });
}
},
this.setState({ savingContext: this.props.roomStore.getStoreState("savingContext") });}},
getInitialState: function () {
getInitialState: function getInitialState() {
var storeState = this.props.roomStore.getStoreState("activeRoom");
return _.extend({
return _.extend({
// Used by the UI showcase.
roomState: this.props.roomState || storeState.roomState,
savingContext: false
}, storeState);
}
};
roomState: this.props.roomState || storeState.roomState,
savingContext: false },
storeState);} };
/**
* Used to display errors in direct calls and rooms to the user.
*/
var FailureInfoView = React.createClass({
displayName: "FailureInfoView",
var FailureInfoView = React.createClass({ displayName: "FailureInfoView",
propTypes: {
failureReason: React.PropTypes.string.isRequired },
propTypes: {
failureReason: React.PropTypes.string.isRequired
},
/**
* Returns the translated message appropraite to the failure reason.
*
* @return {String} The translated message for the failure reason.
*/
_getMessage: function () {
_getMessage: function _getMessage() {
switch (this.props.failureReason) {
case FAILURE_DETAILS.NO_MEDIA:
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
return mozL10n.get("no_media_failure_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message", { clientShortname: mozL10n.get("clientShortname2") });
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
case FAILURE_DETAILS.ICE_FAILED:
return mozL10n.get("ice_failure_message");
default:
return mozL10n.get("generic_failure_message");
}
},
return mozL10n.get("generic_failure_message");}},
render: function render() {
return (
React.createElement("div", { className: "failure-info" },
React.createElement("div", { className: "failure-info-logo" }),
React.createElement("h2", { className: "failure-info-message" }, this._getMessage())));} });
render: function () {
return React.createElement(
"div",
{ className: "failure-info" },
React.createElement("div", { className: "failure-info-logo" }),
React.createElement(
"h2",
{ className: "failure-info-message" },
this._getMessage()
)
);
}
});
/**
* Something went wrong view. Displayed when there's a big problem.
*/
var RoomFailureView = React.createClass({
displayName: "RoomFailureView",
var RoomFailureView = React.createClass({ displayName: "RoomFailureView",
mixins: [sharedMixins.AudioMixin],
mixins: [sharedMixins.AudioMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
failureReason: React.PropTypes.string },
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
failureReason: React.PropTypes.string
},
componentDidMount: function () {
this.play("failure");
},
componentDidMount: function componentDidMount() {
this.play("failure");},
handleRejoinCall: function () {
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
},
render: function () {
handleRejoinCall: function handleRejoinCall() {
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());},
render: function render() {
var btnTitle;
if (this.props.failureReason === FAILURE_DETAILS.ICE_FAILED) {
btnTitle = mozL10n.get("retry_call_button");
} else {
btnTitle = mozL10n.get("rejoin_button");
}
btnTitle = mozL10n.get("retry_call_button");} else
{
btnTitle = mozL10n.get("rejoin_button");}
return (
React.createElement("div", { className: "room-failure" },
React.createElement(FailureInfoView, { failureReason: this.props.failureReason }),
React.createElement("div", { className: "btn-group call-action-group" },
React.createElement("button", { className: "btn btn-info btn-rejoin",
onClick: this.handleRejoinCall },
btnTitle))));} });
return React.createElement(
"div",
{ className: "room-failure" },
React.createElement(FailureInfoView, { failureReason: this.props.failureReason }),
React.createElement(
"div",
{ className: "btn-group call-action-group" },
React.createElement(
"button",
{ className: "btn btn-info btn-rejoin",
onClick: this.handleRejoinCall },
btnTitle
)
)
);
}
});
/**
* Desktop room conversation view.
*/
var DesktopRoomConversationView = React.createClass({
displayName: "DesktopRoomConversationView",
var DesktopRoomConversationView = React.createClass({ displayName: "DesktopRoomConversationView",
mixins: [
ActiveRoomStoreMixin,
sharedMixins.DocumentTitleMixin,
sharedMixins.MediaSetupMixin,
sharedMixins.RoomsAudioMixin,
sharedMixins.WindowCloseMixin],
mixins: [ActiveRoomStoreMixin, sharedMixins.DocumentTitleMixin, sharedMixins.MediaSetupMixin, sharedMixins.RoomsAudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
chatWindowDetached: React.PropTypes.bool.isRequired,
cursorStore: React.PropTypes.instanceOf(loop.store.RemoteCursorStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
facebookEnabled: React.PropTypes.bool.isRequired,
propTypes: {
chatWindowDetached: React.PropTypes.bool.isRequired,
cursorStore: React.PropTypes.instanceOf(loop.store.RemoteCursorStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
facebookEnabled: React.PropTypes.bool.isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
onCallTerminated: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
localPosterUrl: React.PropTypes.string,
onCallTerminated: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired },
componentWillUpdate: function (nextProps, nextState) {
componentWillUpdate: function componentWillUpdate(nextProps, nextState) {
// The SDK needs to know about the configuration and the elements to use
// for display. So the best way seems to pass the information here - ideally
// the sdk wouldn't need to know this, but we can't change that.
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT && nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: !this.state.videoMuted
})
}));
}
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: !this.state.videoMuted }) }));}
// Now that we're ready to share, automatically start sharing a tab only
// if we're not already connected to the room via the sdk, e.g. not in the
// case a remote participant just left.
if (nextState.roomState === ROOM_STATES.SESSION_CONNECTED && !(this.state.roomState === ROOM_STATES.SESSION_CONNECTED || this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS)) {
this.props.dispatcher.dispatch(new sharedActions.StartBrowserShare());
}
},
if (nextState.roomState === ROOM_STATES.SESSION_CONNECTED &&
!(this.state.roomState === ROOM_STATES.SESSION_CONNECTED ||
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS)) {
this.props.dispatcher.dispatch(new sharedActions.StartBrowserShare());}},
/**
* User clicked on the "Leave" button.
*/
leaveRoom: function () {
leaveRoom: function leaveRoom() {
if (this.state.used) {
// The room has been used, so we might want to display the feedback view.
// Therefore we leave the room to ensure that we have stopped sharing etc,
// then trigger the call terminated handler that'll do the right thing
// for the feedback view.
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
this.props.onCallTerminated();
} else {
this.props.onCallTerminated();} else
{
// If the room hasn't been used, we simply close the window.
this.closeWindow();
}
},
this.closeWindow();}},
/**
* Determine if the invitation controls should be shown.
*
* @return {Boolean} True if there's no guests.
*/
_shouldRenderInvitationOverlay: function () {
var hasGuests = typeof this.state.participants === "object" && this.state.participants.filter(function (participant) {
return !participant.owner;
}).length > 0;
_shouldRenderInvitationOverlay: function _shouldRenderInvitationOverlay() {
var hasGuests = _typeof(this.state.participants) === "object" &&
this.state.participants.filter(function (participant) {
return !participant.owner;}).
length > 0;
// Don't show if the room has participants whether from the room state or
// there being non-owner guests in the participants array.
return this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS && !hasGuests;
},
return this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS && !hasGuests;},
/**
* Works out if remote video should be rended or not, depending on the
@@ -242,18 +242,18 @@ loop.roomViews = function (mozL10n) {
* XXX Refactor shouldRenderRemoteVideo & shouldRenderLoading into one fn
* that returns an enum
*/
shouldRenderRemoteVideo: function () {
shouldRenderRemoteVideo: function shouldRenderRemoteVideo() {
switch (this.state.roomState) {
case ROOM_STATES.HAS_PARTICIPANTS:
if (this.state.remoteVideoEnabled) {
return true;
}
return true;}
if (this.state.mediaConnected) {
// since the remoteVideo hasn't yet been enabled, if the
// media is connected, then we should be displaying an avatar.
return false;
}
return false;}
return true;
@@ -272,10 +272,11 @@ loop.roomViews = function (mozL10n) {
return true;
default:
console.warn("DesktopRoomConversationView.shouldRenderRemoteVideo:" + " unexpected roomState: ", this.state.roomState);
return true;
}
},
console.warn("DesktopRoomConversationView.shouldRenderRemoteVideo:" +
" unexpected roomState: ", this.state.roomState);
return true;}},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a local
@@ -284,9 +285,10 @@ loop.roomViews = function (mozL10n) {
* @returns {boolean}
* @private
*/
_isLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT && !this.state.localSrcMediaElement;
},
_isLocalLoading: function _isLocalLoading() {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
!this.state.localSrcMediaElement;},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a remote
@@ -295,94 +297,96 @@ loop.roomViews = function (mozL10n) {
* @returns {boolean}
* @private
*/
_isRemoteLoading: function () {
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS && !this.state.remoteSrcMediaElement && !this.state.mediaConnected);
},
_isRemoteLoading: function _isRemoteLoading() {
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
!this.state.remoteSrcMediaElement &&
!this.state.mediaConnected);},
handleContextMenu: function (e) {
e.preventDefault();
},
render: function () {
handleContextMenu: function handleContextMenu(e) {
e.preventDefault();},
render: function render() {
if (this.state.roomName || this.state.roomContextUrls) {
var roomTitle = this.state.roomName || this.state.roomContextUrls[0].description || this.state.roomContextUrls[0].location;
var roomTitle = this.state.roomName ||
this.state.roomContextUrls[0].description ||
this.state.roomContextUrls[0].location;
if (!roomTitle) {
roomTitle = mozL10n.get("room_name_untitled_page");
}
this.setTitle(roomTitle);
}
roomTitle = mozL10n.get("room_name_untitled_page");}
this.setTitle(roomTitle);}
var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
var roomData = this.props.roomStore.getStoreState("activeRoom");
switch (this.state.roomState) {
case ROOM_STATES.FAILED:
case ROOM_STATES.FULL:
{
case ROOM_STATES.FULL:{
// Note: While rooms are set to hold a maximum of 2 participants, the
// FULL case should never happen on desktop.
return React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: this.state.failureReason });
}
case ROOM_STATES.ENDED:
{
return (
React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: this.state.failureReason }));}
case ROOM_STATES.ENDED:{
// When conversation ended we either display a feedback form or
// close the window. This is decided in the AppControllerView.
return null;
}
default:
{
return React.createElement(
"div",
{ className: "room-conversation-wrapper desktop-room-wrapper",
onContextMenu: this.handleContextMenu },
React.createElement(
sharedViews.MediaLayoutView,
{
cursorStore: this.props.cursorStore,
dispatcher: this.props.dispatcher,
displayScreenShare: false,
isLocalLoading: this._isLocalLoading(),
isRemoteLoading: this._isRemoteLoading(),
isScreenShareLoading: false,
localPosterUrl: this.props.localPosterUrl,
localSrcMediaElement: this.state.localSrcMediaElement,
localVideoMuted: this.state.videoMuted,
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
remotePosterUrl: this.props.remotePosterUrl,
remoteSrcMediaElement: this.state.remoteSrcMediaElement,
renderRemoteVideo: this.shouldRenderRemoteVideo(),
screenShareMediaElement: this.state.screenShareMediaElement,
screenSharePosterUrl: null,
showInitialContext: false,
showMediaWait: false,
showTile: false },
React.createElement(sharedViews.ConversationToolbar, {
audio: { enabled: !this.state.audioMuted, visible: true },
dispatcher: this.props.dispatcher,
hangup: this.leaveRoom,
showHangup: this.props.chatWindowDetached,
video: { enabled: !this.state.videoMuted, visible: true } }),
React.createElement(sharedDesktopViews.SharePanelView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
facebookEnabled: this.props.facebookEnabled,
locationForMetrics: "conversation",
roomData: roomData,
show: shouldRenderInvitationOverlay,
socialShareProviders: this.state.socialShareProviders })
)
);
}
}
}
});
return null;}
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
FailureInfoView: FailureInfoView,
RoomFailureView: RoomFailureView,
DesktopRoomConversationView: DesktopRoomConversationView
};
}(document.mozL10n || navigator.mozL10n);
default:{
return (
React.createElement("div", { className: "room-conversation-wrapper desktop-room-wrapper",
onContextMenu: this.handleContextMenu },
React.createElement(sharedViews.MediaLayoutView, {
cursorStore: this.props.cursorStore,
dispatcher: this.props.dispatcher,
displayScreenShare: false,
isLocalLoading: this._isLocalLoading(),
isRemoteLoading: this._isRemoteLoading(),
isScreenShareLoading: false,
localPosterUrl: this.props.localPosterUrl,
localSrcMediaElement: this.state.localSrcMediaElement,
localVideoMuted: this.state.videoMuted,
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
remotePosterUrl: this.props.remotePosterUrl,
remoteSrcMediaElement: this.state.remoteSrcMediaElement,
renderRemoteVideo: this.shouldRenderRemoteVideo(),
screenShareMediaElement: this.state.screenShareMediaElement,
screenSharePosterUrl: null,
showInitialContext: false,
showMediaWait: false,
showTile: false },
React.createElement(sharedViews.ConversationToolbar, {
audio: { enabled: !this.state.audioMuted, visible: true },
dispatcher: this.props.dispatcher,
hangup: this.leaveRoom,
showHangup: this.props.chatWindowDetached,
video: { enabled: !this.state.videoMuted, visible: true } }),
React.createElement(sharedDesktopViews.SharePanelView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
facebookEnabled: this.props.facebookEnabled,
locationForMetrics: "conversation",
roomData: roomData,
show: shouldRenderInvitationOverlay,
socialShareProviders: this.state.socialShareProviders }))));}}} });
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
FailureInfoView: FailureInfoView,
RoomFailureView: RoomFailureView,
DesktopRoomConversationView: DesktopRoomConversationView };}(
document.mozL10n || navigator.mozL10n);

View File

@@ -1,6 +1,6 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.slideshow = function (mozL10n) {
@@ -9,9 +9,12 @@ loop.slideshow = function (mozL10n) {
/**
* Slideshow initialisation.
*/
function init() {
var requests = [["GetAllStrings"], ["GetLocale"], ["GetPluralRule"]];
var requests = [
["GetAllStrings"],
["GetLocale"],
["GetPluralRule"]];
return loop.requestMulti.apply(null, requests).then(function (results) {
// `requestIdx` is keyed off the order of the `requests`
// array. Be careful to update both when making changes.
@@ -21,63 +24,68 @@ loop.slideshow = function (mozL10n) {
var stringBundle = results[requestIdx];
var locale = results[++requestIdx];
var pluralRule = results[++requestIdx];
mozL10n.initialize({
locale: locale,
pluralRule: pluralRule,
getStrings: function (key) {
mozL10n.initialize({
locale: locale,
pluralRule: pluralRule,
getStrings: function getStrings(key) {
if (!(key in stringBundle)) {
return "{ textContent: '' }";
}
return "{ textContent: '' }";}
return JSON.stringify({
textContent: stringBundle[key] });} });
return JSON.stringify({
textContent: stringBundle[key]
});
}
});
document.documentElement.setAttribute("lang", mozL10n.language.code);
document.documentElement.setAttribute("dir", mozL10n.language.direction);
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
var clientSuperShortname = mozL10n.get("clientSuperShortname");
var data = [{
id: "slide1",
imageClass: "slide1-image",
title: mozL10n.get("fte_slide_1_title"),
text: mozL10n.get("fte_slide_1_copy", {
clientShortname2: mozL10n.get("clientShortname2")
})
}, {
id: "slide2",
imageClass: "slide2-image",
title: mozL10n.get("fte_slide_2_title2"),
text: mozL10n.get("fte_slide_2_copy2", {
clientShortname2: mozL10n.get("clientShortname2")
})
}, {
id: "slide3",
imageClass: "slide3-image",
title: mozL10n.get("fte_slide_3_title"),
text: mozL10n.get("fte_slide_3_copy", {
clientSuperShortname: clientSuperShortname
})
}, {
id: "slide4",
imageClass: "slide4-image",
title: mozL10n.get("fte_slide_4_title", {
clientSuperShortname: clientSuperShortname
}),
text: mozL10n.get("fte_slide_4_copy", {
brandShortname: mozL10n.get("brandShortname")
})
}];
var data = [
{
id: "slide1",
imageClass: "slide1-image",
title: mozL10n.get("fte_slide_1_title"),
text: mozL10n.get("fte_slide_1_copy", {
clientShortname2: mozL10n.get("clientShortname2") }) },
loop.SimpleSlideshow.init("#main", data);
});
}
return {
init: init
};
}(document.mozL10n);
{
id: "slide2",
imageClass: "slide2-image",
title: mozL10n.get("fte_slide_2_title2"),
text: mozL10n.get("fte_slide_2_copy2", {
clientShortname2: mozL10n.get("clientShortname2") }) },
{
id: "slide3",
imageClass: "slide3-image",
title: mozL10n.get("fte_slide_3_title"),
text: mozL10n.get("fte_slide_3_copy", {
clientSuperShortname: clientSuperShortname }) },
{
id: "slide4",
imageClass: "slide4-image",
title: mozL10n.get("fte_slide_4_title", {
clientSuperShortname: clientSuperShortname }),
text: mozL10n.get("fte_slide_4_copy", {
brandShortname: mozL10n.get("brandShortname") }) }];
loop.SimpleSlideshow.init("#main", data);});}
return {
init: init };}(
document.mozL10n);
document.addEventListener("DOMContentLoaded", loop.slideshow.init);

View File

@@ -17,13 +17,15 @@
<script type="text/javascript" src="panels/vendor/l10n.js"></script>
<script type="text/javascript" src="shared/vendor/react.js"></script>
<script type="text/javascript" src="shared/vendor/react-dom.js"></script>
<script type="text/javascript" src="shared/vendor/lodash.js"></script>
<script type="text/javascript" src="shared/vendor/backbone.js"></script>
<script type="text/javascript" src="shared/vendor/classnames.js"></script>
<script type="text/javascript" src="shared/js/loopapi-client.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="panels/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/actions.js"></script>
<script type="text/javascript" src="shared/js/dispatcher.js"></script>

View File

@@ -17,6 +17,7 @@
<script type="text/javascript" src="panels/vendor/l10n.js"></script>
<script type="text/javascript" src="shared/vendor/react.js"></script>
<script type="text/javascript" src="shared/vendor/react-dom.js"></script>
<script type="text/javascript" src="shared/vendor/lodash.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/loopapi-client.js"></script>

View File

@@ -1,7 +1,3 @@
{
"rules": {
// This is useful for some of the tests, e.g.
// expect(new Foo()).to.Throw(/error/)
"no-new": 0
}
"extends": "../../../test/.eslintrc"
}

View File

@@ -1,211 +1,205 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.store.ConversationAppStore", function() {
describe("loop.store.ConversationAppStore", function () {
"use strict";
var expect = chai.expect;
var sharedActions = loop.shared.actions;
var sandbox, activeRoomStore, dispatcher, feedbackPeriodMs, roomUsed;
beforeEach(function() {
beforeEach(function () {
roomUsed = false;
feedbackPeriodMs = 15770000000;
activeRoomStore = {
getStoreState: function() { return { used: roomUsed }; }
};
activeRoomStore = {
getStoreState: function getStoreState() {return { used: roomUsed };} };
sandbox = LoopMochaUtils.createSandbox();
LoopMochaUtils.stubLoopRequest({
GetLoopPref: function() {}
});
LoopMochaUtils.stubLoopRequest({
GetLoopPref: function GetLoopPref() {} });
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
});
sandbox.stub(dispatcher, "dispatch");});
afterEach(function() {
afterEach(function () {
sandbox.restore();
LoopMochaUtils.restore();
});
LoopMochaUtils.restore();});
describe("#constructor", function() {
it("should throw an error if the activeRoomStore is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({
dispatcher: dispatcher,
feedbackPeriod: 1,
feedbackTimestamp: 1
});
}).to.Throw(/activeRoomStore/);
});
it("should throw an error if the dispatcher is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore,
feedbackPeriod: 1,
feedbackTimestamp: 1
});
}).to.Throw(/dispatcher/);
});
describe("#constructor", function () {
it("should throw an error if the activeRoomStore is missing", function () {
expect(function () {
new loop.store.ConversationAppStore(dispatcher, {
feedbackPeriod: 1,
feedbackTimestamp: 1 });}).
it("should throw an error if feedbackPeriod is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore,
dispatcher: dispatcher,
feedbackTimestamp: 1
});
}).to.Throw(/feedbackPeriod/);
});
to.Throw(/activeRoomStore/);});
it("should throw an error if feedbackTimestamp is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore,
dispatcher: dispatcher,
feedbackPeriod: 1
});
}).to.Throw(/feedbackTimestamp/);
});
it("should start listening to events on the window object", function() {
var fakeWindow = {
addEventListener: sinon.stub()
};
it("should throw an error if the dispatcher is missing", function () {
expect(function () {
new loop.store.ConversationAppStore(undefined, {
activeRoomStore: activeRoomStore,
feedbackPeriod: 1,
feedbackTimestamp: 1 });}).
to.Throw(/dispatcher/);});
it("should throw an error if feedbackPeriod is missing", function () {
expect(function () {
new loop.store.ConversationAppStore(dispatcher, {
activeRoomStore: activeRoomStore,
feedbackTimestamp: 1 });}).
to.Throw(/feedbackPeriod/);});
it("should throw an error if feedbackTimestamp is missing", function () {
expect(function () {
new loop.store.ConversationAppStore(dispatcher, {
activeRoomStore: activeRoomStore,
feedbackPeriod: 1 });}).
to.Throw(/feedbackTimestamp/);});
it("should start listening to events on the window object", function () {
var fakeWindow = {
addEventListener: sinon.stub() };
var store = new loop.store.ConversationAppStore(dispatcher, {
activeRoomStore: activeRoomStore,
feedbackPeriod: 1,
feedbackTimestamp: 1,
rootObject: fakeWindow });
var store = new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore,
dispatcher: dispatcher,
feedbackPeriod: 1,
feedbackTimestamp: 1,
rootObject: fakeWindow
});
var eventNames = Object.getOwnPropertyNames(store._eventHandlers);
sinon.assert.callCount(fakeWindow.addEventListener, eventNames.length);
eventNames.forEach(function(eventName) {
sinon.assert.calledWith(fakeWindow.addEventListener, eventName,
store._eventHandlers[eventName]);
});
});
});
eventNames.forEach(function (eventName) {
sinon.assert.calledWith(fakeWindow.addEventListener, eventName,
store._eventHandlers[eventName]);});});});
describe("#getWindowData", function() {
describe("#getWindowData", function () {
var fakeWindowData, fakeGetWindowData, store, getLoopPrefStub;
var setLoopPrefStub;
beforeEach(function() {
fakeWindowData = {
type: "room",
roomToken: "123456"
};
beforeEach(function () {
fakeWindowData = {
type: "room",
roomToken: "123456" };
fakeGetWindowData = {
windowId: "42" };
fakeGetWindowData = {
windowId: "42"
};
getLoopPrefStub = sandbox.stub();
setLoopPrefStub = sandbox.stub();
loop.storedRequests = {
"GetConversationWindowData|42": fakeWindowData
};
LoopMochaUtils.stubLoopRequest({
GetLoopPref: getLoopPrefStub,
SetLoopPref: setLoopPrefStub
});
loop.storedRequests = {
"GetConversationWindowData|42": fakeWindowData };
store = new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore,
dispatcher: dispatcher,
feedbackPeriod: 42,
feedbackTimestamp: 42
});
});
LoopMochaUtils.stubLoopRequest({
GetLoopPref: getLoopPrefStub,
SetLoopPref: setLoopPrefStub });
afterEach(function() {
sandbox.restore();
});
it("should fetch the window type from the Loop API", function() {
store = new loop.store.ConversationAppStore(dispatcher, {
activeRoomStore: activeRoomStore,
feedbackPeriod: 42,
feedbackTimestamp: 42 });});
afterEach(function () {
sandbox.restore();});
it("should fetch the window type from the Loop API", function () {
store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
expect(store.getStoreState().windowType).eql("room");
});
expect(store.getStoreState().windowType).eql("room");});
it("should have the feedback period in initial state", function() {
it("should have the feedback period in initial state", function () {
// Expect ms.
expect(store.getInitialStoreState().feedbackPeriod).to.eql(42 * 1000);
});
expect(store.getInitialStoreState().feedbackPeriod).to.eql(42 * 1000);});
it("should have the dateLastSeen in initial state", function() {
it("should have the dateLastSeen in initial state", function () {
// Expect ms.
expect(store.getInitialStoreState().feedbackTimestamp).to.eql(42 * 1000);
});
expect(store.getInitialStoreState().feedbackTimestamp).to.eql(42 * 1000);});
it("should set showFeedbackForm to true when action is triggered", function() {
it("should set showFeedbackForm to true when action is triggered", function () {
var showFeedbackFormStub = sandbox.stub(store, "showFeedbackForm");
store.showFeedbackForm(new sharedActions.ShowFeedbackForm());
sinon.assert.calledOnce(showFeedbackFormStub);
});
sinon.assert.calledOnce(showFeedbackFormStub);});
it("should set feedback timestamp on ShowFeedbackForm action", function() {
it("should set feedback timestamp on ShowFeedbackForm action", function () {
var clock = sandbox.useFakeTimers();
// Make sure we round down the value.
clock.tick(1001);
store.showFeedbackForm(new sharedActions.ShowFeedbackForm());
sinon.assert.calledOnce(setLoopPrefStub);
sinon.assert.calledWithExactly(setLoopPrefStub,
"feedback.dateLastSeenSec", 1);
});
sinon.assert.calledWithExactly(setLoopPrefStub,
"feedback.dateLastSeenSec", 1);});
it("should dispatch a SetupWindowData action with the data from the Loop API",
function() {
store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupWindowData(_.extend({
windowId: fakeGetWindowData.windowId
}, fakeWindowData)));
});
});
it("should dispatch a SetupWindowData action with the data from the Loop API",
function () {
store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
describe("Window object event handlers", function() {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupWindowData(_.extend({
windowId: fakeGetWindowData.windowId },
fakeWindowData)));});});
describe("Window object event handlers", function () {
var store, fakeWindow;
beforeEach(function() {
fakeWindow = {
addEventListener: sinon.stub(),
removeEventListener: sinon.stub()
};
beforeEach(function () {
fakeWindow = {
addEventListener: sinon.stub(),
removeEventListener: sinon.stub() };
LoopMochaUtils.stubLoopRequest({
GetLoopPref: function() {}
});
store = new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore,
dispatcher: dispatcher,
feedbackPeriod: 1,
feedbackTimestamp: 1,
rootObject: fakeWindow
});
});
LoopMochaUtils.stubLoopRequest({
GetLoopPref: function GetLoopPref() {} });
describe("#unloadHandler", function() {
it("should dispatch a 'WindowUnload' action when invoked", function() {
store = new loop.store.ConversationAppStore(dispatcher, {
activeRoomStore: activeRoomStore,
feedbackPeriod: 1,
feedbackTimestamp: 1,
rootObject: fakeWindow });});
describe("#unloadHandler", function () {
it("should dispatch a 'WindowUnload' action when invoked", function () {
store.unloadHandler();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.WindowUnload());
});
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.WindowUnload());});
it("should remove all registered event handlers from the window object", function() {
it("should remove all registered event handlers from the window object", function () {
var eventHandlers = store._eventHandlers;
var eventNames = Object.getOwnPropertyNames(eventHandlers);
@@ -213,160 +207,155 @@ describe("loop.store.ConversationAppStore", function() {
sinon.assert.callCount(fakeWindow.removeEventListener, eventNames.length);
expect(store._eventHandlers).to.eql(null);
eventNames.forEach(function(eventName) {
sinon.assert.calledWith(fakeWindow.removeEventListener, eventName,
eventHandlers[eventName]);
});
});
});
eventNames.forEach(function (eventName) {
sinon.assert.calledWith(fakeWindow.removeEventListener, eventName,
eventHandlers[eventName]);});});});
describe("#LoopHangupNowHandler", function() {
it("should dispatch a LeaveConversation action", function() {
describe("#LoopHangupNowHandler", function () {
it("should dispatch a LeaveConversation action", function () {
store.LoopHangupNowHandler();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveConversation());
});
});
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveConversation());});});
describe("#leaveConversation", function() {
beforeEach(function() {
describe("#leaveConversation", function () {
beforeEach(function () {
sandbox.stub(loop.shared.mixins.WindowCloseMixin, "closeWindow");
roomUsed = true;
});
roomUsed = true;});
it("should close the window for window types other than `room`", function() {
it("should close the window for window types other than `room`", function () {
store.setStoreState({ windowType: "foobar" });
store.leaveConversation();
sinon.assert.notCalled(dispatcher.dispatch);
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);});
it("should close the window when a room was not used", function() {
it("should close the window when a room was not used", function () {
store.setStoreState({ windowType: "room" });
roomUsed = false;
store.leaveConversation();
sinon.assert.notCalled(dispatcher.dispatch);
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);});
it("should close the window when a room was used and it showed feedback", function () {
store.setStoreState({
showFeedbackForm: true,
windowType: "room" });
it("should close the window when a room was used and it showed feedback", function() {
store.setStoreState({
showFeedbackForm: true,
windowType: "room"
});
store.leaveConversation();
sinon.assert.notCalled(dispatcher.dispatch);
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);});
it("should dispatch a LeaveRoom action if timestamp is 0", function () {
store.setStoreState({
feedbackTimestamp: 0,
windowType: "room" });
it("should dispatch a LeaveRoom action if timestamp is 0", function() {
store.setStoreState({
feedbackTimestamp: 0,
windowType: "room"
});
store.leaveConversation();
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveRoom({
windowStayingOpen: true
}));
});
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveRoom({
windowStayingOpen: true }));});
it("should dispatch a ShowFeedbackForm action if timestamp is 0", function () {
store.setStoreState({
feedbackTimestamp: 0,
windowType: "room" });
it("should dispatch a ShowFeedbackForm action if timestamp is 0", function() {
store.setStoreState({
feedbackTimestamp: 0,
windowType: "room"
});
store.leaveConversation();
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());
});
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());});
it("should dispatch a LeaveRoom action if delta > feedback period", function() {
it("should dispatch a LeaveRoom action if delta > feedback period", function () {
var feedbackTimestamp = new Date() - feedbackPeriodMs;
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs,
windowType: "room"
});
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs,
windowType: "room" });
store.leaveConversation();
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveRoom({
windowStayingOpen: true
}));
});
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveRoom({
windowStayingOpen: true }));});
it("should dispatch a ShowFeedbackForm action if delta > feedback period", function() {
it("should dispatch a ShowFeedbackForm action if delta > feedback period", function () {
var feedbackTimestamp = new Date() - feedbackPeriodMs;
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs,
windowType: "room"
});
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs,
windowType: "room" });
store.leaveConversation();
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());
});
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShowFeedbackForm());});
it("should close the window if delta < feedback period", function() {
it("should close the window if delta < feedback period", function () {
var feedbackTimestamp = new Date().getTime();
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs
});
store.setStoreState({
feedbackTimestamp: feedbackTimestamp,
feedbackPeriod: feedbackPeriodMs });
store.leaveConversation();
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
});
sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);});});
describe("#socialFrameAttachedHandler", function() {
it("should update the store correctly to reflect the attached state", function() {
describe("#socialFrameAttachedHandler", function () {
it("should update the store correctly to reflect the attached state", function () {
store.setStoreState({ chatWindowDetached: true });
store.socialFrameAttachedHandler();
expect(store.getStoreState().chatWindowDetached).to.eql(false);
});
});
expect(store.getStoreState().chatWindowDetached).to.eql(false);});});
describe("#socialFrameDetachedHandler", function() {
it("should update the store correctly to reflect the detached state", function() {
describe("#socialFrameDetachedHandler", function () {
it("should update the store correctly to reflect the detached state", function () {
store.socialFrameDetachedHandler();
expect(store.getStoreState().chatWindowDetached).to.eql(true);
});
});
expect(store.getStoreState().chatWindowDetached).to.eql(true);});});
describe("#ToggleBrowserSharingHandler", function() {
it("should dispatch the correct action", function() {
describe("#ToggleBrowserSharingHandler", function () {
it("should dispatch the correct action", function () {
store.ToggleBrowserSharingHandler({ detail: false });
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.ToggleBrowserSharing({
enabled: true
}));
});
});
});
});
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.ToggleBrowserSharing({
enabled: true }));});});});});

View File

@@ -1,341 +1,337 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.conversation", function() {
describe("loop.conversation", function () {
"use strict";
var FeedbackView = loop.feedbackViews.FeedbackView;
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var fakeWindow, sandbox, setLoopPrefStub, mozL10nGet,
remoteCursorStore, dispatcher, requestStubs;
var fakeWindow, sandbox, setLoopPrefStub, mozL10nGet,
remoteCursorStore, dispatcher, requestStubs;
beforeEach(function() {
beforeEach(function () {
sandbox = LoopMochaUtils.createSandbox();
setLoopPrefStub = sandbox.stub();
LoopMochaUtils.stubLoopRequest(requestStubs = {
GetDoNotDisturb: function() { return true; },
GetAllStrings: function() {
return JSON.stringify({ textContent: "fakeText" });
},
GetLocale: function() {
return "en-US";
},
SetLoopPref: setLoopPrefStub,
GetLoopPref: function(prefName) {
LoopMochaUtils.stubLoopRequest(requestStubs = {
GetDoNotDisturb: function GetDoNotDisturb() {return true;},
GetAllStrings: function GetAllStrings() {
return JSON.stringify({ textContent: "fakeText" });},
GetLocale: function GetLocale() {
return "en-US";},
SetLoopPref: setLoopPrefStub,
GetLoopPref: function GetLoopPref(prefName) {
switch (prefName) {
case "debug.sdk":
case "debug.dispatcher":
return false;
default:
return "http://fake";
}
},
GetAllConstants: function() {
return {
LOOP_SESSION_TYPE: {
GUEST: 1,
FXA: 2
},
LOOP_MAU_TYPE: {
OPEN_PANEL: 0,
OPEN_CONVERSATION: 1,
ROOM_OPEN: 2,
ROOM_SHARE: 3,
ROOM_DELETE: 4
}
};
},
EnsureRegistered: sinon.stub(),
GetAppVersionInfo: function() {
return {
version: "42",
channel: "test",
platform: "test"
};
},
GetAudioBlob: sinon.spy(function() {
return new Blob([new ArrayBuffer(10)], { type: "audio/ogg" });
}),
GetSelectedTabMetadata: function() {
return {};
},
GetConversationWindowData: function() {
return {};
},
TelemetryAddValue: sinon.stub()
});
return "http://fake";}},
GetAllConstants: function GetAllConstants() {
return {
LOOP_SESSION_TYPE: {
GUEST: 1,
FXA: 2 },
LOOP_MAU_TYPE: {
OPEN_PANEL: 0,
OPEN_CONVERSATION: 1,
ROOM_OPEN: 2,
ROOM_SHARE: 3,
ROOM_DELETE: 4 } };},
EnsureRegistered: sinon.stub(),
GetAppVersionInfo: function GetAppVersionInfo() {
return {
version: "42",
channel: "test",
platform: "test" };},
GetAudioBlob: sinon.spy(function () {
return new Blob([new ArrayBuffer(10)], { type: "audio/ogg" });}),
GetSelectedTabMetadata: function GetSelectedTabMetadata() {
return {};},
GetConversationWindowData: function GetConversationWindowData() {
return {};},
TelemetryAddValue: sinon.stub() });
fakeWindow = {
close: sinon.stub(),
document: {},
addEventListener: function addEventListener() {},
removeEventListener: function removeEventListener() {} };
fakeWindow = {
close: sinon.stub(),
document: {},
addEventListener: function() {},
removeEventListener: function() {}
};
loop.shared.mixins.setRootObject(fakeWindow);
// XXX These stubs should be hoisted in a common file
// Bug 1040968
mozL10nGet = sandbox.stub(document.mozL10n, "get", function(x) {
return x;
});
document.mozL10n.initialize({
getStrings: function() { return JSON.stringify({ textContent: "fakeText" }); },
locale: "en_US"
});
mozL10nGet = sandbox.stub(document.mozL10n, "get", function (x) {
return x;});
document.mozL10n.initialize({
getStrings: function getStrings() {return JSON.stringify({ textContent: "fakeText" });},
locale: "en_US" });
dispatcher = new loop.Dispatcher();
remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: {}
});
remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: {} });
loop.store.StoreMixin.register({ remoteCursorStore: remoteCursorStore });
});
afterEach(function() {
loop.store.StoreMixin.register({ remoteCursorStore: remoteCursorStore });});
afterEach(function () {
loop.shared.mixins.setRootObject(window);
sandbox.restore();
LoopMochaUtils.restore();
});
LoopMochaUtils.restore();});
describe("#init", function() {
describe("#init", function () {
var OTRestore;
beforeEach(function() {
sandbox.stub(React, "render");
beforeEach(function () {
sandbox.stub(ReactDOM, "render");
sandbox.stub(document.mozL10n, "initialize");
sandbox.stub(loop.Dispatcher.prototype, "dispatch");
sandbox.stub(loop.shared.utils,
"locationData").returns({
hash: "#42",
pathname: "/"
});
sandbox.stub(loop.shared.utils,
"locationData").returns({
hash: "#42",
pathname: "/" });
OTRestore = window.OT;
window.OT = {
overrideGuidStorage: sinon.stub()
};
});
window.OT = {
overrideGuidStorage: sinon.stub() };});
afterEach(function() {
window.OT = OTRestore;
});
it("should initialize L10n", function() {
afterEach(function () {
window.OT = OTRestore;});
it("should initialize L10n", function () {
loop.conversation.init();
sinon.assert.calledOnce(document.mozL10n.initialize);
sinon.assert.calledWith(document.mozL10n.initialize, sinon.match({ locale: "en-US" }));
});
sinon.assert.calledWith(document.mozL10n.initialize, sinon.match({ locale: "en-US" }));});
it("should create the AppControllerView", function() {
it("should create the AppControllerView", function () {
loop.conversation.init();
sinon.assert.calledOnce(React.render);
sinon.assert.calledWith(React.render,
sinon.match(function(value) {
return TestUtils.isCompositeComponentElement(value,
loop.conversation.AppControllerView);
}));
});
sinon.assert.calledOnce(ReactDOM.render);
sinon.assert.calledWith(ReactDOM.render,
sinon.match(function (value) {
return TestUtils.isCompositeComponentElement(value,
loop.conversation.AppControllerView);}));});
it("should trigger a getWindowData action", function() {
it("should trigger a getWindowData action", function () {
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.GetWindowData({
windowId: "42"
}));
});
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.GetWindowData({
windowId: "42" }));});
it("should log a telemetry event when opening the conversation window", function() {
it("should log a telemetry event when opening the conversation window", function () {
var constants = requestStubs.GetAllConstants();
loop.conversation.init();
sinon.assert.calledOnce(requestStubs["TelemetryAddValue"]);
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"],
"LOOP_ACTIVITY_COUNTER", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);
});
});
sinon.assert.calledWithExactly(requestStubs["TelemetryAddValue"],
"LOOP_ACTIVITY_COUNTER", constants.LOOP_MAU_TYPE.OPEN_CONVERSATION);});});
describe("AppControllerView", function() {
var activeRoomStore,
ccView,
addRemoteCursorStub,
clickRemoteCursorStub;
var conversationAppStore,
roomStore;
describe("AppControllerView", function () {
var activeRoomStore,
ccView,
addRemoteCursorStub,
clickRemoteCursorStub;
var conversationAppStore,
roomStore;
var ROOM_STATES = loop.store.ROOM_STATES;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversation.AppControllerView, {
cursorStore: remoteCursorStore,
dispatcher: dispatcher,
roomStore: roomStore
}));
}
React.createElement(loop.conversation.AppControllerView, {
cursorStore: remoteCursorStore,
dispatcher: dispatcher,
roomStore: roomStore }));}
beforeEach(function() {
activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: {},
sdkDriver: {}
});
roomStore = new loop.store.RoomStore(dispatcher, {
activeRoomStore: activeRoomStore,
constants: {}
});
remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: {}
});
conversationAppStore = new loop.store.ConversationAppStore({
activeRoomStore: activeRoomStore,
dispatcher: dispatcher,
feedbackPeriod: 42,
feedbackTimestamp: 42,
facebookEnabled: false
});
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore
});
beforeEach(function () {
activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: {},
sdkDriver: {} });
roomStore = new loop.store.RoomStore(dispatcher, {
activeRoomStore: activeRoomStore,
constants: {} });
remoteCursorStore = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: {} });
conversationAppStore = new loop.store.ConversationAppStore(dispatcher, {
activeRoomStore: activeRoomStore,
feedbackPeriod: 42,
feedbackTimestamp: 42,
facebookEnabled: false });
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore });
addRemoteCursorStub = sandbox.stub();
clickRemoteCursorStub = sandbox.stub();
LoopMochaUtils.stubLoopRequest({
AddRemoteCursorOverlay: addRemoteCursorStub,
ClickRemoteCursor: clickRemoteCursorStub
});
LoopMochaUtils.stubLoopRequest({
AddRemoteCursorOverlay: addRemoteCursorStub,
ClickRemoteCursor: clickRemoteCursorStub });
loop.config = {
tilesIframeUrl: null,
tilesSupportUrl: null
};
sinon.stub(dispatcher, "dispatch");
});
loop.config = {
tilesIframeUrl: null,
tilesSupportUrl: null };
afterEach(function() {
ccView = undefined;
});
it("should request AddRemoteCursorOverlay when cursor position changes", function() {
sinon.stub(dispatcher, "dispatch");});
afterEach(function () {
ccView = undefined;});
it("should request AddRemoteCursorOverlay when cursor position changes", function () {
mountTestComponent();
remoteCursorStore.setStoreState({
"remoteCursorPosition": {
"ratioX": 10,
"ratioY": 10
}
});
remoteCursorStore.setStoreState({
"remoteCursorPosition": {
"ratioX": 10,
"ratioY": 10 } });
sinon.assert.calledOnce(addRemoteCursorStub);
});
it("should NOT request AddRemoteCursorOverlay when cursor position DOES NOT changes", function() {
sinon.assert.calledOnce(addRemoteCursorStub);});
it("should NOT request AddRemoteCursorOverlay when cursor position DOES NOT changes", function () {
mountTestComponent();
remoteCursorStore.setStoreState({
"realVideoSize": {
"height": 400,
"width": 600
}
});
remoteCursorStore.setStoreState({
"realVideoSize": {
"height": 400,
"width": 600 } });
sinon.assert.notCalled(addRemoteCursorStub);
});
it("should request ClickRemoteCursor when click event detected", function() {
sinon.assert.notCalled(addRemoteCursorStub);});
it("should request ClickRemoteCursor when click event detected", function () {
mountTestComponent();
remoteCursorStore.setStoreState({
"remoteCursorClick": true
});
remoteCursorStore.setStoreState({
"remoteCursorClick": true });
sinon.assert.calledOnce(clickRemoteCursorStub);
});
it("should NOT request ClickRemoteCursor when reset click on store", function() {
sinon.assert.calledOnce(clickRemoteCursorStub);});
it("should NOT request ClickRemoteCursor when reset click on store", function () {
mountTestComponent();
remoteCursorStore.setStoreState({
"remoteCursorClick": false
});
remoteCursorStore.setStoreState({
"remoteCursorClick": false });
sinon.assert.notCalled(clickRemoteCursorStub);
});
it("should display the RoomView for rooms", function() {
sinon.assert.notCalled(clickRemoteCursorStub);});
it("should display the RoomView for rooms", function () {
conversationAppStore.setStoreState({ windowType: "room" });
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
ccView = mountTestComponent();
var desktopRoom = TestUtils.findRenderedComponentWithType(ccView,
loop.roomViews.DesktopRoomConversationView);
var desktopRoom = TestUtils.findRenderedComponentWithType(ccView,
loop.roomViews.DesktopRoomConversationView);
expect(desktopRoom.props.facebookEnabled).to.eql(false);
});
expect(desktopRoom.props.facebookEnabled).to.eql(false);});
it("should pass the correct value of facebookEnabled to DesktopRoomConversationView",
function() {
conversationAppStore.setStoreState({
windowType: "room",
facebookEnabled: true
});
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
ccView = mountTestComponent();
it("should pass the correct value of facebookEnabled to DesktopRoomConversationView",
function () {
conversationAppStore.setStoreState({
windowType: "room",
facebookEnabled: true });
var desktopRoom = TestUtils.findRenderedComponentWithType(ccView,
loop.roomViews.DesktopRoomConversationView);
expect(desktopRoom.props.facebookEnabled).to.eql(true);
});
it("should display the RoomFailureView for failures", function() {
conversationAppStore.setStoreState({
outgoing: false,
windowType: "failed"
});
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.roomViews.RoomFailureView);
});
var desktopRoom = TestUtils.findRenderedComponentWithType(ccView,
loop.roomViews.DesktopRoomConversationView);
it("should set the correct title when rendering feedback view", function() {
expect(desktopRoom.props.facebookEnabled).to.eql(true);});
it("should display the RoomFailureView for failures", function () {
conversationAppStore.setStoreState({
outgoing: false,
windowType: "failed" });
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.roomViews.RoomFailureView);});
it("should set the correct title when rendering feedback view", function () {
conversationAppStore.setStoreState({ showFeedbackForm: true });
ccView = mountTestComponent();
sinon.assert.calledWithExactly(mozL10nGet, "conversation_has_ended");
});
sinon.assert.calledWithExactly(mozL10nGet, "conversation_has_ended");});
it("should render FeedbackView if showFeedbackForm state is true",
function() {
conversationAppStore.setStoreState({ showFeedbackForm: true });
ccView = mountTestComponent();
it("should render FeedbackView if showFeedbackForm state is true",
function () {
conversationAppStore.setStoreState({ showFeedbackForm: true });
TestUtils.findRenderedComponentWithType(ccView, FeedbackView);
});
ccView = mountTestComponent();
it("should dispatch LeaveConversation when handleCallTerminated is called", function() {
TestUtils.findRenderedComponentWithType(ccView, FeedbackView);});
it("should dispatch LeaveConversation when handleCallTerminated is called", function () {
ccView = mountTestComponent();
ccView.handleCallTerminated();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveConversation());
});
});
});
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LeaveConversation());});});});

View File

@@ -1,8 +1,8 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.copy", function() {
describe("loop.copy", function () {
"use strict";
var expect = chai.expect;
@@ -11,115 +11,112 @@ describe("loop.copy", function() {
var TestUtils = React.addons.TestUtils;
var sandbox;
beforeEach(function() {
beforeEach(function () {
sandbox = LoopMochaUtils.createSandbox();
sandbox.stub(l10n, "get");
});
sandbox.stub(l10n, "get");});
afterEach(function() {
afterEach(function () {
loop.shared.mixins.setRootObject(window);
sandbox.restore();
LoopMochaUtils.restore();
});
LoopMochaUtils.restore();});
describe("#init", function() {
beforeEach(function() {
sandbox.stub(React, "render");
sandbox.stub(document.mozL10n, "initialize");
});
it("should initalize L10n", function() {
describe("#init", function () {
beforeEach(function () {
sandbox.stub(ReactDOM, "render");
sandbox.stub(document.mozL10n, "initialize");});
it("should initalize L10n", function () {
loop.copy.init();
sinon.assert.calledOnce(document.mozL10n.initialize);
});
sinon.assert.calledOnce(document.mozL10n.initialize);});
it("should render the copy view", function() {
it("should render the copy view", function () {
loop.copy.init();
sinon.assert.calledOnce(React.render);
sinon.assert.calledWith(React.render, sinon.match(function(value) {
return value.type === CopyView;
}));
});
});
sinon.assert.calledOnce(ReactDOM.render);
sinon.assert.calledWith(ReactDOM.render, sinon.match(function (value) {
return value.type === CopyView;}));});});
describe("#render", function() {
describe("#render", function () {
var view;
beforeEach(function() {
view = TestUtils.renderIntoDocument(React.createElement(CopyView));
});
beforeEach(function () {
view = TestUtils.renderIntoDocument(React.createElement(CopyView));});
it("should have an unchecked box", function() {
expect(view.getDOMNode().querySelector("input[type=checkbox]").checked).eql(false);
});
it("should have two buttons", function() {
expect(view.getDOMNode().querySelectorAll("button").length).eql(2);
});
});
it("should have an unchecked box", function () {
expect(ReactDOM.findDOMNode(view).querySelector("input[type=checkbox]").checked).eql(false);});
describe("handleChanges", function() {
it("should have two buttons", function () {
expect(ReactDOM.findDOMNode(view).querySelectorAll("button").length).eql(2);});});
describe("handleChanges", function () {
var view;
beforeEach(function() {
view = TestUtils.renderIntoDocument(React.createElement(CopyView));
});
beforeEach(function () {
view = TestUtils.renderIntoDocument(React.createElement(CopyView));});
it("should have default state !checked", function() {
expect(view.state.checked).to.be.false;
});
it("should have checked state after change", function() {
TestUtils.Simulate.change(view.getDOMNode().querySelector("input"), {
target: { checked: true }
});
it("should have default state !checked", function () {
expect(view.state.checked).to.be.false;});
expect(view.state.checked).to.be.true;
});
});
describe("handleClicks", function() {
it("should have checked state after change", function () {
TestUtils.Simulate.change(ReactDOM.findDOMNode(view).querySelector("input"), {
target: { checked: true } });
expect(view.state.checked).to.be.true;});});
describe("handleClicks", function () {
var view;
beforeEach(function() {
beforeEach(function () {
sandbox.stub(window, "dispatchEvent");
view = TestUtils.renderIntoDocument(React.createElement(CopyView));
});
view = TestUtils.renderIntoDocument(React.createElement(CopyView));});
function checkDispatched(detail) {
sinon.assert.calledOnce(window.dispatchEvent);
sinon.assert.calledWith(window.dispatchEvent, sinon.match.has("detail", detail));
}
sinon.assert.calledWith(window.dispatchEvent, sinon.match.has("detail", detail));}
it("should dispatch accept !stop on accept", function() {
TestUtils.Simulate.click(view.getDOMNode().querySelector("button:last-child"));
checkDispatched({ accept: true, stop: false });
});
it("should dispatch accept !stop on accept", function () {
TestUtils.Simulate.click(ReactDOM.findDOMNode(view).querySelector("button:last-child"));
it("should dispatch !accept !stop on cancel", function() {
TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
checkDispatched({ accept: true, stop: false });});
checkDispatched({ accept: false, stop: false });
});
it("should dispatch accept stop on checked accept", function() {
TestUtils.Simulate.change(view.getDOMNode().querySelector("input"), {
target: { checked: true }
});
TestUtils.Simulate.click(view.getDOMNode().querySelector("button:last-child"));
it("should dispatch !accept !stop on cancel", function () {
TestUtils.Simulate.click(ReactDOM.findDOMNode(view).querySelector("button"));
checkDispatched({ accept: true, stop: true });
});
checkDispatched({ accept: false, stop: false });});
it("should dispatch !accept stop on checked cancel", function() {
TestUtils.Simulate.change(view.getDOMNode().querySelector("input"), {
target: { checked: true }
});
TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
checkDispatched({ accept: false, stop: true });
});
});
});
it("should dispatch accept stop on checked accept", function () {
TestUtils.Simulate.change(ReactDOM.findDOMNode(view).querySelector("input"), {
target: { checked: true } });
TestUtils.Simulate.click(ReactDOM.findDOMNode(view).querySelector("button:last-child"));
checkDispatched({ accept: true, stop: true });});
it("should dispatch !accept stop on checked cancel", function () {
TestUtils.Simulate.change(ReactDOM.findDOMNode(view).querySelector("input"), {
target: { checked: true } });
TestUtils.Simulate.click(ReactDOM.findDOMNode(view).querySelector("button"));
checkDispatched({ accept: false, stop: true });});});});

View File

@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.shared.desktopViews", function() {
describe("loop.shared.desktopViews", function () {
"use strict";
var expect = chai.expect;
@@ -11,357 +11,352 @@ describe("loop.shared.desktopViews", function() {
var sharedDesktopViews = loop.shared.desktopViews;
var sandbox, clock, dispatcher;
beforeEach(function() {
beforeEach(function () {
sandbox = LoopMochaUtils.createSandbox();
clock = sandbox.useFakeTimers(); // exposes sandbox.clock as a fake timer
sandbox.stub(l10n, "get", function(x) {
return "translated:" + x;
});
sandbox.stub(l10n, "get", function (x) {
return "translated:" + x;});
LoopMochaUtils.stubLoopRequest({
GetLoopPref: function GetLoopPref() {
return true;} });
LoopMochaUtils.stubLoopRequest({
GetLoopPref: function() {
return true;
}
});
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
});
sandbox.stub(dispatcher, "dispatch");});
afterEach(function() {
afterEach(function () {
sandbox.restore();
LoopMochaUtils.restore();
});
LoopMochaUtils.restore();});
describe("CopyLinkButton", function() {
describe("CopyLinkButton", function () {
var view;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: [{
location: "fakeLocation",
url: "fakeUrl"
}]
}
}, props || {});
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: [{
location: "fakeLocation",
url: "fakeUrl" }] } },
props || {});
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.CopyLinkButton, props));
}
React.createElement(sharedDesktopViews.CopyLinkButton, props));}
beforeEach(function() {
view = mountTestComponent();
});
it("should dispatch a CopyRoomUrl action when the copy button is pressed", function() {
var copyBtn = view.getDOMNode();
beforeEach(function () {
view = mountTestComponent();});
it("should dispatch a CopyRoomUrl action when the copy button is pressed", function () {
var copyBtn = ReactDOM.findDOMNode(view);
React.addons.TestUtils.Simulate.click(copyBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
roomUrl: "http://invalid",
from: "conversation"
}));
});
sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
roomUrl: "http://invalid",
from: "conversation" }));});
it("should change the text when the url has been copied", function() {
var copyBtn = view.getDOMNode();
it("should change the text when the url has been copied", function () {
var copyBtn = ReactDOM.findDOMNode(view);
React.addons.TestUtils.Simulate.click(copyBtn);
expect(copyBtn.textContent).eql("translated:invite_copied_link_button");
});
expect(copyBtn.textContent).eql("translated:invite_copied_link_button");});
it("should keep the text for a while after the url has been copied", function() {
var copyBtn = view.getDOMNode();
it("should keep the text for a while after the url has been copied", function () {
var copyBtn = ReactDOM.findDOMNode(view);
React.addons.TestUtils.Simulate.click(copyBtn);
clock.tick(sharedDesktopViews.CopyLinkButton.TRIGGERED_RESET_DELAY / 2);
expect(copyBtn.textContent).eql("translated:invite_copied_link_button");
});
expect(copyBtn.textContent).eql("translated:invite_copied_link_button");});
it("should reset the text a bit after the url has been copied", function() {
var copyBtn = view.getDOMNode();
it("should reset the text a bit after the url has been copied", function () {
var copyBtn = ReactDOM.findDOMNode(view);
React.addons.TestUtils.Simulate.click(copyBtn);
clock.tick(sharedDesktopViews.CopyLinkButton.TRIGGERED_RESET_DELAY);
expect(copyBtn.textContent).eql("translated:invite_copy_link_button");
});
expect(copyBtn.textContent).eql("translated:invite_copy_link_button");});
it("should invoke callback if defined", function() {
it("should invoke callback if defined", function () {
var callback = sinon.stub();
view = mountTestComponent({
callback: callback
});
var copyBtn = view.getDOMNode();
view = mountTestComponent({
callback: callback });
var copyBtn = ReactDOM.findDOMNode(view);
React.addons.TestUtils.Simulate.click(copyBtn);
clock.tick(sharedDesktopViews.CopyLinkButton.TRIGGERED_RESET_DELAY);
sinon.assert.calledOnce(callback);
});
});
sinon.assert.calledOnce(callback);});});
describe("EmailLinkButton", function() {
describe("EmailLinkButton", function () {
var view;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: []
}
}, props || {});
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: [] } },
props || {});
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.EmailLinkButton, props));
}
React.createElement(sharedDesktopViews.EmailLinkButton, props));}
beforeEach(function() {
view = mountTestComponent();
});
it("should dispatch an EmailRoomUrl with no description" +
" for rooms without context when the email button is pressed",
function() {
var emailBtn = view.getDOMNode();
beforeEach(function () {
view = mountTestComponent();});
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.EmailRoomUrl({
roomUrl: "http://invalid",
roomDescription: undefined,
from: "conversation"
}));
});
it("should dispatch an EmailRoomUrl with no description" +
" for rooms without context when the email button is pressed",
function () {
var emailBtn = ReactDOM.findDOMNode(view);
it("should dispatch an EmailRoomUrl with a domain name description for rooms with context",
function() {
view = mountTestComponent({
roomData: {
roomUrl: "http://invalid",
roomContextUrls: [{ location: "http://www.mozilla.com/" }]
}
});
var emailBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.EmailRoomUrl({
roomUrl: "http://invalid",
roomDescription: "www.mozilla.com",
from: "conversation"
}));
});
it("should invoke callback if defined", function() {
var callback = sinon.stub();
view = mountTestComponent({
callback: callback
});
var emailBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(callback);
});
});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.EmailRoomUrl({
roomUrl: "http://invalid",
roomDescription: undefined,
from: "conversation" }));});
describe("FacebookShareButton", function() {
it("should dispatch an EmailRoomUrl with a domain name description for rooms with context",
function () {
view = mountTestComponent({
roomData: {
roomUrl: "http://invalid",
roomContextUrls: [{ location: "http://www.mozilla.com/" }] } });
var emailBtn = ReactDOM.findDOMNode(view);
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.EmailRoomUrl({
roomUrl: "http://invalid",
roomDescription: "www.mozilla.com",
from: "conversation" }));});
it("should invoke callback if defined", function () {
var callback = sinon.stub();
view = mountTestComponent({
callback: callback });
var emailBtn = ReactDOM.findDOMNode(view);
React.addons.TestUtils.Simulate.click(emailBtn);
sinon.assert.calledOnce(callback);});});
describe("FacebookShareButton", function () {
var view;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: []
}
}, props || {});
props = _.extend({
dispatcher: dispatcher,
locationForMetrics: "conversation",
roomData: {
roomUrl: "http://invalid",
roomContextUrls: [] } },
props || {});
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.FacebookShareButton, props));
}
React.createElement(sharedDesktopViews.FacebookShareButton, props));}
it("should dispatch a FacebookShareRoomUrl action when the facebook button is clicked",
function() {
view = mountTestComponent();
var facebookBtn = view.getDOMNode();
it("should dispatch a FacebookShareRoomUrl action when the facebook button is clicked",
function () {
view = mountTestComponent();
React.addons.TestUtils.Simulate.click(facebookBtn);
var facebookBtn = ReactDOM.findDOMNode(view);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.FacebookShareRoomUrl({
from: "conversation",
roomUrl: "http://invalid"
}));
});
it("should invoke callback if defined", function() {
var callback = sinon.stub();
view = mountTestComponent({
callback: callback
});
var facebookBtn = view.getDOMNode();
React.addons.TestUtils.Simulate.click(facebookBtn);
sinon.assert.calledOnce(callback);
});
});
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.FacebookShareRoomUrl({
from: "conversation",
roomUrl: "http://invalid" }));});
describe("SharePanelView", function() {
it("should invoke callback if defined", function () {
var callback = sinon.stub();
view = mountTestComponent({
callback: callback });
var facebookBtn = ReactDOM.findDOMNode(view);
React.addons.TestUtils.Simulate.click(facebookBtn);
sinon.assert.calledOnce(callback);});});
describe("SharePanelView", function () {
var view;
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
facebookEnabled: false,
locationForMetrics: "conversation",
roomData: { roomUrl: "http://invalid" },
savingContext: false,
show: true,
showEditContext: false
}, props);
props = _.extend({
dispatcher: dispatcher,
facebookEnabled: false,
locationForMetrics: "conversation",
roomData: { roomUrl: "http://invalid" },
savingContext: false,
show: true,
showEditContext: false },
props);
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.SharePanelView, props));
}
React.createElement(sharedDesktopViews.SharePanelView, props));}
it("should not display the Facebook Share button when it is disabled in prefs",
function() {
view = mountTestComponent({
facebookEnabled: false
});
expect(view.getDOMNode().querySelectorAll(".btn-facebook"))
.to.have.length.of(0);
});
it("should not display the Facebook Share button when it is disabled in prefs",
function () {
view = mountTestComponent({
facebookEnabled: false });
it("should display the Facebook Share button only when it is enabled in prefs",
function() {
view = mountTestComponent({
facebookEnabled: true
});
expect(view.getDOMNode().querySelectorAll(".btn-facebook"))
.to.have.length.of(1);
});
expect(ReactDOM.findDOMNode(view).querySelectorAll(".btn-facebook")).
to.have.length.of(0);});
it("should not display the panel when show prop is false", function() {
view = mountTestComponent({
show: false
});
expect(view.getDOMNode()).eql(null);
});
it("should display the Facebook Share button only when it is enabled in prefs",
function () {
view = mountTestComponent({
facebookEnabled: true });
it("should not display the panel when roomUrl is not defined", function() {
view = mountTestComponent({
roomData: {}
});
expect(view.getDOMNode()).eql(null);
});
});
expect(ReactDOM.findDOMNode(view).querySelectorAll(".btn-facebook")).
to.have.length.of(1);});
describe("SocialShareDropdown", function() {
it("should not display the panel when show prop is false", function () {
view = mountTestComponent({
show: false });
expect(ReactDOM.findDOMNode(view)).eql(null);});
it("should not display the panel when roomUrl is not defined", function () {
view = mountTestComponent({
roomData: {} });
expect(ReactDOM.findDOMNode(view)).eql(null);});});
describe("SocialShareDropdown", function () {
var fakeProvider, view;
beforeEach(function() {
fakeProvider = {
name: "foo",
origin: "https://foo",
iconURL: "http://example.com/foo.png"
};
});
beforeEach(function () {
fakeProvider = {
name: "foo",
origin: "https://foo",
iconURL: "http://example.com/foo.png" };});
afterEach(function () {
fakeProvider = null;});
afterEach(function() {
fakeProvider = null;
});
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
show: true
}, props);
props = _.extend({
dispatcher: dispatcher,
show: true },
props);
return TestUtils.renderIntoDocument(
React.createElement(sharedDesktopViews.SocialShareDropdown, props));
}
React.createElement(sharedDesktopViews.SocialShareDropdown, props));}
describe("#render", function() {
it("should show no contents when the Social Providers have not been fetched yet", function() {
describe("#render", function () {
it("should show no contents when the Social Providers have not been fetched yet", function () {
view = mountTestComponent();
expect(view.getDOMNode()).to.eql(null);
});
expect(ReactDOM.findDOMNode(view)).to.eql(null);});
it("should show an empty list when no Social Providers are available", function() {
view = mountTestComponent({
socialShareProviders: []
});
var node = view.getDOMNode();
it("should show an empty list when no Social Providers are available", function () {
view = mountTestComponent({
socialShareProviders: [] });
var node = ReactDOM.findDOMNode(view);
expect(node.querySelector(".icon-add-share-service")).to.not.eql(null);
expect(node.querySelectorAll(".dropdown-menu-item").length).to.eql(1);
});
expect(node.querySelectorAll(".dropdown-menu-item").length).to.eql(1);});
it("should show a list of available Social Providers", function() {
view = mountTestComponent({
socialShareProviders: [fakeProvider]
});
var node = view.getDOMNode();
it("should show a list of available Social Providers", function () {
view = mountTestComponent({
socialShareProviders: [fakeProvider] });
var node = ReactDOM.findDOMNode(view);
expect(node.querySelector(".icon-add-share-service")).to.not.eql(null);
expect(node.querySelector(".dropdown-menu-separator")).to.not.eql(null);
var dropdownNodes = node.querySelectorAll(".dropdown-menu-item");
expect(dropdownNodes.length).to.eql(2);
expect(dropdownNodes[1].querySelector("img").src).to.eql(fakeProvider.iconURL);
expect(dropdownNodes[1].querySelector("span").textContent)
.to.eql(fakeProvider.name);
});
});
expect(dropdownNodes[1].querySelector("span").textContent).
to.eql(fakeProvider.name);});});
describe("#handleAddServiceClick", function() {
it("should dispatch an action when the 'add provider' item is clicked", function() {
view = mountTestComponent({
socialShareProviders: []
});
var addItem = view.getDOMNode().querySelector(".dropdown-menu-item:first-child");
describe("#handleAddServiceClick", function () {
it("should dispatch an action when the 'add provider' item is clicked", function () {
view = mountTestComponent({
socialShareProviders: [] });
var addItem = ReactDOM.findDOMNode(view).querySelector(".dropdown-menu-item:first-child");
React.addons.TestUtils.Simulate.click(addItem);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.AddSocialShareProvider());
});
});
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.AddSocialShareProvider());});});
describe("#handleProviderClick", function() {
it("should dispatch an action when a provider item is clicked", function() {
view = mountTestComponent({
roomUrl: "http://example.com",
socialShareProviders: [fakeProvider]
});
var providerItem = view.getDOMNode().querySelector(".dropdown-menu-item:last-child");
describe("#handleProviderClick", function () {
it("should dispatch an action when a provider item is clicked", function () {
view = mountTestComponent({
roomUrl: "http://example.com",
socialShareProviders: [fakeProvider] });
var providerItem = ReactDOM.findDOMNode(view).querySelector(".dropdown-menu-item:last-child");
React.addons.TestUtils.Simulate.click(providerItem);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShareRoomUrl({
provider: fakeProvider,
roomUrl: "http://example.com",
previews: []
}));
});
});
});
});
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShareRoomUrl({
provider: fakeProvider,
roomUrl: "http://example.com",
previews: [] }));});});});});

View File

@@ -1,29 +1,21 @@
/* 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"; /* 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/. */
(function() {
(function () {
"use strict";
// Create fake Components to test chrome-privileged React components.
window.Components = {
utils: {
import: function() {
return {
LoopAPI: {
sendMessageToHandler: function({ name }, callback) {
window.Components = {
utils: {
import: function _import() {
return {
LoopAPI: {
sendMessageToHandler: function sendMessageToHandler(_ref, callback) {var name = _ref.name;
switch (name) {
case "GetLocale":
return callback("en-US");
case "GetPluralRule":
return callback(1);
default:
return callback();
}
}
}
};
}
}
};
})();
return callback();}} } };} } };})();

View File

@@ -1,8 +1,8 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.feedbackViews", function() {
describe("loop.feedbackViews", function () {
"use strict";
var FeedbackView = loop.feedbackViews.FeedbackView;
@@ -10,109 +10,106 @@ describe("loop.feedbackViews", function() {
var TestUtils = React.addons.TestUtils;
var sandbox, mozL10nGet;
beforeEach(function() {
beforeEach(function () {
sandbox = LoopMochaUtils.createSandbox();
mozL10nGet = sandbox.stub(l10n, "get", function(x) {
return "translated:" + x;
});
});
mozL10nGet = sandbox.stub(l10n, "get", function (x) {
return "translated:" + x;});});
afterEach(function() {
sandbox.restore();
});
describe("FeedbackView", function() {
afterEach(function () {
sandbox.restore();});
describe("FeedbackView", function () {
var openURLStub, getLoopPrefStub, feedbackReceivedStub, getAddonVersionStub;
var view;
var fakeURL = "fake.form?version=";
var addonVersion = "1.3.0";
function mountTestComponent(props) {
props = _.extend({
onAfterFeedbackReceived: feedbackReceivedStub
}, props);
props = _.extend({
onAfterFeedbackReceived: feedbackReceivedStub },
props);
return TestUtils.renderIntoDocument(
React.createElement(FeedbackView, props));
}
React.createElement(FeedbackView, props));}
beforeEach(function() {
beforeEach(function () {
openURLStub = sandbox.stub();
getLoopPrefStub = sandbox.stub();
feedbackReceivedStub = sandbox.stub();
getAddonVersionStub = sandbox.stub();
LoopMochaUtils.stubLoopRequest({
OpenURL: openURLStub,
GetLoopPref: getLoopPrefStub,
GetAddonVersion: getAddonVersionStub
});
});
LoopMochaUtils.stubLoopRequest({
OpenURL: openURLStub,
GetLoopPref: getLoopPrefStub,
GetAddonVersion: getAddonVersionStub });});
afterEach(function() {
afterEach(function () {
view = null;
LoopMochaUtils.restore();
});
LoopMochaUtils.restore();});
it("should render a feedback view", function() {
it("should render a feedback view", function () {
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view, FeedbackView);
});
TestUtils.findRenderedComponentWithType(view, FeedbackView);});
it("should render a button with correct text", function() {
it("should render a button with correct text", function () {
view = mountTestComponent();
sinon.assert.calledWithExactly(mozL10nGet, "feedback_request_button");
});
sinon.assert.calledWithExactly(mozL10nGet, "feedback_request_button");});
it("should render a header with correct text", function() {
it("should render a header with correct text", function () {
view = mountTestComponent();
sinon.assert.calledWithExactly(mozL10nGet, "feedback_window_heading");
});
sinon.assert.calledWithExactly(mozL10nGet, "feedback_window_heading");});
it("should open a new page to the feedback form", function() {
it("should open a new page to the feedback form", function () {
getLoopPrefStub.withArgs("feedback.formURL").returns(fakeURL);
getAddonVersionStub.returns(addonVersion);
view = mountTestComponent();
TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
TestUtils.Simulate.click(ReactDOM.findDOMNode(view.refs.feedbackFormBtn));
sinon.assert.calledOnce(openURLStub);
sinon.assert.calledWithExactly(openURLStub, fakeURL);
});
sinon.assert.calledWithExactly(openURLStub, fakeURL);});
it("should fetch the feedback form URL from the prefs", function() {
it("should fetch the feedback form URL from the prefs", function () {
getLoopPrefStub.withArgs("feedback.formURL").returns(fakeURL);
getAddonVersionStub.returns(addonVersion);
view = mountTestComponent();
TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
TestUtils.Simulate.click(ReactDOM.findDOMNode(view.refs.feedbackFormBtn));
sinon.assert.calledOnce(getLoopPrefStub);
sinon.assert.calledWithExactly(getLoopPrefStub, "feedback.formURL");
});
sinon.assert.calledWithExactly(getLoopPrefStub, "feedback.formURL");});
it("should fetch the addon version", function() {
it("should fetch the addon version", function () {
getLoopPrefStub.withArgs("feedback.formURL").returns(fakeURL);
getAddonVersionStub.returns(addonVersion);
view = mountTestComponent();
TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
TestUtils.Simulate.click(ReactDOM.findDOMNode(view.refs.feedbackFormBtn));
sinon.assert.calledOnce(getAddonVersionStub);
sinon.assert.calledWithExactly(getAddonVersionStub);
});
sinon.assert.calledWithExactly(getAddonVersionStub);});
it("should close the window after opening the form", function() {
it("should close the window after opening the form", function () {
getLoopPrefStub.withArgs("feedback.formURL").returns(fakeURL);
getAddonVersionStub.returns(addonVersion);
view = mountTestComponent();
TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
TestUtils.Simulate.click(ReactDOM.findDOMNode(view.refs.feedbackFormBtn));
sinon.assert.calledOnce(feedbackReceivedStub);
});
});
});
sinon.assert.calledOnce(feedbackReceivedStub);});});});

View File

@@ -25,6 +25,7 @@
<!-- test dependencies -->
<script src="/add-on/panels/vendor/l10n.js"></script>
<script src="/shared/vendor/react.js"></script>
<script src="/shared/vendor/react-dom.js"></script>
<script src="/shared/vendor/classnames.js"></script>
<script src="/shared/vendor/backbone.js"></script>
<script src="/add-on/panels/vendor/simpleSlideshow.js"></script>
@@ -44,7 +45,7 @@
<!-- App scripts -->
<script src="/add-on/shared/js/loopapi-client.js"></script>
<script src="/add-on/shared/js/utils.js"></script>
<script src="/add-on/shared/js/models.js"></script>
<script src="/add-on/panels/js/models.js"></script>
<script src="/add-on/shared/js/mixins.js"></script>
<script src="/add-on/shared/js/actions.js"></script>
<script src="/add-on/shared/js/validate.js"></script>

View File

@@ -1,111 +1,108 @@
/* 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"; /* 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 expect = chai.expect;
describe("document.mozL10n", function() {
describe("document.mozL10n", function () {
"use strict";
var sandbox, l10nOptions;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
});
beforeEach(function () {
sandbox = LoopMochaUtils.createSandbox();});
afterEach(function() {
sandbox.restore();
});
describe("#initialize", function() {
beforeEach(function() {
sandbox.stub(console, "error");
});
afterEach(function () {
sandbox.restore();});
it("should correctly set the plural form", function() {
l10nOptions = {
locale: "en-US",
getStrings: function(key) {
describe("#initialize", function () {
beforeEach(function () {
sandbox.stub(console, "error");});
it("should correctly set the plural form", function () {
l10nOptions = {
locale: "en-US",
getStrings: function getStrings(key) {
if (key === "plural") {
return '{"textContent":"{{num}} form;{{num}} forms;{{num}} form 2"}';
}
return '{"textContent":"{{num}} form;{{num}} forms;{{num}} form 2"}';}
return '{"textContent":"' + key + '"}';},
pluralRule: 14 };
return '{"textContent":"' + key + '"}';
},
pluralRule: 14
};
document.mozL10n.initialize(l10nOptions);
expect(document.mozL10n.get("plural", { num: 39 })).eql("39 form 2");
});
expect(document.mozL10n.get("plural", { num: 39 })).eql("39 form 2");});
it("should log an error if the rule number is invalid", function() {
l10nOptions = {
locale: "en-US",
getStrings: function(key) {
it("should log an error if the rule number is invalid", function () {
l10nOptions = {
locale: "en-US",
getStrings: function getStrings(key) {
if (key === "plural") {
return '{"textContent":"{{num}} form;{{num}} forms;{{num}} form 2"}';
}
return '{"textContent":"{{num}} form;{{num}} forms;{{num}} form 2"}';}
return '{"textContent":"' + key + '"}';},
pluralRule: NaN };
return '{"textContent":"' + key + '"}';
},
pluralRule: NaN
};
document.mozL10n.initialize(l10nOptions);
sinon.assert.calledOnce(console.error);
});
sinon.assert.calledOnce(console.error);});
it("should use rule 0 if the rule number is invalid", function() {
l10nOptions = {
locale: "en-US",
getStrings: function(key) {
it("should use rule 0 if the rule number is invalid", function () {
l10nOptions = {
locale: "en-US",
getStrings: function getStrings(key) {
if (key === "plural") {
return '{"textContent":"{{num}} form;{{num}} forms;{{num}} form 2"}';
}
return '{"textContent":"{{num}} form;{{num}} forms;{{num}} form 2"}';}
return '{"textContent":"' + key + '"}';},
pluralRule: NaN };
return '{"textContent":"' + key + '"}';
},
pluralRule: NaN
};
document.mozL10n.initialize(l10nOptions);
expect(document.mozL10n.get("plural", { num: 39 })).eql("39 form");
});
});
expect(document.mozL10n.get("plural", { num: 39 })).eql("39 form");});});
describe("#get", function() {
beforeEach(function() {
l10nOptions = {
locale: "en-US",
getStrings: function(key) {
describe("#get", function () {
beforeEach(function () {
l10nOptions = {
locale: "en-US",
getStrings: function getStrings(key) {
if (key === "plural") {
return '{"textContent":"{{num}} plural form;{{num}} plural forms"}';
}
return '{"textContent":"{{num}} plural form;{{num}} plural forms"}';}
return '{"textContent":"' + key + '"}';
},
getPluralForm: function(num, string) {
return string.split(";")[num === 0 ? 0 : 1];
}
};
document.mozL10n.initialize(l10nOptions);
});
return '{"textContent":"' + key + '"}';},
it("should get a simple string", function() {
expect(document.mozL10n.get("test")).eql("test");
});
getPluralForm: function getPluralForm(num, string) {
return string.split(";")[num === 0 ? 0 : 1];} };
it("should get a plural form", function() {
expect(document.mozL10n.get("plural", { num: 10 })).eql("10 plural forms");
});
it("should correctly get a plural form for num = 0", function() {
expect(document.mozL10n.get("plural", { num: 0 })).eql("0 plural form");
});
});
});
document.mozL10n.initialize(l10nOptions);});
it("should get a simple string", function () {
expect(document.mozL10n.get("test")).eql("test");});
it("should get a plural form", function () {
expect(document.mozL10n.get("plural", { num: 10 })).eql("10 plural forms");});
it("should correctly get a plural form for num = 0", function () {
expect(document.mozL10n.get("plural", { num: 0 })).eql("0 plural form");});});});

View File

@@ -1,8 +1,8 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.slideshow", function() {
describe("loop.slideshow", function () {
"use strict";
var expect = chai.expect;
@@ -11,94 +11,90 @@ describe("loop.slideshow", function() {
var sandbox;
var data;
beforeEach(function() {
beforeEach(function () {
sandbox = LoopMochaUtils.createSandbox();
LoopMochaUtils.stubLoopRequest({
GetAllStrings: function() {
return JSON.stringify({ textContent: "fakeText" });
},
GetLocale: function() {
return "en-US";
},
GetPluralRule: function() {
return 1;
},
GetPluralForm: function() {
return "fakeText";
}
});
LoopMochaUtils.stubLoopRequest({
GetAllStrings: function GetAllStrings() {
return JSON.stringify({ textContent: "fakeText" });},
GetLocale: function GetLocale() {
return "en-US";},
GetPluralRule: function GetPluralRule() {
return 1;},
GetPluralForm: function GetPluralForm() {
return "fakeText";} });
data = [
{
id: "slide1",
imageClass: "slide1-image",
title: "fakeString",
text: "fakeString"
},
{
id: "slide2",
imageClass: "slide2-image",
title: "fakeString",
text: "fakeString"
},
{
id: "slide3",
imageClass: "slide3-image",
title: "fakeString",
text: "fakeString"
},
{
id: "slide4",
imageClass: "slide4-image",
title: "fakeString",
text: "fakeString"
}
];
{
id: "slide1",
imageClass: "slide1-image",
title: "fakeString",
text: "fakeString" },
{
id: "slide2",
imageClass: "slide2-image",
title: "fakeString",
text: "fakeString" },
{
id: "slide3",
imageClass: "slide3-image",
title: "fakeString",
text: "fakeString" },
{
id: "slide4",
imageClass: "slide4-image",
title: "fakeString",
text: "fakeString" }];
document.mozL10n.initialize({
getStrings: function getStrings() {
return JSON.stringify({ textContent: "fakeText" });},
locale: "en-US" });
document.mozL10n.initialize({
getStrings: function() {
return JSON.stringify({ textContent: "fakeText" });
},
locale: "en-US"
});
sandbox.stub(document.mozL10n, "get").returns("fakeString");
sandbox.stub(sharedUtils, "getPlatform").returns("other");
});
sandbox.stub(sharedUtils, "getPlatform").returns("other");});
afterEach(function() {
afterEach(function () {
sandbox.restore();
LoopMochaUtils.restore();
});
LoopMochaUtils.restore();});
describe("#init", function() {
beforeEach(function() {
sandbox.stub(React, "render");
describe("#init", function () {
beforeEach(function () {
sandbox.stub(ReactDOM, "render");
sandbox.stub(document.mozL10n, "initialize");
sandbox.stub(loop.SimpleSlideshow, "init");
});
sandbox.stub(loop.SimpleSlideshow, "init");});
it("should initalize L10n", function() {
it("should initalize L10n", function () {
loop.slideshow.init();
sinon.assert.calledOnce(document.mozL10n.initialize);
sinon.assert.calledWith(document.mozL10n.initialize, sinon.match({ locale: "en-US" }));
});
sinon.assert.calledWith(document.mozL10n.initialize, sinon.match({ locale: "en-US" }));});
it("should call the slideshow init with the right arguments", function() {
it("should call the slideshow init with the right arguments", function () {
loop.slideshow.init();
sinon.assert.calledOnce(loop.SimpleSlideshow.init);
sinon.assert.calledWith(loop.SimpleSlideshow.init, sinon.match("#main", data));
});
sinon.assert.calledWith(loop.SimpleSlideshow.init, sinon.match("#main", data));});
it("should set the document attributes correctly", function() {
it("should set the document attributes correctly", function () {
loop.slideshow.init();
expect(document.documentElement.getAttribute("lang")).to.eql("en-US");
expect(document.documentElement.getAttribute("dir")).to.eql("ltr");
expect(document.body.getAttribute("platform")).to.eql("other");
});
});
});
expect(document.body.getAttribute("platform")).to.eql("other");});});});

View File

@@ -1,4 +1,4 @@
// This is derived from PIOTR F's code,
"use strict"; // This is derived from PIOTR F's code,
// currently available at https://github.com/piotrf/simple-react-slideshow
// Simple React Slideshow Example
@@ -35,181 +35,161 @@ loop.SimpleSlideshow = function () {
"use strict";
// App state
var state = {
currentSlide: 0,
data: [] };
var state = {
currentSlide: 0,
data: []
};
// State transitions
var actions = {
toggleNext: function () {
var actions = {
toggleNext: function toggleNext() {
var current = state.currentSlide;
var next = current + 1;
if (next < state.data.length) {
state.currentSlide = next;
}
render();
},
togglePrev: function () {
state.currentSlide = next;}
render();},
togglePrev: function togglePrev() {
var current = state.currentSlide;
var prev = current - 1;
if (prev >= 0) {
state.currentSlide = prev;
}
render();
},
toggleSlide: function (id) {
state.currentSlide = prev;}
render();},
toggleSlide: function toggleSlide(id) {
var index = state.data.map(function (el) {
return el.id;
});
return (
el.id);});
var currentIndex = index.indexOf(id);
state.currentSlide = currentIndex;
render();
}
};
render();} };
var Slideshow = React.createClass({
displayName: "Slideshow",
propTypes: {
data: React.PropTypes.array.isRequired
},
render: function () {
return React.createElement(
"div",
{ className: "slideshow" },
React.createElement(Slides, { data: this.props.data }),
React.createElement(
"div",
{ className: "control-panel" },
React.createElement(Controls, null)
)
);
}
});
var Slides = React.createClass({
displayName: "Slides",
var Slideshow = React.createClass({ displayName: "Slideshow",
propTypes: {
data: React.PropTypes.array.isRequired },
propTypes: {
data: React.PropTypes.array.isRequired
},
render: function () {
render: function render() {
return (
React.createElement("div", { className: "slideshow" },
React.createElement(Slides, { data: this.props.data }),
React.createElement("div", { className: "control-panel" },
React.createElement(Controls, null))));} });
var Slides = React.createClass({ displayName: "Slides",
propTypes: {
data: React.PropTypes.array.isRequired },
render: function render() {
var slidesNodes = this.props.data.map(function (slideNode, index) {
var isActive = state.currentSlide === index;
return React.createElement(Slide, { active: isActive,
imageClass: slideNode.imageClass,
indexClass: slideNode.id,
text: slideNode.text,
title: slideNode.title });
});
return React.createElement(
"div",
{ className: "slides" },
slidesNodes
);
}
});
return (
React.createElement(Slide, { active: isActive,
imageClass: slideNode.imageClass,
indexClass: slideNode.id,
text: slideNode.text,
title: slideNode.title }));});
var Slide = React.createClass({
displayName: "Slide",
propTypes: {
active: React.PropTypes.bool.isRequired,
imageClass: React.PropTypes.string.isRequired,
indexClass: React.PropTypes.string.isRequired,
text: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired
},
render: function () {
var classes = classNames({
"slide": true,
"slide--active": this.props.active
});
return React.createElement(
"div",
{ className: classes },
React.createElement(
"div",
{ className: this.props.indexClass },
React.createElement(
"div",
{ className: "slide-layout" },
React.createElement("img", { className: this.props.imageClass }),
React.createElement(
"h2",
null,
this.props.title
),
React.createElement(
"div",
{ className: "slide-text" },
this.props.text
)
)
)
);
}
});
return (
React.createElement("div", { className: "slides" },
slidesNodes));} });
var Controls = React.createClass({
displayName: "Controls",
togglePrev: function () {
actions.togglePrev();
},
toggleNext: function () {
actions.toggleNext();
},
render: function () {
var Slide = React.createClass({ displayName: "Slide",
propTypes: {
active: React.PropTypes.bool.isRequired,
imageClass: React.PropTypes.string.isRequired,
indexClass: React.PropTypes.string.isRequired,
text: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired },
render: function render() {
var classes = classNames({
"slide": true,
"slide--active": this.props.active });
return (
React.createElement("div", { className: classes },
React.createElement("div", { className: this.props.indexClass },
React.createElement("div", { className: "slide-layout" },
React.createElement("img", { className: this.props.imageClass }),
React.createElement("h2", null, this.props.title),
React.createElement("div", { className: "slide-text" }, this.props.text)))));} });
var Controls = React.createClass({ displayName: "Controls",
togglePrev: function togglePrev() {
actions.togglePrev();},
toggleNext: function toggleNext() {
actions.toggleNext();},
render: function render() {
var showPrev, showNext;
var current = state.currentSlide;
var last = state.data.length;
if (current > 0) {
showPrev = React.createElement("div", { className: "toggle toggle-prev", onClick: this.togglePrev });
}
showPrev = React.createElement("div", { className: "toggle toggle-prev", onClick: this.togglePrev });}
if (current < last - 1) {
showNext = React.createElement("div", { className: "toggle toggle-next", onClick: this.toggleNext });
}
return React.createElement(
"div",
{ className: "controls" },
showPrev,
showNext
);
}
});
showNext = React.createElement("div", { className: "toggle toggle-next", onClick: this.toggleNext });}
return (
React.createElement("div", { className: "controls" },
showPrev,
showNext));} });
var EmptyMessage = React.createClass({ displayName: "EmptyMessage",
render: function render() {
return (
React.createElement("div", { className: "empty-message" }, "No Data"));} });
var EmptyMessage = React.createClass({
displayName: "EmptyMessage",
render: function () {
return React.createElement(
"div",
{ className: "empty-message" },
"No Data"
);
}
});
function render(renderTo) {
var hasData = state.data.length > 0;
var component;
if (hasData) {
component = React.createElement(Slideshow, { data: state.data });
} else {
component = React.createElement(EmptyMessage, null);
}
React.render(component, document.querySelector(renderTo ? renderTo : "#main"));
}
component = React.createElement(Slideshow, { data: state.data });} else
{
component = React.createElement(EmptyMessage, null);}
ReactDOM.render(
component,
document.querySelector(renderTo ? renderTo : "#main"));}
function init(renderTo, data) {
state.data = data;
render(renderTo);
}
render(renderTo);}
return {
init: init
};
}();
return {
init: init };}();

View File

@@ -47,4 +47,4 @@ pref("loop.facebook.enabled", true);
pref("loop.facebook.appId", "1519239075036718");
pref("loop.facebook.shareUrl", "https://www.facebook.com/dialog/send?app_id=%APP_ID%&link=%ROOM_URL%&redirect_uri=%REDIRECT_URI%");
pref("loop.facebook.fallbackUrl", "https://hello.firefox.com/");
pref("loop.conversationPopOut.enabled", true);
pref("loop.conversationPopOut.enabled", false);

View File

@@ -767,7 +767,7 @@ html, .fx-embedded, #main,
.text-chat-view > .text-chat-entries {
width: 100%;
overflow: auto;
padding-top: 15px;
padding-top: 8px;
/* 40px is the height of .text-chat-box. */
height: calc(100% - 40px);
}
@@ -1006,14 +1006,13 @@ html[dir="rtl"] .text-chat-entry.received .text-chat-arrow {
max-width: 180px;
}
.text-chat-header.special.room-name {
.text-chat-header.special {
color: #666;
font-weight: bold;
margin-bottom: 0;
margin-right: 0;
}
.text-chat-header.special.room-name p {
.text-chat-header.special p {
width: 100%;
padding: 0.8rem 1rem 1.4rem;
margin: 0;

View File

@@ -0,0 +1 @@
<svg width="13" height="10" viewBox="0 0 13 10" xmlns="http://www.w3.org/2000/svg"><path d="M6.246 7.1h3.02c.556 0 1-.448 1-1v-3c0-.557-.448-1-1-1h-3.02V.56c0-.54-.356-.725-.796-.392L.441 3.962c-.441.334-.44.873 0 1.206L5.45 8.962c.441.334.796.157.796-.393V7.1zm6.02 2h-5v-1h4v-7h-4V.09h5V9.1z" fill="#757575" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -1,10 +1,10 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.actions = (function() {
loop.shared.actions = function () {
"use strict";
/**
@@ -15,199 +15,199 @@ loop.shared.actions = (function() {
*/
function Action(name, schema, values) {
var validatedData = new loop.validate.Validator(schema || {})
.validate(values || {});
for (var prop in validatedData) {
this[prop] = validatedData[prop];
}
var validatedData = new loop.validate.Validator(schema || {}).
validate(values || {});
Object.keys(validatedData).forEach(function (prop) {
this[prop] = validatedData[prop];}.
bind(this));
this.name = name;
}
this.name = name;}
Action.define = function(name, schema) {
return Action.bind(null, name, schema);
};
return {
Action: Action,
Action.define = function (name, schema) {
return Action.bind(null, name, schema);};
return {
Action: Action,
/**
* Get the window data for the provided window id
*/
GetWindowData: Action.define("getWindowData", {
windowId: String
}),
GetWindowData: Action.define("getWindowData", {
windowId: String }),
/**
* Extract the token information and type for the standalone window
*/
ExtractTokenInfo: Action.define("extractTokenInfo", {
windowPath: String,
windowHash: String
}),
ExtractTokenInfo: Action.define("extractTokenInfo", {
windowPath: String,
windowHash: String }),
/**
* Used to pass round the window data so that stores can
* record the appropriate data.
*/
SetupWindowData: Action.define("setupWindowData", {
windowId: String,
SetupWindowData: Action.define("setupWindowData", {
windowId: String,
type: String
// Optional Items. There are other optional items typically sent
// around with this action. They are for the setup of calls and rooms and
// depend on the type. See LoopRooms for the details of this data.
}),
}),
/**
* Used to fetch the data from the server for a room or call for the
* token.
*/
FetchServerData: Action.define("fetchServerData", {
FetchServerData: Action.define("fetchServerData", {
// cryptoKey: String - Optional.
token: String,
windowType: String
}),
token: String,
windowType: String }),
/**
* Used to signal when the window is being unloaded.
*/
WindowUnload: Action.define("windowUnload", {
}),
WindowUnload: Action.define("windowUnload", {}),
/**
* Used to indicate the remote peer was disconnected for some reason.
*
* peerHungup is true if the peer intentionally disconnected, false otherwise.
*/
RemotePeerDisconnected: Action.define("remotePeerDisconnected", {
peerHungup: Boolean
}),
RemotePeerDisconnected: Action.define("remotePeerDisconnected", {
peerHungup: Boolean }),
/**
* Used for notifying of connection failures.
*/
ConnectionFailure: Action.define("connectionFailure", {
ConnectionFailure: Action.define("connectionFailure", {
// A string relating to the reason the connection failed.
reason: String
}),
reason: String }),
/**
* Used to notify that the sdk session is now connected to the servers.
*/
ConnectedToSdkServers: Action.define("connectedToSdkServers", {
}),
ConnectedToSdkServers: Action.define("connectedToSdkServers", {}),
/**
* Used to notify that a remote peer has connected to the room.
*/
RemotePeerConnected: Action.define("remotePeerConnected", {
}),
RemotePeerConnected: Action.define("remotePeerConnected", {}),
/**
* Used to notify that the session has a data channel available.
*/
DataChannelsAvailable: Action.define("dataChannelsAvailable", {
available: Boolean
}),
DataChannelsAvailable: Action.define("dataChannelsAvailable", {
available: Boolean }),
/**
* Used to send a message to the other peer.
*/
SendTextChatMessage: Action.define("sendTextChatMessage", {
contentType: String,
message: String,
sentTimestamp: String
}),
SendTextChatMessage: Action.define("sendTextChatMessage", {
contentType: String,
message: String,
sentTimestamp: String }),
/**
* Notifies that a message has been received from the other peer.
*/
ReceivedTextChatMessage: Action.define("receivedTextChatMessage", {
contentType: String,
message: String,
ReceivedTextChatMessage: Action.define("receivedTextChatMessage", {
contentType: String,
message: String,
receivedTimestamp: String
// sentTimestamp: String (optional)
}),
}),
/**
* Used to send cursor data to the other peer
*/
SendCursorData: Action.define("sendCursorData", {
SendCursorData: Action.define("sendCursorData", {
// ratioX: Number (optional)
// ratioY: Number (optional)
type: String
}),
type: String }),
/**
* Notifies that cursor data has been received from the other peer.
*/
ReceivedCursorData: Action.define("receivedCursorData", {
ReceivedCursorData: Action.define("receivedCursorData", {
// ratioX: Number (optional)
// ratioY: Number (optional)
type: String
}),
type: String }),
/**
* Used by the ongoing views to notify stores about the elements
* required for the sdk.
*/
SetupStreamElements: Action.define("setupStreamElements", {
SetupStreamElements: Action.define("setupStreamElements", {
// The configuration for the publisher/subscribe options
publisherConfig: Object
}),
publisherConfig: Object }),
/**
* Used for notifying that a waiting tile was shown.
*/
TileShown: Action.define("tileShown", {
}),
TileShown: Action.define("tileShown", {}),
/**
* Used for notifying that local media has been obtained.
*/
GotMediaPermission: Action.define("gotMediaPermission", {
}),
GotMediaPermission: Action.define("gotMediaPermission", {}),
/**
* Used for notifying that the media is now up for the call.
*/
MediaConnected: Action.define("mediaConnected", {
}),
MediaConnected: Action.define("mediaConnected", {}),
/**
* Used for notifying that the dimensions of a stream just changed. Also
* dispatched when a stream connects for the first time.
*/
VideoDimensionsChanged: Action.define("videoDimensionsChanged", {
isLocal: Boolean,
videoType: String,
dimensions: Object
}),
VideoDimensionsChanged: Action.define("videoDimensionsChanged", {
isLocal: Boolean,
videoType: String,
dimensions: Object }),
/**
* Used for notifying that the hasVideo property of the screen stream, has changed.
*/
VideoScreenStreamChanged: Action.define("videoScreenStreamChanged", {
hasVideo: Boolean
}),
VideoScreenStreamChanged: Action.define("videoScreenStreamChanged", {
hasVideo: Boolean }),
/**
* A stream from local or remote media has been created.
*/
MediaStreamCreated: Action.define("mediaStreamCreated", {
hasAudio: Boolean,
hasVideo: Boolean,
isLocal: Boolean,
srcMediaElement: Object
}),
MediaStreamCreated: Action.define("mediaStreamCreated", {
hasAudio: Boolean,
hasVideo: Boolean,
isLocal: Boolean,
srcMediaElement: Object }),
/**
* A stream from local or remote media has been destroyed.
*/
MediaStreamDestroyed: Action.define("mediaStreamDestroyed", {
isLocal: Boolean
}),
MediaStreamDestroyed: Action.define("mediaStreamDestroyed", {
isLocal: Boolean }),
/**
* Used to inform that the remote stream has enabled or disabled the video
@@ -217,57 +217,57 @@ loop.shared.actions = (function() {
* and documented here:
* https://tokbox.com/opentok/libraries/client/js/reference/VideoEnabledChangedEvent.html
*/
RemoteVideoStatus: Action.define("remoteVideoStatus", {
videoEnabled: Boolean
}),
RemoteVideoStatus: Action.define("remoteVideoStatus", {
videoEnabled: Boolean }),
/**
* Used to mute or unmute a stream
*/
SetMute: Action.define("setMute", {
SetMute: Action.define("setMute", {
// The part of the stream to enable, e.g. "audio" or "video"
type: String,
type: String,
// Whether or not to enable the stream.
enabled: Boolean
}),
enabled: Boolean }),
/**
* Used to start a browser tab share.
*/
StartBrowserShare: Action.define("startBrowserShare", {
}),
StartBrowserShare: Action.define("startBrowserShare", {}),
/**
* Used to end a screen share.
*/
EndScreenShare: Action.define("endScreenShare", {
}),
EndScreenShare: Action.define("endScreenShare", {}),
/**
* Used to mute or unmute a screen share.
*/
ToggleBrowserSharing: Action.define("toggleBrowserSharing", {
ToggleBrowserSharing: Action.define("toggleBrowserSharing", {
// Whether or not to enable the stream.
enabled: Boolean
}),
enabled: Boolean }),
/**
* Used to notify that screen sharing is active or not.
*/
ScreenSharingState: Action.define("screenSharingState", {
ScreenSharingState: Action.define("screenSharingState", {
// One of loop.shared.utils.SCREEN_SHARE_STATES.
state: String
}),
state: String }),
/**
* Used to notify that a shared screen is being received (or not).
*
* XXX this should be split into multiple actions to make the code clearer.
*/
ReceivingScreenShare: Action.define("receivingScreenShare", {
ReceivingScreenShare: Action.define("receivingScreenShare", {
receiving: Boolean
// srcMediaElement: Object (only present if receiving is true)
}),
}),
/**
* Creates a new room.
@@ -276,107 +276,107 @@ loop.shared.actions = (function() {
CreateRoom: Action.define("createRoom", {
// See https://wiki.mozilla.org/Loop/Architecture/Context#Format_of_context.value
// urls: Object - Optional
}),
}),
/**
* When a room has been created.
* XXX: should move to some roomActions module - refs bug 1079284
*/
CreatedRoom: Action.define("createdRoom", {
decryptedContext: Object,
roomToken: String,
roomUrl: String
}),
CreatedRoom: Action.define("createdRoom", {
decryptedContext: Object,
roomToken: String,
roomUrl: String }),
/**
* Rooms creation error.
* XXX: should move to some roomActions module - refs bug 1079284
*/
CreateRoomError: Action.define("createRoomError", {
CreateRoomError: Action.define("createRoomError", {
// There's two types of error possible - one thrown by our code (and Error)
// and the other is an Object about the error codes from the server as
// returned by the Hawk request.
error: Object
}),
error: Object }),
/**
* Deletes a room.
* XXX: should move to some roomActions module - refs bug 1079284
*/
DeleteRoom: Action.define("deleteRoom", {
roomToken: String
}),
DeleteRoom: Action.define("deleteRoom", {
roomToken: String }),
/**
* Room deletion error.
* XXX: should move to some roomActions module - refs bug 1079284
*/
DeleteRoomError: Action.define("deleteRoomError", {
DeleteRoomError: Action.define("deleteRoomError", {
// There's two types of error possible - one thrown by our code (and Error)
// and the other is an Object about the error codes from the server as
// returned by the Hawk request.
error: Object
}),
error: Object }),
/**
* Retrieves room list.
* XXX: should move to some roomActions module - refs bug 1079284
*/
GetAllRooms: Action.define("getAllRooms", {
}),
GetAllRooms: Action.define("getAllRooms", {}),
/**
* An error occured while trying to fetch the room list.
* XXX: should move to some roomActions module - refs bug 1079284
*/
GetAllRoomsError: Action.define("getAllRoomsError", {
GetAllRoomsError: Action.define("getAllRoomsError", {
// There's two types of error possible - one thrown by our code (and Error)
// and the other is an Object about the error codes from the server as
// returned by the Hawk request.
error: [Error, Object]
}),
error: [Error, Object] }),
/**
* Updates room list.
* XXX: should move to some roomActions module - refs bug 1079284
*/
UpdateRoomList: Action.define("updateRoomList", {
roomList: Array
}),
UpdateRoomList: Action.define("updateRoomList", {
roomList: Array }),
/**
* Opens a room.
* XXX: should move to some roomActions module - refs bug 1079284
*/
OpenRoom: Action.define("openRoom", {
roomToken: String
}),
OpenRoom: Action.define("openRoom", {
roomToken: String }),
/**
* Updates the context data attached to a room.
* XXX: should move to some roomActions module - refs bug 1079284
*/
UpdateRoomContext: Action.define("updateRoomContext", {
UpdateRoomContext: Action.define("updateRoomContext", {
roomToken: String
// newRoomName: String, Optional.
// newRoomDescription: String, Optional.
// newRoomThumbnail: String, Optional.
// newRoomURL: String Optional.
// sentTimestamp: String, Optional.
}),
}),
/**
* Updating the context data attached to a room error.
*/
UpdateRoomContextError: Action.define("updateRoomContextError", {
error: [Error, Object]
}),
UpdateRoomContextError: Action.define("updateRoomContextError", {
error: [Error, Object] }),
/**
* Updating the context data attached to a room finished successfully.
*/
UpdateRoomContextDone: Action.define("updateRoomContextDone", {
}),
UpdateRoomContextDone: Action.define("updateRoomContextDone", {}),
/**
* Copy a room url into the user's clipboard.
@@ -385,10 +385,10 @@ loop.shared.actions = (function() {
* Possible values ['panel', 'conversation']
* @roomUrl: the URL that is shared
*/
CopyRoomUrl: Action.define("copyRoomUrl", {
from: String,
roomUrl: String
}),
CopyRoomUrl: Action.define("copyRoomUrl", {
from: String,
roomUrl: String }),
/**
* Email a room url.
@@ -397,11 +397,11 @@ loop.shared.actions = (function() {
* Possible values ['panel', 'conversation']
* @roomUrl: the URL that is shared
*/
EmailRoomUrl: Action.define("emailRoomUrl", {
from: String,
EmailRoomUrl: Action.define("emailRoomUrl", {
from: String,
roomUrl: String
// roomDescription: String, Optional.
}),
}),
/**
* Share a room url with Facebook.
@@ -411,11 +411,11 @@ loop.shared.actions = (function() {
* @roomUrl: the URL that is shared.
* @roomOrigin: the URL browsed when the sharing is started - Optional.
*/
FacebookShareRoomUrl: Action.define("facebookShareRoomUrl", {
from: String,
FacebookShareRoomUrl: Action.define("facebookShareRoomUrl", {
from: String,
roomUrl: String
// roomOrigin: String
}),
}),
/**
* Share a room url via the Social API.
@@ -423,26 +423,26 @@ loop.shared.actions = (function() {
* @provider: one of the share-capable Social Providers included
* @roomUrl: the URL that is shared
*/
ShareRoomUrl: Action.define("shareRoomUrl", {
provider: Object,
roomUrl: String
}),
ShareRoomUrl: Action.define("shareRoomUrl", {
provider: Object,
roomUrl: String }),
/**
* Open the share panel to add a Social share provider.
* XXX: should move to some roomActions module - refs bug 1079284
*/
AddSocialShareProvider: Action.define("addSocialShareProvider", {
}),
AddSocialShareProvider: Action.define("addSocialShareProvider", {}),
/**
* XXX: should move to some roomActions module - refs bug 1079284
*/
RoomFailure: Action.define("roomFailure", {
error: Object,
RoomFailure: Action.define("roomFailure", {
error: Object,
// True when the failures occurs in the join room request to the loop-server.
failedJoinRequest: Boolean
}),
failedJoinRequest: Boolean }),
/**
* Updates the room information when it is received.
@@ -450,7 +450,7 @@ loop.shared.actions = (function() {
*
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
*/
UpdateRoomInfo: Action.define("updateRoomInfo", {
UpdateRoomInfo: Action.define("updateRoomInfo", {
// participants: Array - Optional.
// roomContextUrls: Array - Optional.
// See https://wiki.mozilla.org/Loop/Architecture/Context#Format_of_context.value
@@ -460,46 +460,46 @@ loop.shared.actions = (function() {
// roomState: String - Optional.
roomUrl: String
// socialShareProviders: Array - Optional.
}),
}),
/**
* Notifies if the user agent will handle the room or not.
*/
UserAgentHandlesRoom: Action.define("userAgentHandlesRoom", {
handlesRoom: Boolean
}),
UserAgentHandlesRoom: Action.define("userAgentHandlesRoom", {
handlesRoom: Boolean }),
/**
* Updates the Social API information when it is received.
* XXX: should move to some roomActions module - refs bug 1079284
*/
UpdateSocialShareInfo: Action.define("updateSocialShareInfo", {
socialShareProviders: Array
}),
UpdateSocialShareInfo: Action.define("updateSocialShareInfo", {
socialShareProviders: Array }),
/**
* Starts the process for the user to join the room.
* XXX: should move to some roomActions module - refs bug 1079284
*/
JoinRoom: Action.define("joinRoom", {
}),
JoinRoom: Action.define("joinRoom", {}),
/**
* A special action for metrics logging to define what type of join
* occurred when JoinRoom was activated.
* XXX: should move to some roomActions module - refs bug 1079284
*/
MetricsLogJoinRoom: Action.define("metricsLogJoinRoom", {
MetricsLogJoinRoom: Action.define("metricsLogJoinRoom", {
userAgentHandledRoom: Boolean
// ownRoom: Boolean - Optional. Expected if firefoxHandledRoom is true.
}),
}),
/**
* Starts the process for the user to join the room.
* XXX: should move to some roomActions module - refs bug 1079284
*/
RetryAfterRoomFailure: Action.define("retryAfterRoomFailure", {
}),
RetryAfterRoomFailure: Action.define("retryAfterRoomFailure", {}),
/**
* Signals the user has successfully joined the room on the loop-server.
@@ -507,12 +507,12 @@ loop.shared.actions = (function() {
*
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#Joining_a_Room
*/
JoinedRoom: Action.define("joinedRoom", {
apiKey: String,
sessionToken: String,
sessionId: String,
expires: Number
}),
JoinedRoom: Action.define("joinedRoom", {
apiKey: String,
sessionToken: String,
sessionId: String,
expires: Number }),
/**
* Used to indicate the user wishes to leave the conversation. This is
@@ -520,8 +520,8 @@ loop.shared.actions = (function() {
* view, or just close the window. Whereas, the leaveRoom action is for
* the action of leaving an activeRoomStore room.
*/
LeaveConversation: Action.define("leaveConversation", {
}),
LeaveConversation: Action.define("leaveConversation", {}),
/**
* Used to indicate the user wishes to leave the room.
@@ -530,35 +530,32 @@ loop.shared.actions = (function() {
// Optional, Used to indicate that we know the window is staying open,
// and hence any messages to ensure the call is fully ended must be sent.
// windowStayingOpen: Boolean,
}),
}),
/**
* Signals that the feedback view should be rendered.
*/
ShowFeedbackForm: Action.define("showFeedbackForm", {
}),
ShowFeedbackForm: Action.define("showFeedbackForm", {}),
/**
* Used to record a link click for metrics purposes.
*/
RecordClick: Action.define("recordClick", {
RecordClick: Action.define("recordClick", {
// Note: for ToS and Privacy links, this should be the link, for
// other links this should be a generic description so that we don't
// record what users are clicking, just the information about the fact
// they clicked the link in that spot (e.g. "Shared URL").
linkInfo: String
}),
linkInfo: String }),
/**
* Used to inform of the current session, publisher and connection
* status.
*/
ConnectionStatus: Action.define("connectionStatus", {
event: String,
state: String,
connections: Number,
sendStreams: Number,
recvStreams: Number
})
};
})();
ConnectionStatus: Action.define("connectionStatus", {
event: String,
state: String,
connections: Number,
sendStreams: Number,
recvStreams: Number }) };}();

View File

@@ -1,13 +1,13 @@
/* 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"; /* 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 Components */
var loop = loop || {};
var inChrome = typeof Components != "undefined" && "utils" in Components;
(function(rootObject) {
(function (rootObject) {
"use strict";
var sharedUtils;
@@ -15,13 +15,13 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
this.EXPORTED_SYMBOLS = ["LoopCrypto"];
var Cu = Components.utils;
Cu.importGlobalProperties(["crypto"]);
rootObject = {
crypto: crypto
};
sharedUtils = Cu.import("chrome://loop/content/shared/js/utils.js", {}).utils;
} else {
sharedUtils = this.shared.utils;
}
rootObject = {
crypto: crypto };
sharedUtils = Cu.import("chrome://loop/content/shared/js/utils.js", {}).utils;} else
{
sharedUtils = this.shared.utils;}
var ALGORITHM = "AES-GCM";
var KEY_LENGTH = 128;
@@ -43,8 +43,8 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
* @param {Object}
*/
function setRootObject(obj) {
rootObject = obj;
}
rootObject = obj;}
/**
* Determines if Web Crypto is supported by this browser.
@@ -52,8 +52,8 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
* @return {Boolean} True if Web Crypto is supported
*/
function isSupported() {
return "crypto" in rootObject;
}
return "crypto" in rootObject;}
/**
* Generates a random key using the Web Crypto libraries.
@@ -63,27 +63,27 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
*/
function generateKey() {
if (!isSupported()) {
throw new Error("Web Crypto is not supported");
}
throw new Error("Web Crypto is not supported");}
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
// First get a crypto key.
rootObject.crypto.subtle.generateKey({ name: ALGORITHM, length: KEY_LENGTH },
// `true` means that the key can be extracted from the CryptoKey object.
true,
// Usages for the key.
["encrypt", "decrypt"]
).then(function(cryptoKey) {
rootObject.crypto.subtle.generateKey({ name: ALGORITHM, length: KEY_LENGTH },
// `true` means that the key can be extracted from the CryptoKey object.
true,
// Usages for the key.
["encrypt", "decrypt"]).
then(function (cryptoKey) {
// Now extract the key in the JSON web key format.
return rootObject.crypto.subtle.exportKey(KEY_FORMAT, cryptoKey);
}).then(function(exportedKey) {
return rootObject.crypto.subtle.exportKey(KEY_FORMAT, cryptoKey);}).
then(function (exportedKey) {
// Lastly resolve the promise with the new key.
resolve(exportedKey.k);
}).catch(function(error) {
reject(error);
});
});
}
resolve(exportedKey.k);}).
catch(function (error) {
reject(error);});});}
/**
* Encrypts an object using the specified key.
@@ -97,21 +97,21 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
*/
function encryptBytes(key, data) {
if (!isSupported()) {
throw new Error("Web Crypto is not supported");
}
throw new Error("Web Crypto is not supported");}
var iv = new Uint8Array(INITIALIZATION_VECTOR_LENGTH);
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
// First import the key to a format we can use.
rootObject.crypto.subtle.importKey(KEY_FORMAT,
{ k: key, kty: KEY_TYPE },
ALGORITHM,
// If the key is extractable.
true,
// What we're using it for.
["encrypt"]
).then(function(cryptoKey) {
rootObject.crypto.subtle.importKey(KEY_FORMAT,
{ k: key.replace(/=/g, ""), kty: KEY_TYPE }, // eslint-disable-line no-div-regex
ALGORITHM,
// If the key is extractable.
true,
// What we're using it for.
["encrypt"]).
then(function (cryptoKey) {
// Now we've got the cryptoKey, we can do the actual encryption.
// First get the data into the format we need.
@@ -121,25 +121,25 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
// encrypted information is updated.
rootObject.crypto.getRandomValues(iv);
return rootObject.crypto.subtle.encrypt({
name: ALGORITHM,
iv: iv,
tagLength: ENCRYPT_TAG_LENGTH
}, cryptoKey,
dataBuffer);
}).then(function(cipherText) {
return rootObject.crypto.subtle.encrypt({
name: ALGORITHM,
iv: iv,
tagLength: ENCRYPT_TAG_LENGTH },
cryptoKey,
dataBuffer);}).
then(function (cipherText) {
// Join the initialization vector and context for returning.
var joinedData = _mergeIVandCipherText(iv, new DataView(cipherText));
// Now convert to a string and base-64 encode.
var encryptedData = sharedUtils.btoa(joinedData);
resolve(encryptedData);
}).catch(function(error) {
reject(error);
});
});
}
resolve(encryptedData);}).
catch(function (error) {
reject(error);});});}
/**
* Decrypts an object using the specified key.
@@ -152,35 +152,35 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
*/
function decryptBytes(key, encryptedData) {
if (!isSupported()) {
throw new Error("Web Crypto is not supported");
}
throw new Error("Web Crypto is not supported");}
return new Promise(function(resolve, reject) {
return new Promise(function (resolve, reject) {
// First import the key to a format we can use.
rootObject.crypto.subtle.importKey(KEY_FORMAT,
{ k: key, kty: KEY_TYPE },
ALGORITHM,
// If the key is extractable.
true,
// What we're using it for.
["decrypt"]
).then(function(cryptoKey) {
rootObject.crypto.subtle.importKey(KEY_FORMAT,
{ k: key.replace(/=/g, ""), kty: KEY_TYPE }, // eslint-disable-line no-div-regex
ALGORITHM,
// If the key is extractable.
true,
// What we're using it for.
["decrypt"]).
then(function (cryptoKey) {
// Now we've got the key, start the decryption.
var splitData = _splitIVandCipherText(encryptedData);
return rootObject.crypto.subtle.decrypt({
name: ALGORITHM,
iv: splitData.iv,
tagLength: ENCRYPT_TAG_LENGTH
}, cryptoKey, splitData.cipherText);
}).then(function(plainText) {
return rootObject.crypto.subtle.decrypt({
name: ALGORITHM,
iv: splitData.iv,
tagLength: ENCRYPT_TAG_LENGTH },
cryptoKey, splitData.cipherText);}).
then(function (plainText) {
// Now we just turn it back into a string and then an object.
resolve(sharedUtils.Uint8ArrayToStr(new Uint8Array(plainText)));
}).catch(function(error) {
reject(error);
});
});
}
resolve(sharedUtils.Uint8ArrayToStr(new Uint8Array(plainText)));}).
catch(function (error) {
reject(error);});});}
/**
* Appends the cipher text to the end of the initialization vector and
@@ -200,15 +200,15 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
var i;
for (i = 0; i < INITIALIZATION_VECTOR_LENGTH; i++) {
joinedContext[i] = ivArray[i];
}
joinedContext[i] = ivArray[i];}
for (i = 0; i < cipherTextLength; i++) {
joinedContext[i + INITIALIZATION_VECTOR_LENGTH] = cipherText[i];
}
joinedContext[i + INITIALIZATION_VECTOR_LENGTH] = cipherText[i];}
return joinedContext;}
return joinedContext;
}
/**
* Takes the IV from the start of the passed in array and separates
@@ -224,20 +224,20 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
// Now split out the initialization vector and the cipherText.
var iv = encryptedDataArray.slice(0, INITIALIZATION_VECTOR_LENGTH);
var cipherText = encryptedDataArray.slice(INITIALIZATION_VECTOR_LENGTH,
encryptedDataArray.length);
var cipherText = encryptedDataArray.slice(INITIALIZATION_VECTOR_LENGTH,
encryptedDataArray.length);
return {
iv: iv,
cipherText: cipherText
};
}
return {
iv: iv,
cipherText: cipherText };}
this[inChrome ? "LoopCrypto" : "crypto"] = {
decryptBytes: decryptBytes,
encryptBytes: encryptBytes,
generateKey: generateKey,
isSupported: isSupported,
setRootObject: setRootObject
};
}).call(inChrome ? this : loop, this);
this[inChrome ? "LoopCrypto" : "crypto"] = {
decryptBytes: decryptBytes,
encryptBytes: encryptBytes,
generateKey: generateKey,
isSupported: isSupported,
setRootObject: setRootObject };}).
call(inChrome ? this : loop, this);

View File

@@ -1,6 +1,6 @@
/* 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"; /* 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 dispatcher for actions. This dispatches actions to stores registered
@@ -12,34 +12,34 @@
* It is an error if a returned promise rejects - they should always pass.
*/
var loop = loop || {};
loop.Dispatcher = (function() {
loop.Dispatcher = function () {
"use strict";
function Dispatcher() {
this._eventData = {};
this._actionQueue = [];
this._debug = false;
loop.shared.utils.getBoolPreference("debug.dispatcher", function(enabled) {
this._debug = enabled;
}.bind(this));
}
loop.shared.utils.getBoolPreference("debug.dispatcher", function (enabled) {
this._debug = enabled;}.
bind(this));}
Dispatcher.prototype = {
Dispatcher.prototype = {
/**
* Register a store to receive notifications of specific actions.
*
* @param {Object} store The store object to register
* @param {Array} eventTypes An array of action names
*/
register: function(store, eventTypes) {
eventTypes.forEach(function(type) {
register: function register(store, eventTypes) {
eventTypes.forEach(function (type) {
if (this._eventData.hasOwnProperty(type)) {
this._eventData[type].push(store);
} else {
this._eventData[type] = [store];
}
}.bind(this));
},
this._eventData[type].push(store);} else
{
this._eventData[type] = [store];}}.
bind(this));},
/**
* Unregister a store from receiving notifications of specific actions.
@@ -47,38 +47,38 @@ loop.Dispatcher = (function() {
* @param {Object} store The store object to unregister
* @param {Array} eventTypes An array of action names
*/
unregister: function(store, eventTypes) {
eventTypes.forEach(function(type) {
unregister: function unregister(store, eventTypes) {
eventTypes.forEach(function (type) {
if (!this._eventData.hasOwnProperty(type)) {
return;
}
return;}
var idx = this._eventData[type].indexOf(store);
if (idx === -1) {
return;
}
return;}
this._eventData[type].splice(idx, 1);
if (!this._eventData[type].length) {
delete this._eventData[type];
}
}.bind(this));
},
delete this._eventData[type];}}.
bind(this));},
/**
* Dispatches an action to all registered stores.
*/
dispatch: function(action) {
dispatch: function dispatch(action) {
// Always put it on the queue, to make it simpler.
this._actionQueue.push(action);
this._dispatchNextAction();
},
this._dispatchNextAction();},
/**
* Dispatches the next action in the queue if one is not already active.
*/
_dispatchNextAction: function() {
_dispatchNextAction: function _dispatchNextAction() {
if (!this._actionQueue.length || this._active) {
return;
}
return;}
var action = this._actionQueue.shift();
var type = action.name;
@@ -86,27 +86,26 @@ loop.Dispatcher = (function() {
var registeredStores = this._eventData[type];
if (!registeredStores) {
console.warn("No stores registered for event type ", type);
return;
}
return;}
this._active = true;
if (this._debug) {
console.log("[Dispatcher] Dispatching action", action);
}
console.log("[Dispatcher] Dispatching action", action);}
registeredStores.forEach(function(store) {
registeredStores.forEach(function (store) {
try {
store[type](action);
} catch (x) {
console.error("[Dispatcher] Dispatching action caused an exception: ", x);
}
});
store[type](action);}
catch (x) {
console.error("[Dispatcher] Dispatching action caused an exception: ", x);}});
this._active = false;
this._dispatchNextAction();
}
};
this._dispatchNextAction();} };
return Dispatcher;
})();
return Dispatcher;}();

View File

@@ -1,6 +1,6 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
@@ -13,56 +13,55 @@ loop.shared.views.LinkifiedTextView = function () {
* links starting with http://, https://, or ftp:// as actual clickable
* links inside a <p> container.
*/
var LinkifiedTextView = React.createClass({
displayName: "LinkifiedTextView",
propTypes: {
var LinkifiedTextView = React.createClass({ displayName: "LinkifiedTextView",
propTypes: {
// Call this instead of allowing the default <a> click semantics, if
// given. Also causes sendReferrer and suppressTarget attributes to be
// ignored.
linkClickHandler: React.PropTypes.func,
linkClickHandler: React.PropTypes.func,
// The text to be linkified.
rawText: React.PropTypes.string.isRequired,
rawText: React.PropTypes.string.isRequired,
// Should the links send a referrer? Defaults to false.
sendReferrer: React.PropTypes.bool,
sendReferrer: React.PropTypes.bool,
// Should we suppress target="_blank" on the link? Defaults to false.
// Mostly for testing use.
suppressTarget: React.PropTypes.bool
},
suppressTarget: React.PropTypes.bool },
mixins: [React.addons.PureRenderMixin],
_handleClickEvent: function (e) {
mixins: [
React.addons.PureRenderMixin],
_handleClickEvent: function _handleClickEvent(e) {
e.preventDefault();
e.stopPropagation();
this.props.linkClickHandler(e.currentTarget.href);
},
this.props.linkClickHandler(e.currentTarget.href);},
_generateLinkAttributes: function _generateLinkAttributes(href) {
var linkAttributes = {
href: href };
_generateLinkAttributes: function (href) {
var linkAttributes = {
href: href
};
if (this.props.linkClickHandler) {
linkAttributes.onClick = this._handleClickEvent;
// if this is specified, we short-circuit return to avoid unnecessarily
// creating target and rel attributes.
return linkAttributes;
}
return linkAttributes;}
if (!this.props.suppressTarget) {
linkAttributes.target = "_blank";
}
linkAttributes.target = "_blank";}
if (!this.props.sendReferrer) {
linkAttributes.rel = "noreferrer";
}
linkAttributes.rel = "noreferrer";}
return linkAttributes;},
return linkAttributes;
},
/** a
* Parse the given string into an array of strings and React <a> elements
@@ -72,7 +71,7 @@ loop.shared.views.LinkifiedTextView = function () {
*
* @returns {Array} of strings and React <a> elements in order.
*/
parseStringToElements: function (s) {
parseStringToElements: function parseStringToElements(s) {
var elements = [];
var result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
var reactElementsCounter = 0; // For giving keys to each ReactElement.
@@ -82,47 +81,44 @@ loop.shared.views.LinkifiedTextView = function () {
// and update the string pointer.
if (result.index) {
elements.push(s.substr(0, result.index));
s = s.substr(result.index);
}
s = s.substr(result.index);}
// Push the first link itself, and advance the string pointer again.
// Bug 1196143 - formatURL sanitizes(decodes) the URL from IDN homographic attacks.
sanitizeURL = loop.shared.utils.formatURL(result[0]);
if (sanitizeURL && sanitizeURL.location) {
var linkAttributes = this._generateLinkAttributes(sanitizeURL.location);
elements.push(React.createElement(
"a",
{ href: linkAttributes.href,
key: reactElementsCounter++,
onClick: linkAttributes.onClick,
rel: linkAttributes.rel,
target: linkAttributes.target },
sanitizeURL.location
));
} else {
elements.push(result[0]);
}
elements.push(
React.createElement("a", { href: linkAttributes.href,
key: reactElementsCounter++,
onClick: linkAttributes.onClick,
rel: linkAttributes.rel,
target: linkAttributes.target },
sanitizeURL.location));} else
{
elements.push(result[0]);}
s = s.substr(result[0].length);
// Check for another link, and perhaps continue...
result = loop.shared.urlRegExps.fullUrlMatch.exec(s);
}
result = loop.shared.urlRegExps.fullUrlMatch.exec(s);}
if (s) {
elements.push(s);
}
elements.push(s);}
return elements;
},
render: function () {
return React.createElement(
"p",
null,
this.parseStringToElements(this.props.rawText)
);
}
});
return elements;},
return LinkifiedTextView;
}();
render: function render() {
return (
React.createElement("p", null, this.parseStringToElements(this.props.rawText)));} });
return LinkifiedTextView;}();

View File

@@ -1,9 +1,9 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
(function() {
(function () {
"use strict";
var _slice = Array.prototype.slice;
@@ -37,8 +37,8 @@ var loop = loop || {};
var seq = ++loop._lastMessageID;
args.unshift(seq, command);
return args;
}
return args;}
/**
* Send a request to chrome, running in the main process.
@@ -54,7 +54,7 @@ var loop = loop || {};
loop.request = function request() {
var args = _slice.call(arguments);
return new Promise(function(resolve) {
return new Promise(function (resolve) {
var payload = buildRequestArray(args);
var seq = payload[0];
@@ -62,27 +62,27 @@ var loop = loop || {};
gListeningForMessages = function listener(message) {
var replySeq = message.data[0];
if (!gListenersMap[replySeq]) {
return;
}
gListenersMap[replySeq](message.data[1]);
delete gListenersMap[replySeq];
};
return;}
gListenersMap[replySeq](message.data[1]);
delete gListenersMap[replySeq];};
gRootObj.addMessageListener(kMessageName, gListeningForMessages);}
gRootObj.addMessageListener(kMessageName, gListeningForMessages);
}
gListenersMap[seq] = resolve;
gRootObj.sendAsyncMessage(kMessageName, payload);
});
};
gRootObj.sendAsyncMessage(kMessageName, payload);});};
// These functions should only be used in unit tests.
loop.request.inspect = function() { return _.extend({}, gListenersMap); };
loop.request.reset = function() {
loop.request.inspect = function () {return _.extend({}, gListenersMap);};
loop.request.reset = function () {
gListeningForMessages = false;
gListenersMap = {};
};
gListenersMap = {};};
loop.storedRequests = {};
@@ -92,9 +92,9 @@ var loop = loop || {};
* @param {Array} request Set of request parameters
* @param {mixed} result Whatever the API returned as a result
*/
loop.storeRequest = function(request, result) {
loop.storedRequests[request.join("|")] = result;
};
loop.storeRequest = function (request, result) {
loop.storedRequests[request.join("|")] = result;};
/**
* Retrieve the result of a request that was stored previously. If the result
@@ -105,15 +105,15 @@ var loop = loop || {};
* @return {mixed} Whatever the result of the API request was at the time it was
* stored.
*/
loop.getStoredRequest = function(request) {
loop.getStoredRequest = function (request) {
var key = request.join("|");
if (!(key in loop.storedRequests)) {
console.error("This request has not been stored!", request);
return null;
}
return null;}
return loop.storedRequests[key];};
return loop.storedRequests[key];
};
/**
* Send multiple requests at once as a batch.
@@ -126,35 +126,35 @@ var loop = loop || {};
*/
loop.requestMulti = function requestMulti() {
if (!arguments.length) {
throw new Error("loop.requestMulti: please pass in a list of calls to process in parallel.");
}
throw new Error("loop.requestMulti: please pass in a list of calls to process in parallel.");}
var calls = _slice.call(arguments);
calls.forEach(function(call) {
calls.forEach(function (call) {
if (!Array.isArray(call)) {
throw new Error("loop.requestMulti: each call must be an array of options, " +
"exactly the same as the argument signature of `loop.request()`");
}
throw new Error("loop.requestMulti: each call must be an array of options, " +
"exactly the same as the argument signature of `loop.request()`");}
buildRequestArray(call);
});
return new Promise(function(resolve) {
loop.request(kBatchMessage, calls).then(function(resultSet) {
buildRequestArray(call);});
return new Promise(function (resolve) {
loop.request(kBatchMessage, calls).then(function (resultSet) {
if (!resultSet) {
resolve();
return;
}
return;}
// Collect result as a sequenced array and pass it back.
var result = Object.getOwnPropertyNames(resultSet).map(function(seq) {
return resultSet[seq];
});
var result = Object.getOwnPropertyNames(resultSet).map(function (seq) {
return resultSet[seq];});
resolve(result);});});};
resolve(result);
});
});
};
/**
* Subscribe to push messages coming from chrome scripts. Since these may arrive
@@ -165,33 +165,33 @@ var loop = loop || {};
*/
loop.subscribe = function subscribe(name, callback) {
if (!gListeningForPushMessages) {
gRootObj.addMessageListener(kPushMessageName, gListeningForPushMessages = function(message) {
gRootObj.addMessageListener(kPushMessageName, gListeningForPushMessages = function gListeningForPushMessages(message) {
var eventName = message.data[0];
if (!gSubscriptionsMap[eventName]) {
return;
}
gSubscriptionsMap[eventName].forEach(function(cb) {
return;}
gSubscriptionsMap[eventName].forEach(function (cb) {
var data = message.data[1];
if (!Array.isArray(data)) {
data = [data];
}
cb.apply(null, data);
});
});
}
data = [data];}
cb.apply(null, data);});});}
if (!gSubscriptionsMap[name]) {
gSubscriptionsMap[name] = [];
}
gSubscriptionsMap[name].push(callback);
};
gSubscriptionsMap[name] = [];}
gSubscriptionsMap[name].push(callback);};
// These functions should only be used in unit tests.
loop.subscribe.inspect = function() { return _.extend({}, gSubscriptionsMap); };
loop.subscribe.reset = function() {
loop.subscribe.inspect = function () {return _.extend({}, gSubscriptionsMap);};
loop.subscribe.reset = function () {
gListeningForPushMessages = false;
gSubscriptionsMap = {};
};
gSubscriptionsMap = {};};
/**
* Cancel a subscription to a specific push message.
@@ -201,14 +201,14 @@ var loop = loop || {};
*/
loop.unsubscribe = function unsubscribe(name, callback) {
if (!gSubscriptionsMap[name]) {
return;
}
return;}
var idx = gSubscriptionsMap[name].indexOf(callback);
if (idx === -1) {
return;
}
gSubscriptionsMap[name].splice(idx, 1);
};
return;}
gSubscriptionsMap[name].splice(idx, 1);};
/**
* Cancel all running subscriptions.
@@ -217,7 +217,4 @@ var loop = loop || {};
gSubscriptionsMap = {};
if (gListeningForPushMessages) {
gRootObj.removeMessageListener(kPushMessageName, gListeningForPushMessages);
gListeningForPushMessages = false;
}
};
})();
gListeningForPushMessages = false;}};})();

View File

@@ -1,6 +1,6 @@
/* 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"; /* 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 mixin should be deprecated and a new solution implemented for
// processing menus and taking care of menu positioning globally. This
// new implementation should ensure all menus are positioned using the
@@ -8,7 +8,7 @@
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.mixins = (function() {
loop.shared.mixins = function () {
"use strict";
/**
@@ -26,8 +26,8 @@ loop.shared.mixins = (function() {
* @param {Object}
*/
function setRootObject(obj) {
rootObject = obj;
}
rootObject = obj;}
/**
* window.location mixin. Handles changes in the call url.
@@ -35,37 +35,37 @@ loop.shared.mixins = (function() {
*
* @type {Object}
*/
var UrlHashChangeMixin = {
componentDidMount: function() {
rootObject.addEventListener("hashchange", this.onUrlHashChange, false);
},
var UrlHashChangeMixin = {
componentDidMount: function componentDidMount() {
rootObject.addEventListener("hashchange", this.onUrlHashChange, false);},
componentWillUnmount: function componentWillUnmount() {
rootObject.removeEventListener("hashchange", this.onUrlHashChange, false);} };
componentWillUnmount: function() {
rootObject.removeEventListener("hashchange", this.onUrlHashChange, false);
}
};
/**
* Document location mixin.
*
* @type {Object}
*/
var DocumentLocationMixin = {
locationReload: function() {
rootObject.location.reload();
}
};
var DocumentLocationMixin = {
locationReload: function locationReload() {
rootObject.location.reload();} };
/**
* Document title mixin.
*
* @type {Object}
*/
var DocumentTitleMixin = {
setTitle: function(newTitle) {
rootObject.document.title = newTitle;
}
};
var DocumentTitleMixin = {
setTitle: function setTitle(newTitle) {
rootObject.document.title = newTitle;} };
/**
* Window close mixin, for more testable closing of windows. Instead of
@@ -76,11 +76,11 @@ loop.shared.mixins = (function() {
*
* @see setRootObject for info on how to unit test code that uses this mixin
*/
var WindowCloseMixin = {
closeWindow: function() {
rootObject.close();
}
};
var WindowCloseMixin = {
closeWindow: function closeWindow() {
rootObject.close();} };
/**
* Dropdown menu mixin.
@@ -92,28 +92,28 @@ loop.shared.mixins = (function() {
* constraining element.
* @type {Object}
*/
var DropdownMenuMixin = function(boundingBoxSelector) {
return {
var DropdownMenuMixin = function DropdownMenuMixin(boundingBoxSelector) {
return {
get documentBody() {
return rootObject.document.body;
},
return rootObject.document.body;},
getInitialState: function getInitialState() {
return {
showMenu: false };},
getInitialState: function() {
return {
showMenu: false
};
},
/*
* Event listener callback in charge of closing panels when the users
* clicks on something that is not a dropdown trigger button or menu item.
*/
_onBodyClick: function(event) {
var menuButton = this.refs["menu-button"] && this.refs["menu-button"].getDOMNode();
_onBodyClick: function _onBodyClick(event) {
var menuButton = this.refs["menu-button"];
if (this.refs.anchor) {
menuButton = this.refs.anchor.getDOMNode();
}
menuButton = this.refs.anchor;}
/*
* XXX Because the mixin is inherited by multiple components there are
@@ -123,27 +123,27 @@ loop.shared.mixins = (function() {
* should be refactored to only be attached once to the document and use
* classList instead of refs.
*/
if (event.target.classList.contains("dropdown-menu-item") ||
event.target.classList.contains("dropdown-menu-button")) {
return;
}
if (event.target.classList.contains("dropdown-menu-item") ||
event.target.classList.contains("dropdown-menu-button")) {
return;}
if (event.target !== menuButton) {
this.setState({ showMenu: false });
}
},
this.setState({ showMenu: false });}},
_correctMenuPosition: function() {
var menu = this.refs.menu && this.refs.menu.getDOMNode();
_correctMenuPosition: function _correctMenuPosition() {
var menu = this.refs.menu;
if (!menu) {
return;
}
return;}
if (menu.style.maxWidth) {
menu.style.maxWidth = "none";
}
menu.style.maxWidth = "none";}
if (menu.style.maxHeight) {
menu.style.maxHeight = "none";
}
menu.style.maxHeight = "none";}
// Correct the position of the menu only if necessary.
var x, y, boundingBox, boundingRect;
@@ -156,17 +156,17 @@ loop.shared.mixins = (function() {
if (boundingBoxSelector) {
boundingBox = this.documentBody.querySelector(boundingBoxSelector);
if (boundingBox) {
boundingRect = boundingBox.getBoundingClientRect();
}
}
boundingRect = boundingBox.getBoundingClientRect();}}
if (!boundingRect) {
boundingRect = {
height: this.documentBody.offsetHeight,
left: 0,
top: 0,
width: this.documentBody.offsetWidth
};
}
boundingRect = {
height: this.documentBody.offsetHeight,
left: 0,
top: 0,
width: this.documentBody.offsetWidth };}
// Make sure the menu position will be a certain fixed amount of pixels away
// from the border of the bounding box.
boundingRect.width -= boundOffset;
@@ -176,16 +176,16 @@ loop.shared.mixins = (function() {
y = menuNodeRect.top;
// If there's an anchor present, position it relative to it first.
var anchor = this.refs.anchor && this.refs.anchor.getDOMNode();
var anchor = this.refs.anchor && ReactDOM.findDOMNode(this.refs.anchor);
if (anchor) {
// XXXmikedeboer: at the moment we only support positioning centered above
// anchor node. Please add more modes as necessary.
var anchorNodeRect = anchor.getBoundingClientRect();
// Because we're _correcting_ the position of the dropdown, we assume that
// the node is positioned absolute at 0,0 coordinates (top left).
x = Math.floor(anchorNodeRect.left - (menuNodeRect.width / 2) + (anchorNodeRect.width / 2));
y = Math.floor(anchorNodeRect.top - menuNodeRect.height - anchorNodeRect.height);
}
x = Math.floor(anchorNodeRect.left - menuNodeRect.width / 2 + anchorNodeRect.width / 2);
y = Math.floor(anchorNodeRect.top - menuNodeRect.height - anchorNodeRect.height);}
var overflowX = false;
var overflowY = false;
@@ -193,14 +193,14 @@ loop.shared.mixins = (function() {
if (x + menuNodeRect.width > boundingRect.width) {
// Anchor positioning is already relative, so don't subtract it again.
x = Math.floor(boundingRect.width - ((anchor ? 0 : x) + menuNodeRect.width));
overflowX = true;
}
overflowX = true;}
// Check the vertical overflow.
if (y + menuNodeRect.height > boundingRect.height) {
// Anchor positioning is already relative, so don't subtract it again.
y = Math.floor(boundingRect.height - ((anchor ? 0 : y) + menuNodeRect.height));
overflowY = true;
}
overflowY = true;}
if (anchor || overflowX) {
// Set the maximum dimensions that the menu DOMNode may grow to. The
@@ -209,65 +209,65 @@ loop.shared.mixins = (function() {
// doesn't really do much for now.
if (menuNodeRect.width > boundingRect.width) {
menu.classList.add("overflow");
menu.style.maxWidth = boundingRect.width + "px";
}
menu.style.marginLeft = x + "px";
} else if (!menu.style.marginLeft) {
menu.style.marginLeft = "auto";
}
menu.style.maxWidth = boundingRect.width + "px";}
menu.style.marginLeft = x + "px";} else
if (!menu.style.marginLeft) {
menu.style.marginLeft = "auto";}
if (anchor || overflowY) {
if (menuNodeRect.height > (boundingRect.height + y)) {
if (menuNodeRect.height > boundingRect.height + y) {
menu.classList.add("overflow");
// Set the maximum dimensions that the menu DOMNode may grow to. The
// content overflow style should be defined in CSS.
menu.style.maxHeight = (boundingRect.height + y) + "px";
menu.style.maxHeight = boundingRect.height + y + "px";
// Since we just adjusted the max-height of the menu - thus its actual
// height as well - we need to adjust its vertical offset with the same
// amount.
y += menuNodeRect.height - (boundingRect.height + y);
}
menu.style.marginTop = y + "px";
} else if (!menu.style.marginLeft) {
menu.style.marginTop = "auto";
}
y += menuNodeRect.height - (boundingRect.height + y);}
menu.style.marginTop = y + "px";} else
if (!menu.style.marginLeft) {
menu.style.marginTop = "auto";}
// Added call to _repositionMenu() if it exists, to allow a component to
// add specific repositioning to a menu.
if (this._repositionMenu) {
this._repositionMenu();
}
menu.style.visibility = "visible";
},
this._repositionMenu();}
componentDidMount: function() {
menu.style.visibility = "visible";},
componentDidMount: function componentDidMount() {
this.documentBody.addEventListener("click", this._onBodyClick);
rootObject.addEventListener("blur", this.hideDropdownMenu);
},
rootObject.addEventListener("blur", this.hideDropdownMenu);},
componentWillUnmount: function() {
componentWillUnmount: function componentWillUnmount() {
this.documentBody.removeEventListener("click", this._onBodyClick);
rootObject.removeEventListener("blur", this.hideDropdownMenu);
},
rootObject.removeEventListener("blur", this.hideDropdownMenu);},
showDropdownMenu: function() {
this.setState({ showMenu: true }, this._correctMenuPosition);
},
hideDropdownMenu: function() {
this.setState({ showMenu: false }, function() {
var menu = this.refs.menu && this.refs.menu.getDOMNode();
showDropdownMenu: function showDropdownMenu() {
this.setState({ showMenu: true }, this._correctMenuPosition);},
hideDropdownMenu: function hideDropdownMenu() {
this.setState({ showMenu: false }, function () {
var menu = this.refs.menu && ReactDOM.findDOMNode(this.refs.menu);
if (menu) {
menu.style.visibility = "hidden";
}
});
},
menu.style.visibility = "hidden";}});},
toggleDropdownMenu: function toggleDropdownMenu() {
this[this.state.showMenu ? "hideDropdownMenu" : "showDropdownMenu"]();} };};
toggleDropdownMenu: function() {
this[this.state.showMenu ? "hideDropdownMenu" : "showDropdownMenu"]();
}
};
};
/**
* Document visibility mixin. Allows defining the following hooks for when the
@@ -278,78 +278,78 @@ loop.shared.mixins = (function() {
*
* @type {Object}
*/
var DocumentVisibilityMixin = {
_onDocumentVisibilityChanged: function(event) {
var DocumentVisibilityMixin = {
_onDocumentVisibilityChanged: function _onDocumentVisibilityChanged(event) {
if (!this.isMounted()) {
return;
}
return;}
var hidden = event.target.hidden;
if (hidden && typeof this.onDocumentHidden === "function") {
this.onDocumentHidden();
}
if (!hidden && typeof this.onDocumentVisible === "function") {
this.onDocumentVisible();
}
},
this.onDocumentHidden();}
componentDidMount: function() {
if (!hidden && typeof this.onDocumentVisible === "function") {
this.onDocumentVisible();}},
componentDidMount: function componentDidMount() {
rootObject.document.addEventListener(
"visibilitychange", this._onDocumentVisibilityChanged);
"visibilitychange", this._onDocumentVisibilityChanged);
// Assume that the consumer components is only mounted when the document
// has become visible.
this._onDocumentVisibilityChanged({ target: rootObject.document });
},
this._onDocumentVisibilityChanged({ target: rootObject.document });},
componentWillUnmount: function() {
componentWillUnmount: function componentWillUnmount() {
rootObject.document.removeEventListener(
"visibilitychange", this._onDocumentVisibilityChanged);
}
};
"visibilitychange", this._onDocumentVisibilityChanged);} };
/**
* Media setup mixin. Provides a common location for settings for the media
* elements.
*/
var MediaSetupMixin = {
var MediaSetupMixin = {
/**
* Returns the default configuration for publishing media on the sdk.
*
* @param {Object} options An options object containing:
* - publishVideo A boolean set to true to publish video when the stream is initiated.
*/
getDefaultPublisherConfig: function(options) {
getDefaultPublisherConfig: function getDefaultPublisherConfig(options) {
options = options || {};
if (!("publishVideo" in options)) {
throw new Error("missing option publishVideo");
}
throw new Error("missing option publishVideo");}
// height set to 100%" to fix video layout on Google Chrome
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
return {
insertMode: "append",
fitMode: "contain",
width: "100%",
height: "100%",
publishVideo: options.publishVideo,
showControls: false
};
}
};
return {
insertMode: "append",
fitMode: "contain",
width: "100%",
height: "100%",
publishVideo: options.publishVideo,
showControls: false };} };
/**
* Audio mixin. Allows playing a single audio file and ensuring it
* is stopped when the component is unmounted.
*/
var AudioMixin = {
audio: null,
_audioRequest: null,
var AudioMixin = {
audio: null,
_audioRequest: null,
_isLoopDesktop: function _isLoopDesktop() {
var isUIShowcase = !!(rootObject.document.querySelector &&
rootObject.document.querySelector("#main > .showcase"));
return loop.shared.utils.isDesktop() || isUIShowcase;},
_isLoopDesktop: function() {
var isUIShowcase = !!(rootObject.document.querySelector &&
rootObject.document.querySelector("#main > .showcase"));
return loop.shared.utils.isDesktop() || isUIShowcase;
},
/**
* Internal function that determines if we can play an audio fragment at this
@@ -358,18 +358,18 @@ loop.shared.mixins = (function() {
*
* @return {Promise}
*/
_canPlay: function() {
return new Promise(function(resolve) {
_canPlay: function _canPlay() {
return new Promise(function (resolve) {
if (!this._isLoopDesktop()) {
resolve(true);
return;
}
return;}
loop.request("GetDoNotDisturb").then(function (mayNotDisturb) {
resolve(!mayNotDisturb);});}.
bind(this));},
loop.request("GetDoNotDisturb").then(function(mayNotDisturb) {
resolve(!mayNotDisturb);
});
}.bind(this));
},
/**
* Starts playing an audio file, stopping any audio that is already in progress.
@@ -378,164 +378,162 @@ loop.shared.mixins = (function() {
* @param {Object} options A list of options for the sound:
* - {Boolean} loop Whether or not to loop the sound.
*/
play: function(name, options) {
this._canPlay().then(function(canPlay) {
play: function play(name, options) {
this._canPlay().then(function (canPlay) {
if (!canPlay) {
return;
}
return;}
options = options || {};
options.loop = options.loop || false;
this._ensureAudioStopped();
this._getAudioBlob(name, function(error, blob) {
this._getAudioBlob(name, function (error, blob) {
if (error) {
console.error(error);
return;
}
return;}
if (!blob) {
return;
}
return;}
var url = URL.createObjectURL(blob);
this.audio = new Audio(url);
this.audio.loop = options.loop;
this.audio.play();
}.bind(this));
}.bind(this));
},
this.audio.play();}.
bind(this));}.
bind(this));},
_getAudioBlob: function(name, callback) {
this._canPlay().then(function(canPlay) {
_getAudioBlob: function _getAudioBlob(name, callback) {
this._canPlay().then(function (canPlay) {
if (!canPlay) {
callback();
return;
}
return;}
if (this._isLoopDesktop()) {
loop.request("GetAudioBlob", name).then(function(result) {
loop.request("GetAudioBlob", name).then(function (result) {
if (result && result.isError) {
callback(result);
return;
}
callback(null, result);
});
return;
}
return;}
callback(null, result);});
return;}
var url = "shared/sounds/" + name + ".ogg";
this._audioRequest = new XMLHttpRequest();
this._audioRequest.open("GET", url, true);
this._audioRequest.responseType = "arraybuffer";
this._audioRequest.onload = function() {
this._audioRequest.onload = function () {
var request = this._audioRequest;
var error;
if (request.status < 200 || request.status >= 300) {
error = new Error(request.status + " " + request.statusText);
callback(error);
return;
}
return;}
var type = request.getResponseHeader("Content-Type");
var blob = new Blob([request.response], { type: type });
callback(null, blob);
}.bind(this);
callback(null, blob);}.
bind(this);
this._audioRequest.send(null);}.
bind(this));},
this._audioRequest.send(null);
}.bind(this));
},
/**
* Ensures audio is stopped playing, and removes the object from memory.
*/
_ensureAudioStopped: function() {
_ensureAudioStopped: function _ensureAudioStopped() {
if (this._audioRequest) {
this._audioRequest.abort();
delete this._audioRequest;
}
delete this._audioRequest;}
if (this.audio) {
this.audio.pause();
this.audio.removeAttribute("src");
delete this.audio;
}
},
delete this.audio;}},
/**
* Ensures audio is stopped when the component is unmounted.
*/
componentWillUnmount: function() {
this._ensureAudioStopped();
}
};
componentWillUnmount: function componentWillUnmount() {
this._ensureAudioStopped();} };
/**
* A mixin especially for rooms. This plays the right sound according to
* the state changes. Requires AudioMixin to also be used.
*/
var RoomsAudioMixin = {
mixins: [AudioMixin],
var RoomsAudioMixin = {
mixins: [AudioMixin],
componentWillUpdate: function(nextProps, nextState) {
componentWillUpdate: function componentWillUpdate(nextProps, nextState) {
var ROOM_STATES = loop.store.ROOM_STATES;
function isConnectedToRoom(state) {
return state === ROOM_STATES.HAS_PARTICIPANTS ||
state === ROOM_STATES.SESSION_CONNECTED;
}
return state === ROOM_STATES.HAS_PARTICIPANTS ||
state === ROOM_STATES.SESSION_CONNECTED;}
function notConnectedToRoom(state) {
// Failed and full are states that the user is not
// really connected to the room, but we don't want to
// catch those here, as they get their own sounds.
return state === ROOM_STATES.INIT ||
state === ROOM_STATES.GATHER ||
state === ROOM_STATES.READY ||
state === ROOM_STATES.JOINED ||
state === ROOM_STATES.ENDED;
}
return state === ROOM_STATES.INIT ||
state === ROOM_STATES.GATHER ||
state === ROOM_STATES.READY ||
state === ROOM_STATES.JOINED ||
state === ROOM_STATES.ENDED;}
// Joining the room.
if (notConnectedToRoom(this.state.roomState) &&
isConnectedToRoom(nextState.roomState)) {
this.play("room-joined");
}
if (notConnectedToRoom(this.state.roomState) &&
isConnectedToRoom(nextState.roomState)) {
this.play("room-joined");}
// Other people coming and leaving.
if (this.state.roomState === ROOM_STATES.SESSION_CONNECTED &&
nextState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
this.play("room-joined-in");
}
if (this.state.roomState === ROOM_STATES.SESSION_CONNECTED &&
nextState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
this.play("room-joined-in");}
if (this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
nextState.roomState === ROOM_STATES.SESSION_CONNECTED) {
this.play("room-left");}
if (this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
nextState.roomState === ROOM_STATES.SESSION_CONNECTED) {
this.play("room-left");
}
// Leaving the room - same sound as if a participant leaves
if (isConnectedToRoom(this.state.roomState) &&
notConnectedToRoom(nextState.roomState)) {
this.play("room-left");
}
if (isConnectedToRoom(this.state.roomState) &&
notConnectedToRoom(nextState.roomState)) {
this.play("room-left");}
// Room failures
if (nextState.roomState === ROOM_STATES.FAILED ||
nextState.roomState === ROOM_STATES.FULL) {
this.play("failure");
}
}
};
if (nextState.roomState === ROOM_STATES.FAILED ||
nextState.roomState === ROOM_STATES.FULL) {
this.play("failure");}} };
return {
AudioMixin: AudioMixin,
RoomsAudioMixin: RoomsAudioMixin,
setRootObject: setRootObject,
DropdownMenuMixin: DropdownMenuMixin,
DocumentVisibilityMixin: DocumentVisibilityMixin,
DocumentLocationMixin: DocumentLocationMixin,
DocumentTitleMixin: DocumentTitleMixin,
MediaSetupMixin: MediaSetupMixin,
UrlHashChangeMixin: UrlHashChangeMixin,
WindowCloseMixin: WindowCloseMixin
};
})();
return {
AudioMixin: AudioMixin,
RoomsAudioMixin: RoomsAudioMixin,
setRootObject: setRootObject,
DropdownMenuMixin: DropdownMenuMixin,
DocumentVisibilityMixin: DocumentVisibilityMixin,
DocumentLocationMixin: DocumentLocationMixin,
DocumentTitleMixin: DocumentTitleMixin,
MediaSetupMixin: MediaSetupMixin,
UrlHashChangeMixin: UrlHashChangeMixin,
WindowCloseMixin: WindowCloseMixin };}();

View File

@@ -1,34 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.models = (function() {
"use strict";
/**
* Notification model.
*/
var NotificationModel = Backbone.Model.extend({
defaults: {
details: "",
detailsButtonLabel: "",
detailsButtonCallback: null,
level: "info",
message: ""
}
});
/**
* Notification collection
*/
var NotificationCollection = Backbone.Collection.extend({
model: NotificationModel
});
return {
NotificationCollection: NotificationCollection,
NotificationModel: NotificationModel
};
})();

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.store = loop.store || {};
@@ -8,7 +8,7 @@ loop.store = loop.store || {};
/**
* Manages the different cursors' events being exchanged between the parts.
*/
loop.store.RemoteCursorStore = (function() {
loop.store.RemoteCursorStore = function () {
"use strict";
var CURSOR_MESSAGE_TYPES = loop.shared.utils.CURSOR_MESSAGE_TYPES;
@@ -16,13 +16,13 @@ loop.store.RemoteCursorStore = (function() {
/**
* A store to handle remote cursors events.
*/
var RemoteCursorStore = loop.store.createStore({
var RemoteCursorStore = loop.store.createStore({
actions: [
"sendCursorData",
"receivedCursorData",
"videoDimensionsChanged",
"videoScreenStreamChanged"
],
"sendCursorData",
"receivedCursorData",
"videoDimensionsChanged",
"videoScreenStreamChanged"],
/**
* Initializes the store.
@@ -32,39 +32,39 @@ loop.store.RemoteCursorStore = (function() {
* - sdkDriver: The sdkDriver to use for sending
* the cursor events.
*/
initialize: function(options) {
initialize: function initialize(options) {
options = options || {};
if (!options.sdkDriver) {
throw new Error("Missing option sdkDriver");
}
throw new Error("Missing option sdkDriver");}
this._sdkDriver = options.sdkDriver;
loop.subscribe("CursorPositionChange",
this._cursorPositionChangeListener.bind(this));
loop.subscribe("CursorClick", this._cursorClickListener.bind(this));
},
loop.subscribe("CursorPositionChange",
this._cursorPositionChangeListener.bind(this));
loop.subscribe("CursorClick", this._cursorClickListener.bind(this));},
/**
* Returns initial state data for this active room.
*/
getInitialStoreState: function() {
return {
realVideoSize: null,
remoteCursorClick: null,
remoteCursorPosition: null
};
},
getInitialStoreState: function getInitialStoreState() {
return {
realVideoSize: null,
remoteCursorClick: null,
remoteCursorPosition: null };},
/**
* Sends cursor click position through the sdk.
*/
_cursorClickListener: function() {
this.sendCursorData({
type: CURSOR_MESSAGE_TYPES.CLICK
});
},
_cursorClickListener: function _cursorClickListener() {
this.sendCursorData({
type: CURSOR_MESSAGE_TYPES.CLICK });},
/**
* Prepares the cursor position object to be sent.
@@ -74,13 +74,13 @@ loop.store.RemoteCursorStore = (function() {
* - ratioX: Left position. Number between 0 and 1.
* - ratioY: Top position. Number between 0 and 1.
*/
_cursorPositionChangeListener: function(event) {
this.sendCursorData({
ratioX: event.ratioX,
ratioY: event.ratioY,
type: CURSOR_MESSAGE_TYPES.POSITION
});
},
_cursorPositionChangeListener: function _cursorPositionChangeListener(event) {
this.sendCursorData({
ratioX: event.ratioX,
ratioY: event.ratioY,
type: CURSOR_MESSAGE_TYPES.POSITION });},
/**
* Sends the cursor data to the SDK for broadcasting.
@@ -95,73 +95,72 @@ loop.store.RemoteCursorStore = (function() {
* | CURSOR_MESSAGE_TYPES.CLICK
* }
*/
sendCursorData: function(actionData) {
sendCursorData: function sendCursorData(actionData) {
switch (actionData.type) {
case CURSOR_MESSAGE_TYPES.POSITION:
case CURSOR_MESSAGE_TYPES.CLICK:
this._sdkDriver.sendCursorMessage(actionData);
break;
}
},
break;}},
/**
* Receives cursor data and updates the store.
*
* @param {sharedActions.receivedCursorData} actionData
*/
receivedCursorData: function(actionData) {
receivedCursorData: function receivedCursorData(actionData) {
switch (actionData.type) {
case CURSOR_MESSAGE_TYPES.POSITION:
this.setStoreState({
remoteCursorPosition: {
ratioX: actionData.ratioX,
ratioY: actionData.ratioY
}
});
this.setStoreState({
remoteCursorPosition: {
ratioX: actionData.ratioX,
ratioY: actionData.ratioY } });
break;
case CURSOR_MESSAGE_TYPES.CLICK:
this.setStoreState({
remoteCursorClick: true
});
break;
}
},
this.setStoreState({
remoteCursorClick: true });
break;}},
/**
* Listen to stream dimension changes.
*
* @param {sharedActions.VideoDimensionsChanged} actionData
*/
videoDimensionsChanged: function(actionData) {
videoDimensionsChanged: function videoDimensionsChanged(actionData) {
if (actionData.videoType !== "screen") {
return;
}
return;}
this.setStoreState({
realVideoSize: {
height: actionData.dimensions.height,
width: actionData.dimensions.width } });},
this.setStoreState({
realVideoSize: {
height: actionData.dimensions.height,
width: actionData.dimensions.width
}
});
},
/**
* Listen to screen stream changes. Because the cursor's position is likely
* to be different respect to the new screen size, it's better to delete the
* previous position and keep waiting for the next one.
* @param {sharedActions.VideoScreenStreamChanged} actionData
* @param {sharedActions.VideoScreenStreamChanged} actionData
*/
videoScreenStreamChanged: function(actionData) {
videoScreenStreamChanged: function videoScreenStreamChanged(actionData) {
if (actionData.hasVideo) {
return;
}
return;}
this.setStoreState({
remoteCursorPosition: null
});
}
});
return RemoteCursorStore;
})();
this.setStoreState({
remoteCursorPosition: null });} });
return RemoteCursorStore;}();

View File

@@ -1,33 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.store = loop.store || {};
loop.store.createStore = (function() {
loop.store.createStore = function () {
"use strict";
var baseStorePrototype = {
__registerActions: function(actions) {
var baseStorePrototype = {
__registerActions: function __registerActions(actions) {
// check that store methods are implemented
actions.forEach(function(handler) {
actions.forEach(function (handler) {
if (typeof this[handler] !== "function") {
throw new Error("Store should implement an action handler for " +
handler);
}
}, this);
this.dispatcher.register(this, actions);
},
throw new Error("Store should implement an action handler for " +
handler);}},
this);
this.dispatcher.register(this, actions);},
/**
* Proxy helper for dispatching an action from this store.
*
* @param {sharedAction.Action} action The action to dispatch.
*/
dispatchAction: function(action) {
this.dispatcher.dispatch(action);
},
dispatchAction: function dispatchAction(action) {
this.dispatcher.dispatch(action);},
/**
* Returns current store state. You can request a given state property by
@@ -36,9 +36,9 @@ loop.store.createStore = (function() {
* @param {String|undefined} key An optional state property name.
* @return {Mixed}
*/
getStoreState: function(key) {
return key ? this._storeState[key] : this._storeState;
},
getStoreState: function getStoreState(key) {
return key ? this._storeState[key] : this._storeState;},
/**
* Updates store state and trigger a global "change" event, plus one for
@@ -46,25 +46,25 @@ loop.store.createStore = (function() {
*
* @param {Object} newState The new store state object.
*/
setStoreState: function(newState) {
for (var key in newState) {
setStoreState: function setStoreState(newState) {
Object.keys(newState).forEach(function (key) {
this._storeState[key] = newState[key];
this.trigger("change:" + key);
}
this.trigger("change");
},
this.trigger("change:" + key);}.
bind(this));
this.trigger("change");},
/**
* Resets the store state to the initially defined state.
*/
resetStoreState: function() {
resetStoreState: function resetStoreState() {
if (typeof this.getInitialStoreState === "function") {
this._storeState = this.getInitialStoreState();
} else {
this._storeState = {};
}
}
};
this._storeState = this.getInitialStoreState();} else
{
this._storeState = {};}} };
/**
* Creates a new Store constructor.
@@ -73,36 +73,36 @@ loop.store.createStore = (function() {
* @return {Function} A store constructor.
*/
function createStore(storeProto) {
var BaseStore = function(dispatcher, options) {
var BaseStore = function BaseStore(dispatcher, options) {
options = options || {};
if (!dispatcher) {
throw new Error("Missing required dispatcher");
}
throw new Error("Missing required dispatcher");}
this.dispatcher = dispatcher;
if (Array.isArray(this.actions)) {
this.__registerActions(this.actions);
}
this.__registerActions(this.actions);}
if (typeof this.initialize === "function") {
this.initialize(options);
}
this.initialize(options);}
if (typeof this.getInitialStoreState === "function") {
this._storeState = this.getInitialStoreState();
} else {
this._storeState = {};
}
};
BaseStore.prototype = _.extend({}, // destination object
Backbone.Events,
baseStorePrototype,
storeProto);
return BaseStore;
}
this._storeState = this.getInitialStoreState();} else
{
this._storeState = {};}};
BaseStore.prototype = _.extend({}, // destination object
Backbone.Events,
baseStorePrototype,
storeProto);
return BaseStore;}
return createStore;}();
return createStore;
})();
/**
* Store mixin generator. Usage:
@@ -112,43 +112,42 @@ loop.store.createStore = (function() {
* mixins: [StoreMixin("roomStore")]
* });
*/
loop.store.StoreMixin = (function() {
loop.store.StoreMixin = function () {
"use strict";
var _stores = {};
function StoreMixin(id) {
return {
getStore: function() {
return {
getStore: function getStore() {
// Allows the ui-showcase to override the specified store.
if (id in this.props) {
return this.props[id];
}
return this.props[id];}
if (!_stores[id]) {
throw new Error("Unavailable store " + id);
}
return _stores[id];
},
getStoreState: function() {
return this.getStore().getStoreState();
},
componentWillMount: function() {
this.getStore().on("change", function() {
this.setState(this.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.getStore().off("change", null, this);
}
};
}
StoreMixin.register = function(stores) {
_.extend(_stores, stores);
};
throw new Error("Unavailable store " + id);}
return _stores[id];},
getStoreState: function getStoreState() {
return this.getStore().getStoreState();},
componentWillMount: function componentWillMount() {
this.getStore().on("change", function () {
this.setState(this.getStoreState());},
this);},
componentWillUnmount: function componentWillUnmount() {
this.getStore().off("change", null, this);} };}
StoreMixin.register = function (stores) {
_.extend(_stores, stores);};
/**
* Used for test purposes, to clear the list of registered stores.
*/
StoreMixin.clearRegisteredStores = function() {
_stores = {};
};
return StoreMixin;
})();
StoreMixin.clearRegisteredStores = function () {
_stores = {};};
return StoreMixin;}();

View File

@@ -1,18 +1,18 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.store = loop.store || {};
loop.store.TextChatStore = (function() {
loop.store.TextChatStore = function () {
"use strict";
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES = {
RECEIVED: "recv",
SENT: "sent",
SPECIAL: "special"
};
var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES = {
RECEIVED: "recv",
SENT: "sent",
SPECIAL: "special" };
var CHAT_CONTENT_TYPES = loop.shared.utils.CHAT_CONTENT_TYPES;
@@ -20,15 +20,16 @@ loop.store.TextChatStore = (function() {
* A store to handle text chats. The store has a message list that may
* contain different types of messages and data.
*/
var TextChatStore = loop.store.createStore({
var TextChatStore = loop.store.createStore({
actions: [
"dataChannelsAvailable",
"receivedTextChatMessage",
"sendTextChatMessage",
"updateRoomInfo",
"updateRoomContext",
"remotePeerDisconnected"
],
"dataChannelsAvailable",
"receivedTextChatMessage",
"sendTextChatMessage",
"updateRoomInfo",
"updateRoomContext",
"remotePeerDisconnected",
"remotePeerConnected"],
/**
* Initializes the store.
@@ -38,29 +39,30 @@ loop.store.TextChatStore = (function() {
* - sdkDriver: The sdkDriver to use for sending
* messages.
*/
initialize: function(options) {
initialize: function initialize(options) {
options = options || {};
if (!options.sdkDriver) {
throw new Error("Missing option sdkDriver");
}
throw new Error("Missing option sdkDriver");}
this._sdkDriver = options.sdkDriver;},
this._sdkDriver = options.sdkDriver;
},
/**
* Returns initial state data for this active room.
*/
getInitialStoreState: function() {
return {
textChatEnabled: false,
getInitialStoreState: function getInitialStoreState() {
return {
textChatEnabled: false,
// The messages currently received. Care should be taken when updating
// this - do not update the in-store array directly, but use a clone or
// separate array and then use setStoreState().
messageList: [],
length: 0
};
},
messageList: [],
roomName: null,
length: 0 };},
/**
* Handles information for when data channels are available - enables
@@ -68,13 +70,13 @@ loop.store.TextChatStore = (function() {
*
* @param {sharedActions.DataChannelsAvailable} actionData
*/
dataChannelsAvailable: function(actionData) {
dataChannelsAvailable: function dataChannelsAvailable(actionData) {
this.setStoreState({ textChatEnabled: actionData.available });
if (actionData.available) {
window.dispatchEvent(new CustomEvent("LoopChatEnabled"));
}
},
window.dispatchEvent(new CustomEvent("LoopChatEnabled"));}},
/**
* Appends a message to the store, which may be of type 'sent' or 'received'.
@@ -85,17 +87,17 @@ loop.store.TextChatStore = (function() {
* - {String} message The message detail.
* - {Object} extraData Extra data associated with the message.
*/
_appendTextChatMessage: function(type, messageData) {
_appendTextChatMessage: function _appendTextChatMessage(type, messageData) {
// We create a new list to avoid updating the store's state directly,
// which confuses the views.
var message = {
type: type,
contentType: messageData.contentType,
message: messageData.message,
extraData: messageData.extraData,
sentTimestamp: messageData.sentTimestamp,
receivedTimestamp: messageData.receivedTimestamp
};
var message = {
type: type,
contentType: messageData.contentType,
message: messageData.message,
extraData: messageData.extraData,
sentTimestamp: messageData.sentTimestamp,
receivedTimestamp: messageData.receivedTimestamp };
var newList = [].concat(this._storeState.messageList);
var isContext = message.contentType === CHAT_CONTENT_TYPES.CONTEXT;
if (isContext) {
@@ -105,125 +107,121 @@ loop.store.TextChatStore = (function() {
if (newList[i].contentType === CHAT_CONTENT_TYPES.CONTEXT) {
newList[i] = message;
contextUpdated = true;
break;
}
}
break;}}
if (!contextUpdated) {
newList.push(message);
}
} else {
newList.push(message);
}
newList.push(message);}} else
{
newList.push(message);}
this.setStoreState({ messageList: newList });
// Notify MozLoopService if appropriate that a message has been appended
// and it should therefore check if we need a different sized window or not.
if (message.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME &&
message.contentType !== CHAT_CONTENT_TYPES.CONTEXT &&
message.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION) {
if (message.contentType !== CHAT_CONTENT_TYPES.CONTEXT &&
message.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION) {
if (this._storeState.textChatEnabled) {
window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));
} else {
window.dispatchEvent(new CustomEvent("LoopChatDisabledMessageAppended"));
}
}
},
window.dispatchEvent(new CustomEvent("LoopChatMessageAppended"));} else
{
window.dispatchEvent(new CustomEvent("LoopChatDisabledMessageAppended"));}}},
/**
* Handles received text chat messages.
*
* @param {sharedActions.ReceivedTextChatMessage} actionData
*/
receivedTextChatMessage: function(actionData) {
receivedTextChatMessage: function receivedTextChatMessage(actionData) {
// If we don't know how to deal with this content, then skip it
// as this version doesn't support it.
if (actionData.contentType !== CHAT_CONTENT_TYPES.TEXT &&
actionData.contentType !== CHAT_CONTENT_TYPES.CONTEXT_TILE &&
actionData.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION) {
return;
}
if (actionData.contentType !== CHAT_CONTENT_TYPES.TEXT &&
actionData.contentType !== CHAT_CONTENT_TYPES.CONTEXT_TILE &&
actionData.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION) {
return;}
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.RECEIVED, actionData);},
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.RECEIVED, actionData);
},
/**
* Handles sending of a chat message.
*
* @param {sharedActions.SendTextChatMessage} actionData
*/
sendTextChatMessage: function(actionData) {
sendTextChatMessage: function sendTextChatMessage(actionData) {
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SENT, actionData);
this._sdkDriver.sendTextChatMessage(actionData);
},
this._sdkDriver.sendTextChatMessage(actionData);},
/**
* Handles receiving information about the room - specifically the room name
* so it can be added to the list.
* so it can be updated.
*
* @param {sharedActions.UpdateRoomInfo} actionData
*/
updateRoomInfo: function(actionData) {
updateRoomInfo: function updateRoomInfo(actionData) {
// XXX When we add special messages to desktop, we'll need to not post
// multiple changes of room name, only the first. Bug 1171940 should fix this.
if (actionData.roomName) {
var roomName = actionData.roomName;
if (!roomName && actionData.roomContextUrls && actionData.roomContextUrls.length) {
roomName = actionData.roomContextUrls[0].description ||
actionData.roomContextUrls[0].url;
}
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
message: roomName
});
}
roomName = actionData.roomContextUrls[0].description ||
actionData.roomContextUrls[0].url;}
this.setStoreState({ roomName: roomName });}
// Append the context if we have any.
if (("roomContextUrls" in actionData) && actionData.roomContextUrls &&
actionData.roomContextUrls.length) {
if ("roomContextUrls" in actionData && actionData.roomContextUrls &&
actionData.roomContextUrls.length) {
// We only support the first url at the moment.
var urlData = actionData.roomContextUrls[0];
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: urlData.description,
extraData: {
location: urlData.location,
thumbnail: urlData.thumbnail
}
});
}
},
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: urlData.description,
extraData: {
location: urlData.location,
thumbnail: urlData.thumbnail } });}},
/**
* Handles receiving information about the room context due to a change of the tabs
*
* @param {sharedActions.updateRoomContext} actionData
*/
updateRoomContext: function(actionData) {
updateRoomContext: function updateRoomContext(actionData) {
// Firstly, check if there is a previous context tile, if not, create it
var contextTile = null;
for (var i = this._storeState.messageList.length - 1; i >= 0; i--) {
if (this._storeState.messageList[i].contentType === CHAT_CONTENT_TYPES.CONTEXT_TILE) {
contextTile = this._storeState.messageList[i];
break;
}
}
break;}}
if (!contextTile) {
this._appendContextTileMessage(actionData);
return;
}
return;}
var oldDomain = new URL(contextTile.extraData.newRoomURL).hostname;
var currentDomain = new URL(actionData.newRoomURL).hostname;
if (oldDomain === currentDomain) {
return;
}
return;}
this._appendContextTileMessage(actionData);},
this._appendContextTileMessage(actionData);
},
/**
@@ -233,44 +231,61 @@ loop.store.TextChatStore = (function() {
*
* @param {sharedActions.remotePeerDisconnected} actionData
*/
remotePeerDisconnected: function(actionData) {
remotePeerDisconnected: function remotePeerDisconnected(actionData) {
var notificationTextKey;
if (actionData.peerHungup) {
notificationTextKey = "peer_left_session";
} else {
notificationTextKey = "peer_unexpected_quit";
}
notificationTextKey = "peer_left_session";} else
{
notificationTextKey = "peer_unexpected_quit";}
var message = {
contentType: CHAT_CONTENT_TYPES.NOTIFICATION,
message: notificationTextKey,
receivedTimestamp: (new Date()).toISOString()
};
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.RECEIVED, message);
},
var message = {
contentType: CHAT_CONTENT_TYPES.NOTIFICATION,
message: notificationTextKey,
receivedTimestamp: new Date().toISOString(),
extraData: {
peerStatus: "disconnected" } };
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.RECEIVED, message);},
remotePeerConnected: function remotePeerConnected() {
var notificationTextKey = "peer_join_session";
var message = {
contentType: CHAT_CONTENT_TYPES.NOTIFICATION,
message: notificationTextKey,
receivedTimestamp: new Date().toISOString(),
extraData: {
peerStatus: "connected" } };
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.RECEIVED, message);},
/**
* Appends a context tile message to the UI and sends it.
*
* @param {sharedActions.updateRoomContext} data
*/
_appendContextTileMessage: function(data) {
var msgData = {
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: data.newRoomDescription,
extraData: {
roomToken: data.roomToken,
newRoomThumbnail: data.newRoomThumbnail,
newRoomURL: data.newRoomURL
},
sentTimestamp: (new Date()).toISOString()
};
_appendContextTileMessage: function _appendContextTileMessage(data) {
var msgData = {
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: data.newRoomDescription,
extraData: {
roomToken: data.roomToken,
newRoomThumbnail: data.newRoomThumbnail,
newRoomURL: data.newRoomURL },
this.sendTextChatMessage(msgData);
}
});
sentTimestamp: new Date().toISOString() };
return TextChatStore;
})();
this.sendTextChatMessage(msgData);} });
return TextChatStore;}();

View File

@@ -1,6 +1,6 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.shared = loop.shared || {};
@@ -17,253 +17,253 @@ loop.shared.views.chat = function (mozL10n) {
/**
* Renders an individual entry for the text chat entries view.
*/
var TextChatEntry = React.createClass({
displayName: "TextChatEntry",
var TextChatEntry = React.createClass({ displayName: "TextChatEntry",
mixins: [React.addons.PureRenderMixin],
mixins: [React.addons.PureRenderMixin],
propTypes: {
contentType: React.PropTypes.string.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
extraData: React.PropTypes.object,
message: React.PropTypes.string.isRequired,
showTimestamp: React.PropTypes.bool.isRequired,
timestamp: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired },
propTypes: {
contentType: React.PropTypes.string.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher),
extraData: React.PropTypes.object,
message: React.PropTypes.string.isRequired,
showTimestamp: React.PropTypes.bool.isRequired,
timestamp: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired
},
/**
* Pretty print timestamp. From time in milliseconds to HH:MM
* (or L10N equivalent).
*
*/
_renderTimestamp: function () {
_renderTimestamp: function _renderTimestamp() {
var date = new Date(this.props.timestamp);
return React.createElement(
"span",
{ className: "text-chat-entry-timestamp" },
date.toLocaleTimeString(mozL10n.language.code, { hour: "numeric", minute: "numeric",
hour12: false })
);
},
return (
React.createElement("span", { className: "text-chat-entry-timestamp" },
date.toLocaleTimeString(mozL10n.language.code,
{ hour: "numeric", minute: "numeric",
hour12: false })));},
render: function render() {
var classes = classNames({
"text-chat-entry": this.props.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION,
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
"sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
"special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
"text-chat-notif": this.props.contentType === CHAT_CONTENT_TYPES.NOTIFICATION });
render: function () {
var classes = classNames({
"text-chat-entry": this.props.contentType !== CHAT_CONTENT_TYPES.NOTIFICATION,
"received": this.props.type === CHAT_MESSAGE_TYPES.RECEIVED,
"sent": this.props.type === CHAT_MESSAGE_TYPES.SENT,
"special": this.props.type === CHAT_MESSAGE_TYPES.SPECIAL,
"room-name": this.props.contentType === CHAT_CONTENT_TYPES.ROOM_NAME,
"text-chat-notif": this.props.contentType === CHAT_CONTENT_TYPES.NOTIFICATION
});
if (this.props.contentType === CHAT_CONTENT_TYPES.CONTEXT_TILE) {
return React.createElement(
"div",
{ className: classes },
React.createElement(sharedViews.ContextUrlView, {
allowClick: true,
description: this.props.message,
dispatcher: this.props.dispatcher,
thumbnail: this.props.extraData.newRoomThumbnail,
url: this.props.extraData.newRoomURL }),
this.props.showTimestamp ? this._renderTimestamp() : null
);
}
return (
React.createElement("div", { className: classes },
React.createElement(sharedViews.ContextUrlView, {
allowClick: true,
description: this.props.message,
dispatcher: this.props.dispatcher,
thumbnail: this.props.extraData.newRoomThumbnail,
url: this.props.extraData.newRoomURL }),
this.props.showTimestamp ? this._renderTimestamp() : null));}
if (this.props.contentType === CHAT_CONTENT_TYPES.NOTIFICATION) {
return React.createElement(
"div",
{ className: classes },
React.createElement(
"div",
{ className: "content-wrapper" },
React.createElement("img", { className: "notification-icon", src: "shared/img/leave_notification.svg" }),
React.createElement(
"p",
null,
mozL10n.get(this.props.message)
)
),
this.props.showTimestamp ? this._renderTimestamp() : null
);
}
return (
React.createElement("div", { className: classes },
React.createElement("div", { className: "content-wrapper" },
React.createElement("img", { className: "notification-icon",
src: this.props.extraData &&
this.props.extraData.peerStatus === "connected" ?
"shared/img/join_notification.svg" :
"shared/img/leave_notification.svg" }),
React.createElement("p", null, mozL10n.get(this.props.message))),
this.props.showTimestamp ? this._renderTimestamp() : null));}
var linkClickHandler;
if (loop.shared.utils.isDesktop()) {
linkClickHandler = function (url) {
loop.request("OpenURL", url);
};
}
linkClickHandler = function linkClickHandler(url) {
loop.request("OpenURL", url);};}
return React.createElement(
"div",
{ className: classes },
React.createElement(sharedViews.LinkifiedTextView, {
linkClickHandler: linkClickHandler,
rawText: this.props.message }),
React.createElement("span", { className: "text-chat-arrow" }),
this.props.showTimestamp ? this._renderTimestamp() : null
);
}
});
var TextChatRoomName = React.createClass({
displayName: "TextChatRoomName",
mixins: [React.addons.PureRenderMixin],
return (
React.createElement("div", { className: classes },
React.createElement(sharedViews.LinkifiedTextView, {
linkClickHandler: linkClickHandler,
rawText: this.props.message }),
React.createElement("span", { className: "text-chat-arrow" }),
this.props.showTimestamp ? this._renderTimestamp() : null));} });
var TextChatHeader = React.createClass({ displayName: "TextChatHeader",
mixins: [React.addons.PureRenderMixin],
propTypes: {
chatHeaderName: React.PropTypes.string.isRequired },
render: function render() {
return (
React.createElement("div", { className: "text-chat-header special" },
React.createElement("p", null, mozL10n.get("room_you_have_joined_title", { chatHeaderName: this.props.chatHeaderName }))));} });
propTypes: {
message: React.PropTypes.string.isRequired
},
render: function () {
return React.createElement(
"div",
{ className: "text-chat-header special room-name" },
React.createElement(
"p",
null,
mozL10n.get("rooms_welcome_title", { conversationName: this.props.message })
)
);
}
});
/**
* Manages the text entries in the chat entries view. This is split out from
* TextChatView so that scrolling can be managed more efficiently - this
* component only updates when the message list is changed.
*/
var TextChatEntriesView = React.createClass({
displayName: "TextChatEntriesView",
var TextChatEntriesView = React.createClass({ displayName: "TextChatEntriesView",
mixins: [
React.addons.PureRenderMixin,
sharedMixins.AudioMixin],
mixins: [React.addons.PureRenderMixin, sharedMixins.AudioMixin],
statics: {
ONE_MINUTE: 60
},
statics: {
ONE_MINUTE: 60 },
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
messageList: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
showInitialContext: React.PropTypes.bool.isRequired
},
getInitialState: function () {
return {
receivedMessageCount: 0
};
},
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
messageList: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
roomName: React.PropTypes.string,
showInitialContext: React.PropTypes.bool.isRequired },
_hasChatMessages: function () {
getInitialState: function getInitialState() {
return {
receivedMessageCount: 0 };},
_hasChatMessages: function _hasChatMessages() {
return this.props.messageList.some(function (message) {
return message.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME && message.contentType !== CHAT_CONTENT_TYPES.CONTEXT;
});
},
return message.contentType !== CHAT_CONTENT_TYPES.CONTEXT;});},
componentWillUpdate: function () {
var node = this.getDOMNode();
componentWillUpdate: function componentWillUpdate() {
var node = ReactDOM.findDOMNode(this);
if (!node) {
return;
}
return;}
// Scroll only if we're right at the bottom of the display, or if we've
// not had any chat messages so far.
this.shouldScroll = !this._hasChatMessages() || node.scrollHeight === node.scrollTop + node.clientHeight;
},
this.shouldScroll = !this._hasChatMessages() ||
node.scrollHeight === node.scrollTop + node.clientHeight;},
componentWillReceiveProps: function (nextProps) {
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
var receivedMessageCount = nextProps.messageList.filter(function (message) {
return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
}).length;
return message.type === CHAT_MESSAGE_TYPES.RECEIVED;}).
length;
// If the number of received messages has increased, we play a sound.
if (receivedMessageCount > this.state.receivedMessageCount) {
this.play("message");
this.setState({ receivedMessageCount: receivedMessageCount });
}
},
this.setState({ receivedMessageCount: receivedMessageCount });}},
componentDidUpdate: function () {
componentDidUpdate: function componentDidUpdate() {
// Don't scroll if we haven't got any chat messages yet - e.g. for context
// display, we want to display starting at the top.
if (this.shouldScroll && this._hasChatMessages()) {
// This ensures the paint is complete.
window.requestAnimationFrame(function () {
try {
var node = this.getDOMNode();
node.scrollTop = node.scrollHeight - node.clientHeight;
} catch (ex) {
console.error("TextChatEntriesView.componentDidUpdate exception", ex);
}
}.bind(this));
}
},
var node = ReactDOM.findDOMNode(this);
node.scrollTop = node.scrollHeight - node.clientHeight;}
catch (ex) {
console.error("TextChatEntriesView.componentDidUpdate exception", ex);}}.
render: function () {
bind(this));}},
render: function render() {
/* Keep track of the last printed timestamp. */
var lastTimestamp = 0;
var entriesClasses = classNames({
"text-chat-entries": true
});
var entriesClasses = classNames({
"text-chat-entries": true,
// Added for testability
"custom-room-name": this.props.roomName && this.props.roomName.length > 0 });
var headerName = this.props.roomName || mozL10n.get("clientShortname2");
return (
React.createElement("div", { className: entriesClasses },
React.createElement("div", { className: "text-chat-scroller" },
loop.shared.utils.isDesktop() ? null :
React.createElement(TextChatHeader, { chatHeaderName: headerName }),
this.props.messageList.map(function (entry, i) {
if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
if (!this.props.showInitialContext) {return null;}
switch (entry.contentType) {
case CHAT_CONTENT_TYPES.CONTEXT:
return (
React.createElement("div", { className: "context-url-view-wrapper", key: i },
React.createElement(sharedViews.ContextUrlView, {
allowClick: true,
description: entry.message,
dispatcher: this.props.dispatcher,
thumbnail: entry.extraData.thumbnail,
url: entry.extraData.location })));
default:
console.error("Unsupported contentType",
entry.contentType);
return null;}}
/* For SENT messages there is no received timestamp. */
var timestamp = entry.receivedTimestamp || entry.sentTimestamp;
var timeDiff = this._isOneMinDelta(timestamp, lastTimestamp);
var shouldShowTimestamp = this._shouldShowTimestamp(i,
timeDiff);
if (shouldShowTimestamp) {
lastTimestamp = timestamp;}
return (
React.createElement(TextChatEntry, { contentType: entry.contentType,
dispatcher: this.props.dispatcher,
extraData: entry.extraData,
key: i,
message: entry.message,
showTimestamp: shouldShowTimestamp,
timestamp: timestamp,
type: entry.type }));},
this))));},
return React.createElement(
"div",
{ className: entriesClasses },
React.createElement(
"div",
{ className: "text-chat-scroller" },
this.props.messageList.map(function (entry, i) {
if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
if (!this.props.showInitialContext) {
return null;
}
switch (entry.contentType) {
case CHAT_CONTENT_TYPES.ROOM_NAME:
return React.createElement(TextChatRoomName, {
key: i,
message: entry.message });
case CHAT_CONTENT_TYPES.CONTEXT:
return React.createElement(
"div",
{ className: "context-url-view-wrapper", key: i },
React.createElement(sharedViews.ContextUrlView, {
allowClick: true,
description: entry.message,
dispatcher: this.props.dispatcher,
thumbnail: entry.extraData.thumbnail,
url: entry.extraData.location })
);
default:
console.error("Unsupported contentType", entry.contentType);
return null;
}
}
/* For SENT messages there is no received timestamp. */
var timestamp = entry.receivedTimestamp || entry.sentTimestamp;
var timeDiff = this._isOneMinDelta(timestamp, lastTimestamp);
var shouldShowTimestamp = this._shouldShowTimestamp(i, timeDiff);
if (shouldShowTimestamp) {
lastTimestamp = timestamp;
}
return React.createElement(TextChatEntry, { contentType: entry.contentType,
dispatcher: this.props.dispatcher,
extraData: entry.extraData,
key: i,
message: entry.message,
showTimestamp: shouldShowTimestamp,
timestamp: timestamp,
type: entry.type });
}, this)
)
);
},
/**
* Decide to show timestamp or not on a message.
@@ -274,18 +274,19 @@ loop.shared.views.chat = function (mozL10n) {
* @param {boolean} timeDiff If difference between consecutive messages is
* bigger than one minute.
*/
_shouldShowTimestamp: function (idx, timeDiff) {
_shouldShowTimestamp: function _shouldShowTimestamp(idx, timeDiff) {
if (!idx) {
return true;
}
return true;}
/* If consecutive messages are from different senders */
if (this.props.messageList[idx].type !== this.props.messageList[idx - 1].type) {
return true;
}
if (this.props.messageList[idx].type !==
this.props.messageList[idx - 1].type) {
return true;}
return timeDiff;},
return timeDiff;
},
/**
* Determines if difference between the two timestamp arguments
@@ -296,18 +297,18 @@ loop.shared.views.chat = function (mozL10n) {
* @param {string} currTime Timestamp of message yet to be rendered.
* @param {string} prevTime Last timestamp printed in the chat view.
*/
_isOneMinDelta: function (currTime, prevTime) {
_isOneMinDelta: function _isOneMinDelta(currTime, prevTime) {
var date1 = new Date(currTime);
var date2 = new Date(prevTime);
var delta = date1 - date2;
if (delta / 1000 >= this.constructor.ONE_MINUTE) {
return true;
}
return true;}
return false;} });
return false;
}
});
/**
* Displays a text chat entry input box for sending messages.
@@ -317,22 +318,26 @@ loop.shared.views.chat = function (mozL10n) {
* @property {Boolean} textChatEnabled Set to true to enable the box. If false, the
* text chat box won't be displayed.
*/
var TextChatInputView = React.createClass({
displayName: "TextChatInputView",
var TextChatInputView = React.createClass({ displayName: "TextChatInputView",
mixins: [
React.addons.PureRenderMixin],
mixins: [React.addons.LinkedStateMixin, React.addons.PureRenderMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showPlaceholder: React.PropTypes.bool.isRequired,
textChatEnabled: React.PropTypes.bool.isRequired
},
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showPlaceholder: React.PropTypes.bool.isRequired,
textChatEnabled: React.PropTypes.bool.isRequired },
getInitialState: function getInitialState() {
return {
messageDetail: "" };},
handleChange: function handleChange(event) {
this.setState({ messageDetail: event.target.value });},
getInitialState: function () {
return {
messageDetail: ""
};
},
/**
* Handles a key being pressed - looking for the return key for submitting
@@ -340,55 +345,54 @@ loop.shared.views.chat = function (mozL10n) {
*
* @param {Object} event The DOM event.
*/
handleKeyDown: function (event) {
handleKeyDown: function handleKeyDown(event) {
if (event.which === 13) {
this.handleFormSubmit(event);
}
},
this.handleFormSubmit(event);}},
/**
* Handles submitting of the form - dispatches a send text chat message.
*
* @param {Object} event The DOM event.
*/
handleFormSubmit: function (event) {
handleFormSubmit: function handleFormSubmit(event) {
event.preventDefault();
// Don't send empty messages.
if (!this.state.messageDetail) {
return;
}
return;}
this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: this.state.messageDetail,
sentTimestamp: new Date().toISOString() }));
this.props.dispatcher.dispatch(new sharedActions.SendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: this.state.messageDetail,
sentTimestamp: new Date().toISOString()
}));
// Reset the form to empty, ready for the next message.
this.setState({ messageDetail: "" });
},
this.setState({ messageDetail: "" });},
render: function () {
render: function render() {
if (!this.props.textChatEnabled) {
return null;
}
return null;}
return (
React.createElement("div", { className: "text-chat-box" },
React.createElement("form", { onSubmit: this.handleFormSubmit },
React.createElement("input", {
onChange: this.handleChange,
onKeyDown: this.handleKeyDown,
placeholder: this.props.showPlaceholder ? mozL10n.get("chat_textbox_placeholder") : "",
type: "text",
value: this.state.messageDetail }))));} });
return React.createElement(
"div",
{ className: "text-chat-box" },
React.createElement(
"form",
{ onSubmit: this.handleFormSubmit },
React.createElement("input", {
onKeyDown: this.handleKeyDown,
placeholder: this.props.showPlaceholder ? mozL10n.get("chat_textbox_placeholder") : "",
type: "text",
valueLink: this.linkState("messageDetail") })
)
);
}
});
/**
* Displays the text chat view. This includes the text chat messages as well
@@ -398,64 +402,66 @@ loop.shared.views.chat = function (mozL10n) {
* @property {Boolean} showInitialContext Set to true to show the room name
* and initial context tile for linker clicker's special list items
*/
var TextChatView = React.createClass({
displayName: "TextChatView",
var TextChatView = React.createClass({ displayName: "TextChatView",
mixins: [
loop.store.StoreMixin("textChatStore")],
mixins: [React.addons.LinkedStateMixin, loop.store.StoreMixin("textChatStore")],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showInitialContext: React.PropTypes.bool.isRequired,
showTile: React.PropTypes.bool.isRequired
},
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
showInitialContext: React.PropTypes.bool.isRequired,
showTile: React.PropTypes.bool.isRequired },
getInitialState: function () {
return this.getStoreState();
},
render: function () {
getInitialState: function getInitialState() {
return this.getStoreState();},
render: function render() {
var messageList = this.state.messageList;
// Filter out items not displayed when showing initial context.
// We do this here so that we can set the classes correctly on the view.
if (!this.props.showInitialContext) {
messageList = messageList.filter(function (item) {
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL || item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME && item.contentType !== CHAT_CONTENT_TYPES.CONTEXT;
});
}
return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
item.contentType !== CHAT_CONTENT_TYPES.CONTEXT;});}
// Only show the placeholder if we've sent messages.
var hasSentMessages = messageList.some(function (item) {
return item.type === CHAT_MESSAGE_TYPES.SENT;
});
return item.type === CHAT_MESSAGE_TYPES.SENT;});
var textChatViewClasses = classNames({
"text-chat-view": true,
"text-chat-entries-empty": !messageList.length,
"text-chat-disabled": !this.state.textChatEnabled
});
return React.createElement(
"div",
{ className: textChatViewClasses },
React.createElement(TextChatEntriesView, {
dispatcher: this.props.dispatcher,
messageList: messageList,
showInitialContext: this.props.showInitialContext }),
React.createElement(TextChatInputView, {
dispatcher: this.props.dispatcher,
showPlaceholder: !hasSentMessages,
textChatEnabled: this.state.textChatEnabled }),
React.createElement(sharedViews.AdsTileView, {
dispatcher: this.props.dispatcher,
showTile: this.props.showTile })
);
}
});
var textChatViewClasses = classNames({
"text-chat-view": true,
"text-chat-entries-empty": !messageList.length,
"text-chat-disabled": !this.state.textChatEnabled });
return {
TextChatEntriesView: TextChatEntriesView,
TextChatEntry: TextChatEntry,
TextChatView: TextChatView
};
}(navigator.mozL10n || document.mozL10n);
return (
React.createElement("div", { className: textChatViewClasses },
React.createElement(TextChatEntriesView, {
dispatcher: this.props.dispatcher,
messageList: messageList,
roomName: this.state.roomName,
showInitialContext: this.props.showInitialContext }),
React.createElement(TextChatInputView, {
dispatcher: this.props.dispatcher,
showPlaceholder: !hasSentMessages,
textChatEnabled: this.state.textChatEnabled }),
React.createElement(sharedViews.AdsTileView, {
dispatcher: this.props.dispatcher,
showTile: this.props.showTile })));} });
return {
TextChatEntriesView: TextChatEntriesView,
TextChatEntry: TextChatEntry,
TextChatView: TextChatView };}(
navigator.mozL10n || document.mozL10n);

View File

@@ -1,4 +1,4 @@
// This is derived from Diego Perini's code,
"use strict"; // This is derived from Diego Perini's code,
// currently available at https://gist.github.com/dperini/729294
// Regular Expression for URL validation
@@ -32,7 +32,7 @@
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.urlRegExps = (function() {
loop.shared.urlRegExps = function () {
"use strict";
@@ -40,33 +40,30 @@ loop.shared.urlRegExps = (function() {
// if you need to debug changes to this:
var fullUrlMatch = new RegExp(
// Protocol identifier.
"(?:(?:https?|ftp)://)" +
// User:pass authentication.
"((?:\\S+(?::\\S*)?@)?" +
"(?:" +
// IP address dotted notation octets:
// excludes loopback network 0.0.0.0,
// excludes reserved space >= 224.0.0.0,
// excludes network & broadcast addresses,
// (first & last IP address of each class).
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
"|" +
// Host name.
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
// Domain name.
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
// TLD identifier.
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))" +
// Port number.
"(?::\\d{2,5})?" +
// Resource path.
"(?:[/?#]\\S*)?)", "i");
// Protocol identifier.
"(?:(?:https?|ftp)://)" +
// User:pass authentication.
"((?:\\S+(?::\\S*)?@)?" +
"(?:" +
// IP address dotted notation octets:
// excludes loopback network 0.0.0.0,
// excludes reserved space >= 224.0.0.0,
// excludes network & broadcast addresses,
// (first & last IP address of each class).
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
"|" +
// Host name.
"(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
// Domain name.
"(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
// TLD identifier.
"(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))" +
// Port number.
"(?::\\d{2,5})?" +
// Resource path.
"(?:[/?#]\\S*)?)", "i");
return {
fullUrlMatch: fullUrlMatch
};
})();
return {
fullUrlMatch: fullUrlMatch };}();

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
var loop = loop || {};
loop.validate = (function() {
loop.validate = function () {
"use strict";
/**
@@ -14,10 +14,10 @@ loop.validate = (function() {
* @return {Array} Array difference
*/
function difference(arr1, arr2) {
return arr1.filter(function(item) {
return arr2.indexOf(item) === -1;
});
}
return arr1.filter(function (item) {
return arr2.indexOf(item) === -1;});}
/**
* Retrieves the type name of an object or constructor. Fallback to "unknown"
@@ -28,19 +28,19 @@ loop.validate = (function() {
*/
function typeName(obj) {
if (obj === null) {
return "null";
}
return "null";}
if (typeof obj === "function") {
return obj.name || obj.toString().match(/^function\s?([^\s(]*)/)[1];
}
return obj.name || obj.toString().match(/^function\s?([^\s(]*)/)[1];}
if (typeof obj.constructor === "function") {
return typeName(obj.constructor);
}
return typeName(obj.constructor);}
return "unknown";}
return "unknown";
}
/**
* Simple typed values validator.
@@ -49,10 +49,10 @@ loop.validate = (function() {
* @param {Object} schema Validation schema
*/
function Validator(schema) {
this.schema = schema || {};
}
this.schema = schema || {};}
Validator.prototype = {
Validator.prototype = {
/**
* Validates all passed values against declared dependencies.
*
@@ -60,11 +60,11 @@ loop.validate = (function() {
* @return {Object} The validated values object
* @throws {TypeError} If validation fails
*/
validate: function(values) {
validate: function validate(values) {
this._checkRequiredProperties(values);
this._checkRequiredTypes(values);
return values;
},
return values;},
/**
* Checks if any of Object values matches any of current dependency type
@@ -73,17 +73,17 @@ loop.validate = (function() {
* @param {Object} values The values object
* @throws {TypeError}
*/
_checkRequiredTypes: function(values) {
Object.keys(this.schema).forEach(function(name) {
_checkRequiredTypes: function _checkRequiredTypes(values) {
Object.keys(this.schema).forEach(function (name) {
var types = this.schema[name];
types = Array.isArray(types) ? types : [types];
if (!this._dependencyMatchTypes(values[name], types)) {
throw new TypeError("invalid dependency: " + name +
"; expected " + types.map(typeName).join(", ") +
", got " + typeName(values[name]));
}
}, this);
},
throw new TypeError("invalid dependency: " + name +
"; expected " + types.map(typeName).join(", ") +
", got " + typeName(values[name]));}},
this);},
/**
* Checks if a values object owns the required keys defined in dependencies.
@@ -92,15 +92,15 @@ loop.validate = (function() {
* @param {Object} values The values object
* @throws {TypeError} If any dependency is missing.
*/
_checkRequiredProperties: function(values) {
var definedProperties = Object.keys(values).filter(function(name) {
return typeof values[name] !== "undefined";
});
_checkRequiredProperties: function _checkRequiredProperties(values) {
var definedProperties = Object.keys(values).filter(function (name) {
return typeof values[name] !== "undefined";});
var diff = difference(Object.keys(this.schema), definedProperties);
if (diff.length > 0) {
throw new TypeError("missing required " + diff.join(", "));
}
},
throw new TypeError("missing required " + diff.join(", "));}},
/**
* Checks if a given value matches any of the provided type requirements.
@@ -110,22 +110,20 @@ loop.validate = (function() {
* @return {Boolean}
* @throws {TypeError} If the value doesn't match any types.
*/
_dependencyMatchTypes: function(value, types) {
return types.some(function(Type) {
_dependencyMatchTypes: function _dependencyMatchTypes(value, types) {
return types.some(function (Type) {
try {
return typeof Type === "undefined" || // skip checking
Type === null && value === null || // null type
value.constructor === Type || // native type
Type.prototype.isPrototypeOf(value) || // custom type
typeName(value) === typeName(Type); // type string eq.
Type === null && value === null || // null type
value.constructor === Type || // native type
Type.prototype.isPrototypeOf(value) || // custom type
typeName(value) === typeName(Type); // type string eq.
} catch (e) {
return false;
}
});
}
};
return false;}});} };
return {
Validator: Validator
};
})();
return {
Validator: Validator };}();

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
{
"rules": {
// This is useful for some of the tests, e.g.
// expect(new Foo()).to.Throw(/error/)
"no-new": 0
"extends": "../../test/.eslintrc",
"globals": {
"ReactDOMServer": false,
}
}

View File

@@ -1,110 +1,105 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.crypto", function() {
describe("loop.crypto", function () {
"use strict";
var expect = chai.expect;
var sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
beforeEach(function () {
sandbox = sinon.sandbox.create();});
afterEach(function() {
afterEach(function () {
sandbox.restore();
loop.crypto.setRootObject(window);
});
loop.crypto.setRootObject(window);});
describe("#isSupported", function() {
it("should return true by default", function() {
expect(loop.crypto.isSupported()).eql(true);
});
it("should return false if crypto isn't supported", function() {
describe("#isSupported", function () {
it("should return true by default", function () {
expect(loop.crypto.isSupported()).eql(true);});
it("should return false if crypto isn't supported", function () {
loop.crypto.setRootObject({});
expect(loop.crypto.isSupported()).eql(false);
});
});
expect(loop.crypto.isSupported()).eql(false);});});
describe("#generateKey", function() {
it("should throw if web crypto is not available", function() {
describe("#generateKey", function () {
it("should throw if web crypto is not available", function () {
loop.crypto.setRootObject({});
expect(function() {
loop.crypto.generateKey();
}).to.Throw(/not supported/);
});
expect(function () {
loop.crypto.generateKey();}).
to.Throw(/not supported/);});
it("should generate a key", function() {
it("should generate a key", function () {
// The key is a random string, so we can't really test much else.
return expect(loop.crypto.generateKey()).to.eventually.be.a("string");
});
});
return expect(loop.crypto.generateKey()).to.eventually.be.a("string");});});
describe("#encryptBytes", function() {
it("should throw if web crypto is not available", function() {
describe("#encryptBytes", function () {
it("should throw if web crypto is not available", function () {
loop.crypto.setRootObject({});
expect(function() {
loop.crypto.encryptBytes();
}).to.Throw(/not supported/);
});
expect(function () {
loop.crypto.encryptBytes();}).
to.Throw(/not supported/);});
it("should encrypt an object with a specific key", function() {
return expect(loop.crypto.encryptBytes("Wt2-bZKeHO2wnaq00ZM6Nw",
JSON.stringify({ test: true }))).to.eventually.be.a("string");
});
});
describe("#decryptBytes", function() {
it("should throw if web crypto is not available", function() {
it("should encrypt an object with a specific key", function () {
return expect(loop.crypto.encryptBytes("Wt2-bZKeHO2wnaq00ZM6Nw",
JSON.stringify({ test: true }))).to.eventually.be.a("string");});});
describe("#decryptBytes", function () {
it("should throw if web crypto is not available", function () {
loop.crypto.setRootObject({});
expect(function() {
loop.crypto.decryptBytes();
}).to.Throw(/not supported/);
});
expect(function () {
loop.crypto.decryptBytes();}).
to.Throw(/not supported/);});
it("should decypt an object via a specific key", function() {
it("should decypt an object via a specific key", function () {
var key = "Wt2-bZKeHO2wnaq00ZM6Nw";
var encryptedContext = "XvN9FDEm/GtE/5Bx5ezpn7JVDeZrtwOJy2CBjTGgJ4L33HhHOqEW+5k=";
return expect(loop.crypto.decryptBytes(key, encryptedContext)).to.eventually.eql(JSON.stringify({ test: true }));
});
return expect(loop.crypto.decryptBytes(key, encryptedContext)).to.eventually.eql(JSON.stringify({ test: true }));});
it("should fail if the key didn't work", function() {
it("should fail if the key didn't work", function () {
var bad = "Bad-bZKeHO2wnaq00ZM6Nw";
var encryptedContext = "TGZaAE3mqsBFK0GfheZXXDCaRKXJmIKJ8WzF0KBEl4Aldzf3iYlAsLQdA8XSXXvtJR2UYz+f";
return expect(loop.crypto.decryptBytes(bad, encryptedContext)).to.be.rejected;
});
});
return expect(loop.crypto.decryptBytes(bad, encryptedContext)).to.be.rejected;});});
describe("Full cycle", function() {
it("should be able to encrypt and decypt in a full cycle", function(done) {
var context = JSON.stringify({
contextObject: true,
UTF8String: "对话"
});
return loop.crypto.generateKey().then(function(key) {
loop.crypto.encryptBytes(key, context).then(function(encryptedContext) {
loop.crypto.decryptBytes(key, encryptedContext).then(function(decryptedContext) {
describe("Full cycle", function () {
it("should be able to encrypt and decypt in a full cycle", function (done) {
var context = JSON.stringify({
contextObject: true,
UTF8String: "对话" });
return loop.crypto.generateKey().then(function (key) {
loop.crypto.encryptBytes(key, context).then(function (encryptedContext) {
loop.crypto.decryptBytes(key, encryptedContext).then(function (decryptedContext) {
expect(decryptedContext).eql(context);
done();
}).catch(function(error) {
done(error);
});
}).catch(function(error) {
done(error);
});
}).catch(function(error) {
done(error);
});
});
});
done();}).
catch(function (error) {
done(error);});}).
});
catch(function (error) {
done(error);});}).
catch(function (error) {
done(error);});});});});

View File

@@ -1,34 +1,34 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.Dispatcher", function() {
describe("loop.Dispatcher", function () {
"use strict";
var expect = chai.expect;
var sharedActions = loop.shared.actions;
var dispatcher, sandbox;
beforeEach(function() {
beforeEach(function () {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
});
dispatcher = new loop.Dispatcher();});
afterEach(function() {
sandbox.restore();
});
describe("#register", function() {
it("should register a store against an action name", function() {
afterEach(function () {
sandbox.restore();});
describe("#register", function () {
it("should register a store against an action name", function () {
var object = { fake: true };
dispatcher.register(object, ["getWindowData"]);
// XXXmikedeboer: Consider changing these tests to not access private
// properties anymore (`_eventData`).
expect(dispatcher._eventData.getWindowData[0]).eql(object);
});
expect(dispatcher._eventData.getWindowData[0]).eql(object);});
it("should register multiple store against an action name", function() {
it("should register multiple store against an action name", function () {
var object1 = { fake: true };
var object2 = { fake2: true };
@@ -36,21 +36,21 @@ describe("loop.Dispatcher", function() {
dispatcher.register(object2, ["getWindowData"]);
expect(dispatcher._eventData.getWindowData[0]).eql(object1);
expect(dispatcher._eventData.getWindowData[1]).eql(object2);
});
});
expect(dispatcher._eventData.getWindowData[1]).eql(object2);});});
describe("#unregister", function() {
it("should unregister a store against an action name", function() {
describe("#unregister", function () {
it("should unregister a store against an action name", function () {
var object = { fake: true };
dispatcher.register(object, ["getWindowData"]);
dispatcher.unregister(object, ["getWindowData"]);
expect(dispatcher._eventData.hasOwnProperty("getWindowData")).eql(false);
});
expect(dispatcher._eventData.hasOwnProperty("getWindowData")).eql(false);});
it("should unregister multiple stores against an action name", function() {
it("should unregister multiple stores against an action name", function () {
var object1 = { fake: true };
var object2 = { fake2: true };
@@ -61,56 +61,56 @@ describe("loop.Dispatcher", function() {
expect(dispatcher._eventData.getWindowData.length).eql(1);
dispatcher.unregister(object2, ["getWindowData"]);
expect(dispatcher._eventData.hasOwnProperty("getWindowData")).eql(false);
});
});
expect(dispatcher._eventData.hasOwnProperty("getWindowData")).eql(false);});});
describe("#dispatch", function() {
describe("#dispatch", function () {
var getDataStore1, getDataStore2, gotMediaPermissionStore1, mediaConnectedStore1;
var getDataAction, gotMediaPermissionAction, mediaConnectedAction;
beforeEach(function() {
getDataAction = new sharedActions.GetWindowData({
windowId: "42"
});
beforeEach(function () {
getDataAction = new sharedActions.GetWindowData({
windowId: "42" });
gotMediaPermissionAction = new sharedActions.GotMediaPermission();
mediaConnectedAction = new sharedActions.MediaConnected({
sessionData: {}
});
mediaConnectedAction = new sharedActions.MediaConnected({
sessionData: {} });
getDataStore1 = {
getWindowData: sinon.stub() };
getDataStore2 = {
getWindowData: sinon.stub() };
gotMediaPermissionStore1 = {
gotMediaPermission: sinon.stub() };
mediaConnectedStore1 = {
mediaConnected: function mediaConnected() {} };
getDataStore1 = {
getWindowData: sinon.stub()
};
getDataStore2 = {
getWindowData: sinon.stub()
};
gotMediaPermissionStore1 = {
gotMediaPermission: sinon.stub()
};
mediaConnectedStore1 = {
mediaConnected: function() {}
};
dispatcher.register(getDataStore1, ["getWindowData"]);
dispatcher.register(getDataStore2, ["getWindowData"]);
dispatcher.register(gotMediaPermissionStore1, ["gotMediaPermission"]);
dispatcher.register(mediaConnectedStore1, ["mediaConnected"]);
});
dispatcher.register(mediaConnectedStore1, ["mediaConnected"]);});
it("should dispatch an action to the required object", function() {
it("should dispatch an action to the required object", function () {
dispatcher.dispatch(gotMediaPermissionAction);
sinon.assert.notCalled(getDataStore1.getWindowData);
sinon.assert.calledOnce(gotMediaPermissionStore1.gotMediaPermission);
sinon.assert.calledWithExactly(gotMediaPermissionStore1.gotMediaPermission,
gotMediaPermissionAction);
sinon.assert.calledWithExactly(gotMediaPermissionStore1.gotMediaPermission,
gotMediaPermissionAction);
sinon.assert.notCalled(getDataStore2.getWindowData);
});
sinon.assert.notCalled(getDataStore2.getWindowData);});
it("should dispatch actions to multiple objects", function() {
it("should dispatch actions to multiple objects", function () {
dispatcher.dispatch(getDataAction);
sinon.assert.calledOnce(getDataStore1.getWindowData);
@@ -119,24 +119,24 @@ describe("loop.Dispatcher", function() {
sinon.assert.notCalled(gotMediaPermissionStore1.gotMediaPermission);
sinon.assert.calledOnce(getDataStore2.getWindowData);
sinon.assert.calledWithExactly(getDataStore2.getWindowData, getDataAction);
});
sinon.assert.calledWithExactly(getDataStore2.getWindowData, getDataAction);});
it("should dispatch multiple actions", function() {
it("should dispatch multiple actions", function () {
dispatcher.dispatch(gotMediaPermissionAction);
dispatcher.dispatch(getDataAction);
sinon.assert.calledOnce(gotMediaPermissionStore1.gotMediaPermission);
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledOnce(getDataStore2.getWindowData);
});
sinon.assert.calledOnce(getDataStore2.getWindowData);});
describe("Error handling", function() {
beforeEach(function() {
sandbox.stub(console, "error");
});
it("should handle uncaught exceptions", function() {
describe("Error handling", function () {
beforeEach(function () {
sandbox.stub(console, "error");});
it("should handle uncaught exceptions", function () {
getDataStore1.getWindowData.throws("Uncaught Error");
dispatcher.dispatch(getDataAction);
@@ -144,39 +144,39 @@ describe("loop.Dispatcher", function() {
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledOnce(getDataStore2.getWindowData);
sinon.assert.calledOnce(gotMediaPermissionStore1.gotMediaPermission);
});
sinon.assert.calledOnce(gotMediaPermissionStore1.gotMediaPermission);});
it("should log uncaught exceptions", function() {
it("should log uncaught exceptions", function () {
getDataStore1.getWindowData.throws("Uncaught Error");
dispatcher.dispatch(getDataAction);
sinon.assert.calledOnce(console.error);
});
});
sinon.assert.calledOnce(console.error);});});
describe("Queued actions", function() {
beforeEach(function() {
describe("Queued actions", function () {
beforeEach(function () {
// Restore the stub, so that we can easily add a function to be
// returned. Unfortunately, sinon doesn't make this easy.
sandbox.stub(mediaConnectedStore1, "mediaConnected", function() {
sandbox.stub(mediaConnectedStore1, "mediaConnected", function () {
dispatcher.dispatch(getDataAction);
sinon.assert.notCalled(getDataStore1.getWindowData);
sinon.assert.notCalled(getDataStore2.getWindowData);
});
});
sinon.assert.notCalled(getDataStore2.getWindowData);});});
it("should not dispatch an action if the previous action hasn't finished", function() {
it("should not dispatch an action if the previous action hasn't finished", function () {
// Dispatch the first action. The action handler dispatches the second
// action - see the beforeEach above.
dispatcher.dispatch(mediaConnectedAction);
sinon.assert.calledOnce(mediaConnectedStore1.mediaConnected);
});
sinon.assert.calledOnce(mediaConnectedStore1.mediaConnected);});
it("should dispatch an action when the previous action finishes", function() {
it("should dispatch an action when the previous action finishes", function () {
// Dispatch the first action. The action handler dispatches the second
// action - see the beforeEach above.
dispatcher.dispatch(mediaConnectedAction);
@@ -184,8 +184,4 @@ describe("loop.Dispatcher", function() {
sinon.assert.calledOnce(mediaConnectedStore1.mediaConnected);
// These should be called, because the dispatcher synchronously queues actions.
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledOnce(getDataStore2.getWindowData);
});
});
});
});
sinon.assert.calledOnce(getDataStore2.getWindowData);});});});});

View File

@@ -22,6 +22,8 @@
<!-- libs -->
<script src="/shared/vendor/react.js"></script>
<script src="/shared/vendor/react-dom.js"></script>
<script src="/test/vendor/react-dom-server.js"></script>
<script src="/shared/vendor/classnames.js"></script>
<script src="/shared/vendor/backbone.js"></script>

View File

@@ -1,181 +1,181 @@
/*
* Many of these tests are ported from Autolinker.js:
*
* https://github.com/gregjacobs/Autolinker.js/blob/master/tests/AutolinkerSpec.js
*
* which is released under the MIT license. Thanks to Greg Jacobs for his hard
* work there.
*
* The MIT License (MIT)
* Original Work Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com)
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
describe("loop.shared.views.LinkifiedTextView", function() {
"use strict"; /*
* Many of these tests are ported from Autolinker.js:
*
* https://github.com/gregjacobs/Autolinker.js/blob/master/tests/AutolinkerSpec.js
*
* which is released under the MIT license. Thanks to Greg Jacobs for his hard
* work there.
*
* The MIT License (MIT)
* Original Work Copyright (c) 2014 Gregory Jacobs (http://greg-jacobs.com)
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
describe("loop.shared.views.LinkifiedTextView", function () {
"use strict";
var expect = chai.expect;
var LinkifiedTextView = loop.shared.views.LinkifiedTextView;
var TestUtils = React.addons.TestUtils;
describe("LinkifiedTextView", function() {
describe("LinkifiedTextView", function () {
function renderToMarkup(string, extraProps) {
return React.renderToStaticMarkup(
React.createElement(
LinkifiedTextView,
_.extend({ rawText: string }, extraProps)));
}
return ReactDOMServer.renderToStaticMarkup(
React.createElement(
LinkifiedTextView,
_.extend({ rawText: string }, extraProps)));}
describe("#render", function() {
describe("#render", function () {
function testRender(testData) {
it(testData.desc, function() {
var markup = renderToMarkup(testData.rawText,
{ suppressTarget: true, sendReferrer: true });
it(testData.desc, function () {
var markup = renderToMarkup(testData.rawText,
{ suppressTarget: true, sendReferrer: true });
expect(markup).to.equal(testData.markup);});}
expect(markup).to.equal(testData.markup);
});
}
function testSkip(testData) {
it.skip(testData.desc, function() {
var markup = renderToMarkup(testData.rawText,
{ suppressTarget: true, sendReferrer: true });
it.skip(testData.desc, function () {
var markup = renderToMarkup(testData.rawText,
{ suppressTarget: true, sendReferrer: true });
expect(markup).to.equal(testData.markup);
});
}
expect(markup).to.equal(testData.markup);});}
describe("this.props.suppressTarget", function() {
it("should make links w/o a target attr if suppressTarget is true",
function() {
var markup = renderToMarkup("http://example.com", { suppressTarget: true });
expect(markup).to.equal(
'<p><a href="http://example.com/" rel="noreferrer">http://example.com/</a></p>');
});
it("should make links with target=_blank if suppressTarget is not given",
function() {
var markup = renderToMarkup("http://example.com", {});
describe("this.props.suppressTarget", function () {
it("should make links w/o a target attr if suppressTarget is true",
function () {
var markup = renderToMarkup("http://example.com", { suppressTarget: true });
expect(markup).to.equal(
'<p><a href="http://example.com/" rel="noreferrer" target="_blank">http://example.com/</a></p>');
});
});
expect(markup).to.equal(
'<p><a href="http://example.com/" rel="noreferrer">http://example.com/</a></p>');});
describe("this.props.sendReferrer", function() {
it("should make links w/o rel=noreferrer if sendReferrer is true",
function() {
var markup = renderToMarkup("http://example.com", { sendReferrer: true });
expect(markup).to.equal(
'<p><a href="http://example.com/" target="_blank">http://example.com/</a></p>');
});
it("should make links with target=_blank if suppressTarget is not given",
function () {
var markup = renderToMarkup("http://example.com", {});
it("should make links with rel=noreferrer if sendReferrer is not given",
function() {
var markup = renderToMarkup("http://example.com", {});
expect(markup).to.equal(
'<p><a href="http://example.com/" rel="noreferrer" target="_blank">http://example.com/</a></p>');});});
expect(markup).to.equal(
'<p><a href="http://example.com/" rel="noreferrer" target="_blank">http://example.com/</a></p>');
});
});
describe("this.props.linkClickHandler", function() {
describe("this.props.sendReferrer", function () {
it("should make links w/o rel=noreferrer if sendReferrer is true",
function () {
var markup = renderToMarkup("http://example.com", { sendReferrer: true });
expect(markup).to.equal(
'<p><a href="http://example.com/" target="_blank">http://example.com/</a></p>');});
it("should make links with rel=noreferrer if sendReferrer is not given",
function () {
var markup = renderToMarkup("http://example.com", {});
expect(markup).to.equal(
'<p><a href="http://example.com/" rel="noreferrer" target="_blank">http://example.com/</a></p>');});});
describe("this.props.linkClickHandler", function () {
function mountTestComponent(string, extraProps) {
return TestUtils.renderIntoDocument(
React.createElement(
LinkifiedTextView,
_.extend({ rawText: string }, extraProps)));
}
React.createElement(
LinkifiedTextView,
_.extend({ rawText: string }, extraProps)));}
it("should be called when a generated link is clicked", function() {
it("should be called when a generated link is clicked", function () {
var fakeUrl = "http://example.com";
var linkClickHandler = sinon.stub();
var comp = mountTestComponent(fakeUrl, { linkClickHandler: linkClickHandler });
TestUtils.Simulate.click(comp.getDOMNode().querySelector("a"));
TestUtils.Simulate.click(ReactDOM.findDOMNode(comp).querySelector("a"));
sinon.assert.calledOnce(linkClickHandler);
});
sinon.assert.calledOnce(linkClickHandler);});
it("should cause sendReferrer and suppressTarget props to be ignored",
function() {
var linkClickHandler = function() {};
var markup = renderToMarkup("http://example.com", {
linkClickHandler: linkClickHandler,
sendReferrer: false,
suppressTarget: false
});
it("should cause sendReferrer and suppressTarget props to be ignored",
function () {
var linkClickHandler = function linkClickHandler() {};
expect(markup).to.equal(
'<p><a href="http://example.com/">http://example.com/</a></p>');
});
var markup = renderToMarkup("http://example.com", {
linkClickHandler: linkClickHandler,
sendReferrer: false,
suppressTarget: false });
describe("#_handleClickEvent", function() {
expect(markup).to.equal(
'<p><a href="http://example.com/">http://example.com/</a></p>');});
describe("#_handleClickEvent", function () {
var fakeEvent;
var fakeUrl = "http://example.com";
beforeEach(function() {
fakeEvent = {
currentTarget: { href: fakeUrl },
preventDefault: sinon.stub(),
stopPropagation: sinon.stub()
};
});
beforeEach(function () {
fakeEvent = {
currentTarget: { href: fakeUrl },
preventDefault: sinon.stub(),
stopPropagation: sinon.stub() };});
it("should call preventDefault on the given event", function() {
it("should call preventDefault on the given event", function () {
function linkClickHandler() {}
var comp = mountTestComponent(
fakeUrl, { linkClickHandler: linkClickHandler });
fakeUrl, { linkClickHandler: linkClickHandler });
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(fakeEvent.preventDefault);
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
});
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);});
it("should call stopPropagation on the given event", function() {
it("should call stopPropagation on the given event", function () {
function linkClickHandler() {}
var comp = mountTestComponent(
fakeUrl, { linkClickHandler: linkClickHandler });
fakeUrl, { linkClickHandler: linkClickHandler });
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(fakeEvent.stopPropagation);
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);
});
sinon.assert.calledWithExactly(fakeEvent.stopPropagation);});
it("should call this.props.linkClickHandler with event.currentTarget.href", function() {
it("should call this.props.linkClickHandler with event.currentTarget.href", function () {
var linkClickHandler = sinon.stub();
var comp = mountTestComponent(
fakeUrl, { linkClickHandler: linkClickHandler });
fakeUrl, { linkClickHandler: linkClickHandler });
comp._handleClickEvent(fakeEvent);
sinon.assert.calledOnce(linkClickHandler);
sinon.assert.calledWithExactly(linkClickHandler, fakeUrl);
});
});
});
sinon.assert.calledWithExactly(linkClickHandler, fakeUrl);});});});
// Note that these are really integration tests with the parser and React.
// Since we're depending on that integration to provide us with security
@@ -186,207 +186,203 @@ describe("loop.shared.views.LinkifiedTextView", function() {
// parseStringToElements. We may also want both unit and integration
// testing for some subset of these.
var tests = [
{
desc: "should only add a container to a string with no URLs",
rawText: "This is a test.",
markup: "<p>This is a test.</p>"
},
{
desc: "should linkify a string containing only a URL with a trailing slash",
rawText: "http://example.com/",
markup: '<p><a href="http://example.com/">http://example.com/</a></p>'
},
{
desc: "should linkify a string containing only a URL with no trailing slash",
rawText: "http://example.com",
markup: '<p><a href="http://example.com/">http://example.com/</a></p>'
},
{
desc: "should linkify a URL with text preceding it",
rawText: "This is a link to http://example.com",
markup: '<p>This is a link to <a href="http://example.com/">http://example.com/</a></p>'
},
{
desc: "should linkify a URL with text before and after",
rawText: "Look at http://example.com near the bottom",
markup: '<p>Look at <a href="http://example.com/">http://example.com/</a> near the bottom</p>'
},
{
desc: "should linkify an http URL",
rawText: "This is an http://example.com test",
markup: '<p>This is an <a href="http://example.com/">http://example.com/</a> test</p>'
},
{
desc: "should linkify an https URL",
rawText: "This is an https://example.com test",
markup: '<p>This is an <a href="https://example.com/">https://example.com/</a> test</p>'
},
{
desc: "should not linkify a data URL",
rawText: "This is an data:image/png;base64,iVBORw0KGgoAAA test",
markup: "<p>This is an data:image/png;base64,iVBORw0KGgoAAA test</p>"
},
{
desc: "should linkify URLs with a port number and no trailing slash",
rawText: "Joe went to http://example.com:8000 today.",
markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a trailing slash",
rawText: "Joe went to http://example.com:8000/ today.",
markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a path",
rawText: "Joe went to http://example.com:8000/mysite/page today.",
markup: '<p>Joe went to <a href="http://example.com:8000/mysite/page">http://example.com:8000/mysite/page</a> today.</p>'
},
{
desc: "should linkify URLs with a port number and a query string",
rawText: "Joe went to http://example.com:8000?page=index today.",
markup: '<p>Joe went to <a href="http://example.com:8000/?page=index">http://example.com:8000/?page=index</a> today.</p>'
},
{
desc: "should linkify a URL with a port number and a hash string",
rawText: "Joe went to http://example.com:8000#page=index today.",
markup: '<p>Joe went to <a href="http://example.com:8000/#page=index">http://example.com:8000/#page=index</a> today.</p>'
},
{
desc: "should NOT include preceding ':' intros without a space",
rawText: "the link:http://example.com/",
markup: '<p>the link:<a href="http://example.com/">http://example.com/</a></p>'
},
{
desc: "should NOT autolink URLs with 'javascript:' URI scheme",
rawText: "do not link javascript:window.alert('hi') please",
markup: "<p>do not link javascript:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink URLs with the 'JavAscriPt:' scheme",
rawText: "do not link JavAscriPt:window.alert('hi') please",
markup: "<p>do not link JavAscriPt:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink possible URLs with the 'vbscript:' URI scheme",
rawText: "do not link vbscript:window.alert('hi') please",
markup: "<p>do not link vbscript:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink URLs with the 'vBsCriPt:' URI scheme",
rawText: "do not link vBsCriPt:window.alert('hi') please",
markup: "<p>do not link vBsCriPt:window.alert(&#x27;hi&#x27;) please</p>"
},
{
desc: "should NOT autolink a string in the form of 'version:1.0'",
rawText: "version:1.0",
markup: "<p>version:1.0</p>"
},
{
desc: "should linkify an ftp URL",
rawText: "This is an ftp://example.com test",
markup: '<p>This is an <a href="ftp://example.com/">ftp://example.com/</a> test</p>'
},
{
desc: "should only add a container to a string with no URLs",
rawText: "This is a test.",
markup: "<p>This is a test.</p>" },
{
desc: "should linkify a string containing only a URL with a trailing slash",
rawText: "http://example.com/",
markup: '<p><a href="http://example.com/">http://example.com/</a></p>' },
{
desc: "should linkify a string containing only a URL with no trailing slash",
rawText: "http://example.com",
markup: '<p><a href="http://example.com/">http://example.com/</a></p>' },
{
desc: "should linkify a URL with text preceding it",
rawText: "This is a link to http://example.com",
markup: '<p>This is a link to <a href="http://example.com/">http://example.com/</a></p>' },
{
desc: "should linkify a URL with text before and after",
rawText: "Look at http://example.com near the bottom",
markup: '<p>Look at <a href="http://example.com/">http://example.com/</a> near the bottom</p>' },
{
desc: "should linkify an http URL",
rawText: "This is an http://example.com test",
markup: '<p>This is an <a href="http://example.com/">http://example.com/</a> test</p>' },
{
desc: "should linkify an https URL",
rawText: "This is an https://example.com test",
markup: '<p>This is an <a href="https://example.com/">https://example.com/</a> test</p>' },
{
desc: "should not linkify a data URL",
rawText: "This is an data:image/png;base64,iVBORw0KGgoAAA test",
markup: "<p>This is an data:image/png;base64,iVBORw0KGgoAAA test</p>" },
{
desc: "should linkify URLs with a port number and no trailing slash",
rawText: "Joe went to http://example.com:8000 today.",
markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>' },
{
desc: "should linkify URLs with a port number and a trailing slash",
rawText: "Joe went to http://example.com:8000/ today.",
markup: '<p>Joe went to <a href="http://example.com:8000/">http://example.com:8000/</a> today.</p>' },
{
desc: "should linkify URLs with a port number and a path",
rawText: "Joe went to http://example.com:8000/mysite/page today.",
markup: '<p>Joe went to <a href="http://example.com:8000/mysite/page">http://example.com:8000/mysite/page</a> today.</p>' },
{
desc: "should linkify URLs with a port number and a query string",
rawText: "Joe went to http://example.com:8000?page=index today.",
markup: '<p>Joe went to <a href="http://example.com:8000/?page=index">http://example.com:8000/?page=index</a> today.</p>' },
{
desc: "should linkify a URL with a port number and a hash string",
rawText: "Joe went to http://example.com:8000#page=index today.",
markup: '<p>Joe went to <a href="http://example.com:8000/#page=index">http://example.com:8000/#page=index</a> today.</p>' },
{
desc: "should NOT include preceding ':' intros without a space",
rawText: "the link:http://example.com/",
markup: '<p>the link:<a href="http://example.com/">http://example.com/</a></p>' },
{
desc: "should NOT autolink URLs with 'javascript:' URI scheme",
rawText: "do not link javascript:window.alert('hi') please",
markup: "<p>do not link javascript:window.alert(&#x27;hi&#x27;) please</p>" },
{
desc: "should NOT autolink URLs with the 'JavAscriPt:' scheme",
rawText: "do not link JavAscriPt:window.alert('hi') please",
markup: "<p>do not link JavAscriPt:window.alert(&#x27;hi&#x27;) please</p>" },
{
desc: "should NOT autolink possible URLs with the 'vbscript:' URI scheme",
rawText: "do not link vbscript:window.alert('hi') please",
markup: "<p>do not link vbscript:window.alert(&#x27;hi&#x27;) please</p>" },
{
desc: "should NOT autolink URLs with the 'vBsCriPt:' URI scheme",
rawText: "do not link vBsCriPt:window.alert('hi') please",
markup: "<p>do not link vBsCriPt:window.alert(&#x27;hi&#x27;) please</p>" },
{
desc: "should NOT autolink a string in the form of 'version:1.0'",
rawText: "version:1.0",
markup: "<p>version:1.0</p>" },
{
desc: "should linkify an ftp URL",
rawText: "This is an ftp://example.com test",
markup: '<p>This is an <a href="ftp://example.com/">ftp://example.com/</a> test</p>' },
// We don't want to include trailing dots in URLs, even though those
// are valid DNS names, as that should match user intent more of the
// time, as well as avoid this stuff:
//
// http://saynt2day.blogspot.it/2013/03/danger-of-trailing-dot-in-domain-name.html
//
{
desc: "should linkify 'http://example.com.', w/o a trailing dot",
rawText: "Joe went to http://example.com.",
markup: '<p>Joe went to <a href="http://example.com/">http://example.com/</a>.</p>' },
// XXX several more tests like this we could port from Autolinkify.js
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should exclude invalid chars after domain part",
rawText: "Joe went to http://www.example.com's today",
markup: '<p>Joe went to <a href="http://www.example.com/">http://www.example.com/</a>&#x27;s today</p>' },
{
desc: "should not linkify protocol-relative URLs",
rawText: "//C/Programs",
markup: "<p>//C/Programs</p>" },
{
desc: "should not linkify malformed URI sequences",
rawText: "http://www.example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
markup: "<p>http://www.example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%</p>" },
// do a few tests to convince ourselves that, when our code is handled
// HTML in the input box, the integration of our code with React should
// cause that to rendered to appear as HTML source code, rather than
// getting injected into our real HTML DOM
{
desc: "should linkify simple HTML include an href properly escaped",
rawText: '<p>Joe went to <a href="http://www.example.com">example</a></p>',
markup: '<p>&lt;p&gt;Joe went to &lt;a href=&quot;<a href="http://www.example.com/">http://www.example.com/</a>&quot;&gt;example&lt;/a&gt;&lt;/p&gt;</p>' },
{
desc: "should linkify HTML with nested tags and resource path properly escaped",
rawText: '<a href="http://example.com"><img src="http://example.com" /></a>',
markup: '<p>&lt;a href=&quot;<a href="http://example.com/">http://example.com/</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com/">http://example.com/</a>&quot; /&gt;&lt;/a&gt;</p>' },
{
desc: "should linkify and decode a string containing a Homographic attack URL with no trailing slash",
rawText: "http://ebаy.com",
markup: '<p><a href="http://xn--eby-7cd.com/">http://xn--eby-7cd.com/</a></p>' }];
// We don't want to include trailing dots in URLs, even though those
// are valid DNS names, as that should match user intent more of the
// time, as well as avoid this stuff:
//
// http://saynt2day.blogspot.it/2013/03/danger-of-trailing-dot-in-domain-name.html
//
{
desc: "should linkify 'http://example.com.', w/o a trailing dot",
rawText: "Joe went to http://example.com.",
markup: '<p>Joe went to <a href="http://example.com/">http://example.com/</a>.</p>'
},
// XXX several more tests like this we could port from Autolinkify.js
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should exclude invalid chars after domain part",
rawText: "Joe went to http://www.example.com's today",
markup: '<p>Joe went to <a href="http://www.example.com/">http://www.example.com/</a>&#x27;s today</p>'
},
{
desc: "should not linkify protocol-relative URLs",
rawText: "//C/Programs",
markup: "<p>//C/Programs</p>"
},
{
desc: "should not linkify malformed URI sequences",
rawText: "http://www.example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%",
markup: "<p>http://www.example.com/DNA/pizza/menu/lots-of-different-kinds-of-pizza/%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%8D%E0%B8%88%E0%B8%A1%E0%B8%A3%E0%</p>"
},
// do a few tests to convince ourselves that, when our code is handled
// HTML in the input box, the integration of our code with React should
// cause that to rendered to appear as HTML source code, rather than
// getting injected into our real HTML DOM
{
desc: "should linkify simple HTML include an href properly escaped",
rawText: '<p>Joe went to <a href="http://www.example.com">example</a></p>',
markup: '<p>&lt;p&gt;Joe went to &lt;a href=&quot;<a href="http://www.example.com/">http://www.example.com/</a>&quot;&gt;example&lt;/a&gt;&lt;/p&gt;</p>'
},
{
desc: "should linkify HTML with nested tags and resource path properly escaped",
rawText: '<a href="http://example.com"><img src="http://example.com" /></a>',
markup: '<p>&lt;a href=&quot;<a href="http://example.com/">http://example.com/</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com/">http://example.com/</a>&quot; /&gt;&lt;/a&gt;</p>'
},
{
desc: "should linkify and decode a string containing a Homographic attack URL with no trailing slash",
rawText: "http://ebаy.com",
markup: '<p><a href="http://xn--eby-7cd.com/">http://xn--eby-7cd.com/</a></p>'
}
];
var skippedTests = [
// XXX lots of tests similar to below we could port:
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should link localhost URLs with an allowed URL scheme",
rawText: "Joe went to http://localhost today",
markup: '<p>Joe went to <a href="http://localhost">localhost</a></p> today'
},
// XXX lots of tests similar to below we could port:
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should not include a ? if at the end of a URL",
rawText: "Did Joe go to http://example.com?",
markup: '<p>Did Joe go to <a href="http://example.com/">http://example.com/</a>?</p>'
},
{
desc: "should linkify 'check out http://example.com/monkey.', w/o trailing dots",
rawText: "check out http://example.com/monkey...",
markup: '<p>check out <a href="http://example.com/monkey">http://example.com/monkey</a>...</p>'
},
// another variant of eating too many trailing characters, it includes
// the trailing ", which it shouldn't. Makes links inside pasted HTML
// source be slightly broken. Not key for our target users, I suspect,
// but still...
{
desc: "should linkify HTML with nested tags and a resource path properly escaped",
rawText: '<a href="http://example.com"><img src="http://example.com/someImage.jpg" /></a>',
markup: '<p>&lt;a href=&quot;<a href="http://example.com/">http://example.com/</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com/someImage.jpg&quot;">http://example.com/someImage.jpg&quot;</a> /&gt;&lt;/a&gt;</p>'
},
// XXX handle domains without schemes (bug 1186245)
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should linkify a.museum (known TLD), but not abc.qqqq",
rawText: "a.museum should be linked, but abc.qqqq should not",
markup: '<p><a href="http://a.museum">a.museum</a> should be linked, but abc.qqqq should not</p>'
},
{
desc: "should linkify example.xyz (known TLD), but not example.etc (unknown TLD)",
rawText: "example.xyz should be linked, example.etc should not",
rawMarkup: '<><a href="http://example.xyz">example.xyz</a> should be linked, example.etc should not</p>'
}
];
// XXX lots of tests similar to below we could port:
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should link localhost URLs with an allowed URL scheme",
rawText: "Joe went to http://localhost today",
markup: '<p>Joe went to <a href="http://localhost">localhost</a></p> today' },
// XXX lots of tests similar to below we could port:
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should not include a ? if at the end of a URL",
rawText: "Did Joe go to http://example.com?",
markup: '<p>Did Joe go to <a href="http://example.com/">http://example.com/</a>?</p>' },
{
desc: "should linkify 'check out http://example.com/monkey.', w/o trailing dots",
rawText: "check out http://example.com/monkey...",
markup: '<p>check out <a href="http://example.com/monkey">http://example.com/monkey</a>...</p>' },
// another variant of eating too many trailing characters, it includes
// the trailing ", which it shouldn't. Makes links inside pasted HTML
// source be slightly broken. Not key for our target users, I suspect,
// but still...
{
desc: "should linkify HTML with nested tags and a resource path properly escaped",
rawText: '<a href="http://example.com"><img src="http://example.com/someImage.jpg" /></a>',
markup: '<p>&lt;a href=&quot;<a href="http://example.com/">http://example.com/</a>&quot;&gt;&lt;img src=&quot;<a href="http://example.com/someImage.jpg&quot;">http://example.com/someImage.jpg&quot;</a> /&gt;&lt;/a&gt;</p>' },
// XXX handle domains without schemes (bug 1186245)
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
{
desc: "should linkify a.museum (known TLD), but not abc.qqqq",
rawText: "a.museum should be linked, but abc.qqqq should not",
markup: '<p><a href="http://a.museum">a.museum</a> should be linked, but abc.qqqq should not</p>' },
{
desc: "should linkify example.xyz (known TLD), but not example.etc (unknown TLD)",
rawText: "example.xyz should be linked, example.etc should not",
rawMarkup: '<><a href="http://example.xyz">example.xyz</a> should be linked, example.etc should not</p>' }];
tests.forEach(testRender);
// XXX Over time, we'll want to make these pass..
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1186254
skippedTests.forEach(testSkip);
});
});
});
skippedTests.forEach(testSkip);});});});

View File

@@ -1,9 +1,9 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/* exported LoopMochaUtils */
var LoopMochaUtils = (function(global, _) {
var LoopMochaUtils = function (global, _) {
"use strict";
var gStubbedRequests;
@@ -37,15 +37,15 @@ var LoopMochaUtils = (function(global, _) {
var resolved = false;
var resolvedWith = null;
this.then = function(contFn) {
this.then = function (contFn) {
if (resolved) {
contFn(resolvedWith);
return this;
}
return this;}
continuations.push(contFn);
return this;
};
return this;};
/**
* Used to resolve an object of type SyncThenable.
@@ -53,43 +53,43 @@ var LoopMochaUtils = (function(global, _) {
* @param {*} result The result to return.
* @return {SyncThenable} A resolved SyncThenable object.
*/
this.resolve = function(result) {
this.resolve = function (result) {
resolved = true;
resolvedWith = result;
if (!continuations.length) {
return this;
}
return this;}
var contFn = continuations.shift();
while (contFn) {
contFn(result);
contFn = continuations.shift();
}
return this;
};
contFn = continuations.shift();}
this.reject = function(result) {
throw result;
};
return this;};
this.catch = function() {};
asyncFn(this.resolve.bind(this), this.reject.bind(this));
}
this.reject = function (result) {
throw result;};
SyncThenable.all = function(promises) {
return new SyncThenable(function(resolve) {
this.catch = function () {};
asyncFn(this.resolve.bind(this), this.reject.bind(this));}
SyncThenable.all = function (promises) {
return new SyncThenable(function (resolve) {
var results = [];
promises.forEach(function(promise) {
promise.then(function(result) {
results.push(result);
});
});
promises.forEach(function (promise) {
promise.then(function (result) {
results.push(result);});});
resolve(results);});};
resolve(results);
});
};
/**
* This simulates the equivalent of Promise.resolve() - calling the
@@ -98,11 +98,11 @@ var LoopMochaUtils = (function(global, _) {
* @param {*} result The result to return.
* @return {SyncThenable} A resolved SyncThenable object.
*/
SyncThenable.resolve = function(result) {
return new SyncThenable(function(resolve) {
resolve(result);
});
};
SyncThenable.resolve = function (result) {
return new SyncThenable(function (resolve) {
resolve(result);});};
/**
* Simple wrapper around `sinon.sandbox.create()` to also stub the native Promise
@@ -113,17 +113,17 @@ var LoopMochaUtils = (function(global, _) {
function createSandbox() {
var sandbox = sinon.sandbox.create();
sandbox.stub(global, "Promise", SyncThenable);
return sandbox;
}
return sandbox;}
/**
* Internal, see `handleIncomingRequest`.
*/
function invokeListenerCallbacks(data) {
gListenerCallbacks.forEach(function(cb) {
cb(data);
});
}
gListenerCallbacks.forEach(function (cb) {
cb(data);});}
/**
* Invoked when `window.sendAsyncMessage` is called. This means a message came
@@ -140,22 +140,22 @@ var LoopMochaUtils = (function(global, _) {
if (action === "Batch") {
result = {};
data[0].forEach(function(req) {
result[req[0]] = handleIncomingRequest(req, true);
});
invokeListenerCallbacks({ data: [seq, result] });
} else {
data[0].forEach(function (req) {
result[req[0]] = handleIncomingRequest(req, true);});
invokeListenerCallbacks({ data: [seq, result] });} else
{
if (!gStubbedRequests[action]) {
throw new Error("Action '" + action + "' not part of stubbed requests! Please add it!");
}
throw new Error("Action '" + action + "' not part of stubbed requests! Please add it!");}
result = gStubbedRequests[action].apply(gStubbedRequests, data);
if (isBatch) {
return result;
}
invokeListenerCallbacks({ data: [seq, result] });
}
return undefined;
}
return result;}
invokeListenerCallbacks({ data: [seq, result] });}
return undefined;}
/**
* Stub function that replaces `window.addMessageListener`.
@@ -166,11 +166,11 @@ var LoopMochaUtils = (function(global, _) {
*/
function addMessageListenerInternal(name, listenerCallback) {
if (name === "Loop:Message") {
gListenerCallbacks.push(listenerCallback);
} else if (name === "Loop:Message:Push") {
gPushListenerCallbacks.push(listenerCallback);
}
}
gListenerCallbacks.push(listenerCallback);} else
if (name === "Loop:Message:Push") {
gPushListenerCallbacks.push(listenerCallback);}}
/**
* Stub function that replaces `window.sendAsyncMessageInternal`. See
@@ -180,8 +180,8 @@ var LoopMochaUtils = (function(global, _) {
* @param {Array} data Payload of the request.
*/
function sendAsyncMessageInternal(messageName, data) {
handleIncomingRequest(data);
}
handleIncomingRequest(data);}
/**
* Entry point for test writers to add stubs for message handlers that they
@@ -199,20 +199,20 @@ var LoopMochaUtils = (function(global, _) {
if (!global.addMessageListener || global.addMessageListener !== addMessageListenerInternal) {
// Save older versions for later.
if (!gOldSendAsyncMessage) {
gOldAddMessageListener = global.addMessageListener;
}
gOldAddMessageListener = global.addMessageListener;}
if (!gOldSendAsyncMessage) {
gOldSendAsyncMessage = global.sendAsyncMessage;
}
gOldSendAsyncMessage = global.sendAsyncMessage;}
global.addMessageListener = addMessageListenerInternal;
global.sendAsyncMessage = sendAsyncMessageInternal;
gStubbedRequests = {};
}
gStubbedRequests = {};}
_.extend(gStubbedRequests, stubbedRequests);}
_.extend(gStubbedRequests, stubbedRequests);
}
/**
* Broadcast a push message on demand, which will invoke any active listeners
@@ -226,10 +226,10 @@ var LoopMochaUtils = (function(global, _) {
// Convert to a proper array.
var args = Array.prototype.slice.call(arguments);
var name = args.shift();
gPushListenerCallbacks.forEach(function(cb) {
cb({ data: [name, args] });
});
}
gPushListenerCallbacks.forEach(function (cb) {
cb({ data: [name, args] });});}
/**
* Restores our internal state to its original values and reverts adjustments
@@ -239,25 +239,25 @@ var LoopMochaUtils = (function(global, _) {
*/
function restore() {
if (!global.addMessageListener) {
return;
}
return;}
if (gOldAddMessageListener) {
global.addMessageListener = gOldAddMessageListener;
} else {
delete global.addMessageListener;
}
global.addMessageListener = gOldAddMessageListener;} else
{
delete global.addMessageListener;}
if (gOldSendAsyncMessage) {
global.sendAsyncMessage = gOldSendAsyncMessage;
} else {
delete global.sendAsyncMessage;
}
global.sendAsyncMessage = gOldSendAsyncMessage;} else
{
delete global.sendAsyncMessage;}
gStubbedRequests = null;
gListenerCallbacks = [];
gPushListenerCallbacks = [];
loop.request.reset();
loop.subscribe.reset();
}
loop.subscribe.reset();}
/**
* Used to initiate trapping of errors and warnings when running tests.
@@ -265,73 +265,73 @@ var LoopMochaUtils = (function(global, _) {
* results.
*/
function trapErrors() {
window.addEventListener("error", function(error) {
gUncaughtError = error;
});
window.addEventListener("error", function (error) {
gUncaughtError = error;});
var consoleWarn = console.warn;
var consoleError = console.error;
console.warn = function() {
console.warn = function () {
var args = Array.prototype.slice.call(arguments);
try {
throw new Error();
} catch (e) {
gCaughtIssues.push([args, e.stack]);
}
consoleWarn.apply(console, args);
};
console.error = function() {
throw new Error();}
catch (e) {
gCaughtIssues.push([args, e.stack]);}
consoleWarn.apply(console, args);};
console.error = function () {
var args = Array.prototype.slice.call(arguments);
try {
throw new Error();
} catch (e) {
gCaughtIssues.push([args, e.stack]);
}
consoleError.apply(console, args);
};
}
throw new Error();}
catch (e) {
gCaughtIssues.push([args, e.stack]);}
consoleError.apply(console, args);};}
/**
* Adds tests to check no warnings nor errors have occurred since trapErrors
* was called.
*/
function addErrorCheckingTests() {
describe("Uncaught Error Check", function() {
it("should load the tests without errors", function() {
chai.expect(gUncaughtError && gUncaughtError.message).to.be.undefined;
});
});
describe("Uncaught Error Check", function () {
it("should load the tests without errors", function () {
chai.expect(gUncaughtError && gUncaughtError.message).to.be.undefined;});});
describe("Unexpected Logged Warnings and Errors Check", function() {
it("should not log any warnings nor errors", function() {
describe("Unexpected Logged Warnings and Errors Check", function () {
it("should not log any warnings nor errors", function () {
if (gCaughtIssues.length) {
throw new Error(gCaughtIssues);
} else {
chai.expect(gCaughtIssues.length).to.eql(0);
}
});
});
}
throw new Error(gCaughtIssues);} else
{
chai.expect(gCaughtIssues.length).to.eql(0);}});});}
/**
* Utility function for starting the mocha test run. Adds a marker for when
* the tests have completed.
*/
function runTests() {
mocha.run(function() {
mocha.run(function () {
var completeNode = document.createElement("p");
completeNode.setAttribute("id", "complete");
completeNode.appendChild(document.createTextNode("Complete"));
document.getElementById("mocha").appendChild(completeNode);
});
}
document.getElementById("mocha").appendChild(completeNode);});}
return {
addErrorCheckingTests: addErrorCheckingTests,
createSandbox: createSandbox,
publish: publish,
restore: restore,
runTests: runTests,
stubLoopRequest: stubLoopRequest,
trapErrors: trapErrors
};
})(this, _);
return {
addErrorCheckingTests: addErrorCheckingTests,
createSandbox: createSandbox,
publish: publish,
restore: restore,
runTests: runTests,
stubLoopRequest: stubLoopRequest,
trapErrors: trapErrors };}(
this, _);

View File

@@ -1,287 +1,287 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loopapi-client", function() {
describe("loopapi-client", function () {
"use strict";
var expect = chai.expect;
var sandbox;
beforeEach(function() {
beforeEach(function () {
sandbox = sinon.sandbox.create();
window.addMessageListener = sinon.stub();
window.removeMessageListener = sinon.stub();
window.sendAsyncMessage = sinon.stub();
});
window.sendAsyncMessage = sinon.stub();});
afterEach(function() {
afterEach(function () {
loop.request.reset();
loop.subscribe.reset();
sandbox.restore();
delete window.addMessageListener;
delete window.removeMessageListener;
delete window.sendAsyncMessage;
});
delete window.sendAsyncMessage;});
describe("loop.request", function() {
it("should send a message", function() {
describe("loop.request", function () {
it("should send a message", function () {
var promise = loop.request("GetLoopPref", "enabled");
expect(promise).to.be.an.instanceof(Promise);
sinon.assert.calledOnce(window.sendAsyncMessage);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "GetLoopPref", "enabled"]);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "GetLoopPref", "enabled"]);
sinon.assert.calledOnce(window.addMessageListener);
// Call the added listener, so that the promise resolves.
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"]
});
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"] });
return promise;
});
it("should correct the command name", function() {
return promise;});
it("should correct the command name", function () {
var promise = loop.request("getLoopPref", "enabled");
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "GetLoopPref", "enabled"]);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "GetLoopPref", "enabled"]);
// Call the added listener, so that the promise resolves.
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"]
});
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"] });
return promise;
});
it("should pass all arguments in-order", function() {
return promise;});
it("should pass all arguments in-order", function () {
var promise = loop.request("SetLoopPref", "enabled", false, 1, 2, 3);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "SetLoopPref", "enabled", false, 1, 2, 3]);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "SetLoopPref", "enabled", false, 1, 2, 3]);
// Call the added listener, so that the promise resolves.
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"]
});
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"] });
return promise;
});
it("should resolve the promise when a response is received", function() {
return promise;});
it("should resolve the promise when a response is received", function () {
var listener;
window.addMessageListener = function(name, callback) {
listener = callback;
};
window.addMessageListener = function (name, callback) {
listener = callback;};
var promise = loop.request("GetLoopPref", "enabled").then(function(result) {
expect(result).to.eql("result");
});
listener({
data: [loop._lastMessageID, "result"]
});
var promise = loop.request("GetLoopPref", "enabled").then(function (result) {
expect(result).to.eql("result");});
return promise;
});
it("should not start listening for messages more than once", function() {
return new Promise(function(resolve) {
loop.request("GetLoopPref", "enabled").then(function() {
listener({
data: [loop._lastMessageID, "result"] });
return promise;});
it("should not start listening for messages more than once", function () {
return new Promise(function (resolve) {
loop.request("GetLoopPref", "enabled").then(function () {
sinon.assert.calledOnce(window.addMessageListener);
loop.request("GetLoopPref", "enabled").then(function() {
loop.request("GetLoopPref", "enabled").then(function () {
sinon.assert.calledOnce(window.addMessageListener);
resolve();
});
resolve();});
// Call the added listener, so that the promise resolves.
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"]
});
});
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"] });});
// Call the added listener, so that the promise resolves.
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"]
});
});
});
});
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"] });});});});
describe("loop.storeRequest", function() {
afterEach(function() {
loop.storedRequests = {};
});
it("should the result of a request", function() {
describe("loop.storeRequest", function () {
afterEach(function () {
loop.storedRequests = {};});
it("should the result of a request", function () {
loop.storeRequest(["GetLoopPref"], true);
expect(loop.storedRequests).to.deep.equal({
"GetLoopPref": true
});
});
expect(loop.storedRequests).to.deep.equal({
"GetLoopPref": true });});
it("should the result of a request with multiple params", function() {
loop.storeRequest(["GetLoopPref", "enabled", "or", "not", "well",
"perhaps", true, 2], true);
expect(loop.storedRequests).to.deep.equal({
"GetLoopPref|enabled|or|not|well|perhaps|true|2": true
});
});
});
describe("loop.getStoredRequest", function() {
afterEach(function() {
loop.storedRequests = {};
});
it("should the result of a request with multiple params", function () {
loop.storeRequest(["GetLoopPref", "enabled", "or", "not", "well",
"perhaps", true, 2], true);
it("should retrieve a result", function() {
expect(loop.storedRequests).to.deep.equal({
"GetLoopPref|enabled|or|not|well|perhaps|true|2": true });});});
describe("loop.getStoredRequest", function () {
afterEach(function () {
loop.storedRequests = {};});
it("should retrieve a result", function () {
loop.storedRequests["GetLoopPref"] = true;
expect(loop.getStoredRequest(["GetLoopPref"])).to.eql(true);
});
expect(loop.getStoredRequest(["GetLoopPref"])).to.eql(true);});
it("should return log and return null for invalid requests", function() {
it("should return log and return null for invalid requests", function () {
sandbox.stub(console, "error");
expect(loop.getStoredRequest(["SomethingNeverStored"])).to.eql(null);
sinon.assert.calledOnce(console.error);
sinon.assert.calledWithExactly(console.error,
"This request has not been stored!", ["SomethingNeverStored"]);
});
});
sinon.assert.calledWithExactly(console.error,
"This request has not been stored!", ["SomethingNeverStored"]);});});
describe("loop.requestMulti", function() {
it("should send a batch of messages", function() {
describe("loop.requestMulti", function () {
it("should send a batch of messages", function () {
var promise = loop.requestMulti(
["GetLoopPref", "enabled"],
["GetLoopPref", "e10s.enabled"]
);
["GetLoopPref", "enabled"],
["GetLoopPref", "e10s.enabled"]);
expect(promise).to.be.an.instanceof(Promise);
sinon.assert.calledOnce(window.sendAsyncMessage);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "Batch", [
[loop._lastMessageID - 2, "GetLoopPref", "enabled"],
[loop._lastMessageID - 1, "GetLoopPref", "e10s.enabled"]]
]);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "Batch", [
[loop._lastMessageID - 2, "GetLoopPref", "enabled"],
[loop._lastMessageID - 1, "GetLoopPref", "e10s.enabled"]]]);
// Call the added listener, so that the promise resolves.
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"]
});
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"] });
return promise;
});
it("should correct command names", function() {
return promise;});
it("should correct command names", function () {
var promise = loop.requestMulti(
["GetLoopPref", "enabled"],
// Use lowercase 'g' on purpose, it should get corrected:
["getLoopPref", "e10s.enabled"]
);
["GetLoopPref", "enabled"],
// Use lowercase 'g' on purpose, it should get corrected:
["getLoopPref", "e10s.enabled"]);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "Batch", [
[loop._lastMessageID - 2, "GetLoopPref", "enabled"],
[loop._lastMessageID - 1, "GetLoopPref", "e10s.enabled"]]]);
sinon.assert.calledWithExactly(window.sendAsyncMessage, "Loop:Message",
[loop._lastMessageID, "Batch", [
[loop._lastMessageID - 2, "GetLoopPref", "enabled"],
[loop._lastMessageID - 1, "GetLoopPref", "e10s.enabled"]]
]);
// Call the added listener, so that the promise resolves.
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"]
});
window.addMessageListener.args[0][1]({
name: "Loop:Message",
data: [loop._lastMessageID, "true"] });
return promise;
});
it("should resolve the promise when a response is received", function() {
return promise;});
it("should resolve the promise when a response is received", function () {
var listener;
window.addMessageListener = function(name, callback) {
listener = callback;
};
window.addMessageListener = function (name, callback) {
listener = callback;};
var promise = loop.requestMulti(
["GetLoopPref", "enabled"],
["GetLoopPref", "e10s.enabled"]
).then(function(result) {
expect(result).to.eql(["result1", "result2"]);
});
["GetLoopPref", "enabled"],
["GetLoopPref", "e10s.enabled"]).
then(function (result) {
expect(result).to.eql(["result1", "result2"]);});
listener({
listener({
data: [
loop._lastMessageID, {
"1": "result1",
"2": "result2"
}
]
});
loop._lastMessageID, {
"1": "result1",
"2": "result2" }] });
return promise;
});
it("should throw an error when no requests are passed in", function() {
expect(loop.requestMulti).to.throw(Error, /please pass in a list of calls/);
});
it("should throw when invalid request is passed in", function() {
expect(loop.requestMulti.bind(null, ["command"], null)).to
.throw(Error, /each call must be an array of options/);
expect(loop.requestMulti.bind(null, null, ["command"])).to
.throw(Error, /each call must be an array of options/);
});
});
return promise;});
describe("loop.subscribe", function() {
it("should throw an error when no requests are passed in", function () {
expect(loop.requestMulti).to.throw(Error, /please pass in a list of calls/);});
it("should throw when invalid request is passed in", function () {
expect(loop.requestMulti.bind(null, ["command"], null)).to.
throw(Error, /each call must be an array of options/);
expect(loop.requestMulti.bind(null, null, ["command"])).to.
throw(Error, /each call must be an array of options/);});});
describe("loop.subscribe", function () {
var sendMessage = null;
var callCount = 0;
beforeEach(function() {
beforeEach(function () {
callCount = 0;
window.addMessageListener = function(name, callback) {
window.addMessageListener = function (name, callback) {
sendMessage = callback;
++callCount;
};
});
++callCount;};});
afterEach(function() {
sendMessage = null;
});
it("subscribe to a push message", function() {
loop.subscribe("LoopStatusChanged", function() {});
afterEach(function () {
sendMessage = null;});
it("subscribe to a push message", function () {
loop.subscribe("LoopStatusChanged", function () {});
var subscriptions = loop.subscribe.inspect();
expect(callCount).to.eql(1);
expect(Object.getOwnPropertyNames(subscriptions).length).to.eql(1);
expect(subscriptions.LoopStatusChanged.length).to.eql(1);
});
expect(subscriptions.LoopStatusChanged.length).to.eql(1);});
it("should start listening for push messages when a subscriber registers", function() {
loop.subscribe("LoopStatusChanged", function() {});
it("should start listening for push messages when a subscriber registers", function () {
loop.subscribe("LoopStatusChanged", function () {});
expect(callCount).to.eql(1);
loop.subscribe("LoopStatusChanged", function() {});
loop.subscribe("LoopStatusChanged", function () {});
expect(callCount).to.eql(1);
loop.subscribe("Test", function() {});
expect(callCount).to.eql(1);
});
loop.subscribe("Test", function () {});
expect(callCount).to.eql(1);});
it("incoming push messages should invoke subscriptions", function() {
it("incoming push messages should invoke subscriptions", function () {
var stub1 = sinon.stub();
var stub2 = sinon.stub();
var stub3 = sinon.stub();
@@ -303,51 +303,48 @@ describe("loopapi-client", function() {
sinon.assert.calledOnce(stub2);
sinon.assert.calledWithExactly(stub2, "Foo", "Bar");
sinon.assert.calledOnce(stub3);
sinon.assert.calledWithExactly(stub3, "Foo", "Bar");
});
sinon.assert.calledWithExactly(stub3, "Foo", "Bar");});
it("should invoke subscription with non-array arguments too", function() {
it("should invoke subscription with non-array arguments too", function () {
var stub = sinon.stub();
loop.subscribe("LoopStatusChanged", stub);
sendMessage({ data: ["LoopStatusChanged", "Foo"] });
sinon.assert.calledOnce(stub);
sinon.assert.calledWithExactly(stub, "Foo");
});
});
sinon.assert.calledWithExactly(stub, "Foo");});});
describe("unsubscribe", function() {
it("should remove subscriptions from the map", function() {
var handler = function() {};
describe("unsubscribe", function () {
it("should remove subscriptions from the map", function () {
var handler = function handler() {};
loop.subscribe("LoopStatusChanged", handler);
loop.unsubscribe("LoopStatusChanged", handler);
expect(loop.subscribe.inspect().LoopStatusChanged.length).to.eql(0);
});
expect(loop.subscribe.inspect().LoopStatusChanged.length).to.eql(0);});
it("should not remove a subscription when a different handler is passed in", function() {
var handler = function() {};
it("should not remove a subscription when a different handler is passed in", function () {
var handler = function handler() {};
loop.subscribe("LoopStatusChanged", handler);
loop.unsubscribe("LoopStatusChanged", function() {});
expect(loop.subscribe.inspect().LoopStatusChanged.length).to.eql(1);
});
loop.unsubscribe("LoopStatusChanged", function () {});
expect(loop.subscribe.inspect().LoopStatusChanged.length).to.eql(1);});
it("should not throw when unsubscribing from an unknown subscription", function() {
loop.unsubscribe("foobar");
});
});
describe("unsubscribeAll", function() {
it("should clear all present subscriptions", function() {
loop.subscribe("LoopStatusChanged", function() {});
it("should not throw when unsubscribing from an unknown subscription", function () {
loop.unsubscribe("foobar");});});
describe("unsubscribeAll", function () {
it("should clear all present subscriptions", function () {
loop.subscribe("LoopStatusChanged", function () {});
expect(Object.getOwnPropertyNames(loop.subscribe.inspect()).length).to.eql(1);
loop.unsubscribeAll();
expect(Object.getOwnPropertyNames(loop.subscribe.inspect()).length).to.eql(0);
});
});
});
expect(Object.getOwnPropertyNames(loop.subscribe.inspect()).length).to.eql(0);});});});

View File

@@ -1,8 +1,8 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.shared.mixins", function() {
describe("loop.shared.mixins", function () {
"use strict";
var expect = chai.expect;
@@ -11,344 +11,341 @@ describe("loop.shared.mixins", function() {
var TestUtils = React.addons.TestUtils;
var ROOM_STATES = loop.store.ROOM_STATES;
beforeEach(function() {
sandbox = LoopMochaUtils.createSandbox();
});
beforeEach(function () {
sandbox = LoopMochaUtils.createSandbox();});
afterEach(function() {
afterEach(function () {
sandbox.restore();
sharedMixins.setRootObject(window);
});
sharedMixins.setRootObject(window);});
describe("loop.shared.mixins.UrlHashChangeMixin", function() {
describe("loop.shared.mixins.UrlHashChangeMixin", function () {
function createTestComponent(onUrlHashChange) {
var TestComp = React.createClass({
mixins: [loop.shared.mixins.UrlHashChangeMixin],
onUrlHashChange: onUrlHashChange || function() {},
render: function() {
return React.DOM.div();
}
});
return new React.createElement(TestComp);
}
var TestComp = React.createClass({ displayName: "TestComp",
mixins: [loop.shared.mixins.UrlHashChangeMixin],
onUrlHashChange: onUrlHashChange || function () {},
render: function render() {
return React.DOM.div();} });
it("should watch for hashchange event", function() {
return new React.createElement(TestComp);}
it("should watch for hashchange event", function () {
var addEventListener = sandbox.spy();
sharedMixins.setRootObject({
addEventListener: addEventListener
});
sharedMixins.setRootObject({
addEventListener: addEventListener });
TestUtils.renderIntoDocument(createTestComponent());
sinon.assert.calledOnce(addEventListener);
sinon.assert.calledWith(addEventListener, "hashchange");
});
sinon.assert.calledWith(addEventListener, "hashchange");});
it("should call onUrlHashChange when the url is updated", function() {
sharedMixins.setRootObject({
addEventListener: function(name, cb) {
it("should call onUrlHashChange when the url is updated", function () {
sharedMixins.setRootObject({
addEventListener: function addEventListener(name, cb) {
if (name === "hashchange") {
cb();
}
}
});
cb();}} });
var onUrlHashChange = sandbox.stub();
TestUtils.renderIntoDocument(createTestComponent(onUrlHashChange));
sinon.assert.calledOnce(onUrlHashChange);
});
});
sinon.assert.calledOnce(onUrlHashChange);});});
describe("loop.shared.mixins.DocumentLocationMixin", function() {
describe("loop.shared.mixins.DocumentLocationMixin", function () {
var reloadStub, TestComp;
beforeEach(function() {
beforeEach(function () {
reloadStub = sandbox.stub();
sharedMixins.setRootObject({
location: {
reload: reloadStub
}
});
sharedMixins.setRootObject({
location: {
reload: reloadStub } });
TestComp = React.createClass({
mixins: [loop.shared.mixins.DocumentLocationMixin],
render: function() {
return React.DOM.div();
}
});
});
it("should call window.location.reload", function() {
TestComp = React.createClass({ displayName: "TestComp",
mixins: [loop.shared.mixins.DocumentLocationMixin],
render: function render() {
return React.DOM.div();} });});
it("should call window.location.reload", function () {
var comp = TestUtils.renderIntoDocument(React.createElement(TestComp));
comp.locationReload();
sinon.assert.calledOnce(reloadStub);
});
});
sinon.assert.calledOnce(reloadStub);});});
describe("loop.shared.mixins.DocumentTitleMixin", function() {
describe("loop.shared.mixins.DocumentTitleMixin", function () {
var TestComp, rootObject;
beforeEach(function() {
rootObject = {
document: {}
};
beforeEach(function () {
rootObject = {
document: {} };
sharedMixins.setRootObject(rootObject);
TestComp = React.createClass({
mixins: [loop.shared.mixins.DocumentTitleMixin],
render: function() {
return React.DOM.div();
}
});
});
TestComp = React.createClass({ displayName: "TestComp",
mixins: [loop.shared.mixins.DocumentTitleMixin],
render: function render() {
return React.DOM.div();} });});
it("should set window.document.title", function() {
it("should set window.document.title", function () {
var comp = TestUtils.renderIntoDocument(React.createElement(TestComp));
comp.setTitle("It's a Fake!");
expect(rootObject.document.title).eql("It's a Fake!");
});
});
expect(rootObject.document.title).eql("It's a Fake!");});});
describe("loop.shared.mixins.WindowCloseMixin", function() {
describe("loop.shared.mixins.WindowCloseMixin", function () {
var TestComp, rootObject;
beforeEach(function() {
rootObject = {
close: sandbox.stub()
};
beforeEach(function () {
rootObject = {
close: sandbox.stub() };
sharedMixins.setRootObject(rootObject);
TestComp = React.createClass({
mixins: [loop.shared.mixins.WindowCloseMixin],
render: function() {
return React.DOM.div();
}
});
});
TestComp = React.createClass({ displayName: "TestComp",
mixins: [loop.shared.mixins.WindowCloseMixin],
render: function render() {
return React.DOM.div();} });});
it("should call window.close", function() {
it("should call window.close", function () {
var comp = TestUtils.renderIntoDocument(React.createElement(TestComp));
comp.closeWindow();
sinon.assert.calledOnce(rootObject.close);
sinon.assert.calledWithExactly(rootObject.close);
});
});
sinon.assert.calledWithExactly(rootObject.close);});});
describe("loop.shared.mixins.DocumentVisibilityMixin", function() {
describe("loop.shared.mixins.DocumentVisibilityMixin", function () {
var TestComp, onDocumentVisibleStub, onDocumentHiddenStub;
beforeEach(function() {
beforeEach(function () {
onDocumentVisibleStub = sandbox.stub();
onDocumentHiddenStub = sandbox.stub();
TestComp = React.createClass({
mixins: [loop.shared.mixins.DocumentVisibilityMixin],
onDocumentHidden: onDocumentHiddenStub,
onDocumentVisible: onDocumentVisibleStub,
render: function() {
return React.DOM.div();
}
});
});
TestComp = React.createClass({ displayName: "TestComp",
mixins: [loop.shared.mixins.DocumentVisibilityMixin],
onDocumentHidden: onDocumentHiddenStub,
onDocumentVisible: onDocumentVisibleStub,
render: function render() {
return React.DOM.div();} });});
function setupFakeVisibilityEventDispatcher(event) {
loop.shared.mixins.setRootObject({
document: {
addEventListener: function(_, fn) {
fn(event);
},
removeEventListener: sandbox.stub()
}
});
}
loop.shared.mixins.setRootObject({
document: {
addEventListener: function addEventListener(_, fn) {
fn(event);},
it("should call onDocumentVisible when document visibility changes to visible",
function() {
setupFakeVisibilityEventDispatcher({ target: { hidden: false } });
removeEventListener: sandbox.stub() } });}
TestUtils.renderIntoDocument(React.createElement(TestComp));
// Twice, because it's also called when the component was mounted.
sinon.assert.calledTwice(onDocumentVisibleStub);
});
it("should call onDocumentVisible when document visibility changes to hidden",
function() {
setupFakeVisibilityEventDispatcher({ target: { hidden: true } });
TestUtils.renderIntoDocument(React.createElement(TestComp));
it("should call onDocumentVisible when document visibility changes to visible",
function () {
setupFakeVisibilityEventDispatcher({ target: { hidden: false } });
sinon.assert.calledOnce(onDocumentHiddenStub);
});
});
TestUtils.renderIntoDocument(React.createElement(TestComp));
describe("loop.shared.mixins.MediaSetupMixin", function() {
// Twice, because it's also called when the component was mounted.
sinon.assert.calledTwice(onDocumentVisibleStub);});
it("should call onDocumentVisible when document visibility changes to hidden",
function () {
setupFakeVisibilityEventDispatcher({ target: { hidden: true } });
TestUtils.renderIntoDocument(React.createElement(TestComp));
sinon.assert.calledOnce(onDocumentHiddenStub);});});
describe("loop.shared.mixins.MediaSetupMixin", function () {
var view;
beforeEach(function() {
var TestComp = React.createClass({
mixins: [loop.shared.mixins.MediaSetupMixin],
render: function() {
return React.DOM.div();
}
});
beforeEach(function () {
var TestComp = React.createClass({ displayName: "TestComp",
mixins: [loop.shared.mixins.MediaSetupMixin],
render: function render() {
return React.DOM.div();} });
view = TestUtils.renderIntoDocument(React.createElement(TestComp));
});
describe("#getDefaultPublisherConfig", function() {
it("should throw if publishVideo is not given", function() {
expect(function() {
view.getDefaultPublisherConfig();
}).to.throw(/missing/);
});
it("should return a set of defaults based on the options", function() {
expect(view.getDefaultPublisherConfig({
publishVideo: true
}).publishVideo).eql(true);
});
});
});
view = TestUtils.renderIntoDocument(React.createElement(TestComp));});
describe("loop.shared.mixins.AudioMixin", function() {
describe("#getDefaultPublisherConfig", function () {
it("should throw if publishVideo is not given", function () {
expect(function () {
view.getDefaultPublisherConfig();}).
to.throw(/missing/);});
it("should return a set of defaults based on the options", function () {
expect(view.getDefaultPublisherConfig({
publishVideo: true }).
publishVideo).eql(true);});});});
describe("loop.shared.mixins.AudioMixin", function () {
var TestComp, getAudioBlobStub, fakeAudio;
beforeEach(function() {
beforeEach(function () {
getAudioBlobStub = sinon.stub().returns(
new Blob([new ArrayBuffer(10)], { type: "audio/ogg" }));
LoopMochaUtils.stubLoopRequest({
GetDoNotDisturb: function() { return true; },
GetAudioBlob: getAudioBlobStub,
GetLoopPref: sandbox.stub()
});
new Blob([new ArrayBuffer(10)], { type: "audio/ogg" }));
LoopMochaUtils.stubLoopRequest({
GetDoNotDisturb: function GetDoNotDisturb() {return true;},
GetAudioBlob: getAudioBlobStub,
GetLoopPref: sandbox.stub() });
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy() };
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
TestComp = React.createClass({
mixins: [loop.shared.mixins.AudioMixin],
componentDidMount: function() {
this.play("failure");
},
render: function() {
return React.DOM.div();
}
});
TestComp = React.createClass({ displayName: "TestComp",
mixins: [loop.shared.mixins.AudioMixin],
componentDidMount: function componentDidMount() {
this.play("failure");},
});
render: function render() {
return React.DOM.div();} });});
afterEach(function() {
LoopMochaUtils.restore();
});
it("should not play a failure sound when doNotDisturb true", function() {
afterEach(function () {
LoopMochaUtils.restore();});
it("should not play a failure sound when doNotDisturb true", function () {
TestUtils.renderIntoDocument(React.createElement(TestComp));
sinon.assert.notCalled(getAudioBlobStub);
sinon.assert.notCalled(fakeAudio.play);
});
sinon.assert.notCalled(fakeAudio.play);});
it("should play a failure sound, once", function () {
LoopMochaUtils.stubLoopRequest({
GetDoNotDisturb: function GetDoNotDisturb() {return false;} });
it("should play a failure sound, once", function() {
LoopMochaUtils.stubLoopRequest({
GetDoNotDisturb: function() { return false; }
});
TestUtils.renderIntoDocument(React.createElement(TestComp));
sinon.assert.calledOnce(getAudioBlobStub);
sinon.assert.calledWithExactly(getAudioBlobStub, "failure");
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(false);
});
});
expect(fakeAudio.loop).to.equal(false);});});
describe("loop.shared.mixins.RoomsAudioMixin", function() {
describe("loop.shared.mixins.RoomsAudioMixin", function () {
var comp;
function createTestComponent(initialState) {
var TestComp = React.createClass({
mixins: [loop.shared.mixins.RoomsAudioMixin],
render: function() {
return React.DOM.div();
},
var TestComp = React.createClass({ displayName: "TestComp",
mixins: [loop.shared.mixins.RoomsAudioMixin],
render: function render() {
return React.DOM.div();},
getInitialState: function getInitialState() {
return { roomState: initialState };} });
getInitialState: function() {
return { roomState: initialState };
}
});
var renderedComp = TestUtils.renderIntoDocument(
React.createElement(TestComp));
React.createElement(TestComp));
sandbox.stub(renderedComp, "play");
return renderedComp;
}
return renderedComp;}
beforeEach(function() {
});
it("should play a sound when the local user joins the room", function() {
beforeEach(function () {});
it("should play a sound when the local user joins the room", function () {
comp = createTestComponent(ROOM_STATES.INIT);
comp.setState({ roomState: ROOM_STATES.SESSION_CONNECTED });
sinon.assert.calledOnce(comp.play);
sinon.assert.calledWithExactly(comp.play, "room-joined");
});
sinon.assert.calledWithExactly(comp.play, "room-joined");});
it("should play a sound when another user joins the room", function() {
it("should play a sound when another user joins the room", function () {
comp = createTestComponent(ROOM_STATES.SESSION_CONNECTED);
comp.setState({ roomState: ROOM_STATES.HAS_PARTICIPANTS });
sinon.assert.calledOnce(comp.play);
sinon.assert.calledWithExactly(comp.play, "room-joined-in");
});
sinon.assert.calledWithExactly(comp.play, "room-joined-in");});
it("should play a sound when another user leaves the room", function() {
it("should play a sound when another user leaves the room", function () {
comp = createTestComponent(ROOM_STATES.HAS_PARTICIPANTS);
comp.setState({ roomState: ROOM_STATES.SESSION_CONNECTED });
sinon.assert.calledOnce(comp.play);
sinon.assert.calledWithExactly(comp.play, "room-left");
});
sinon.assert.calledWithExactly(comp.play, "room-left");});
it("should play a sound when the local user leaves the room", function() {
it("should play a sound when the local user leaves the room", function () {
comp = createTestComponent(ROOM_STATES.HAS_PARTICIPANTS);
comp.setState({ roomState: ROOM_STATES.READY });
sinon.assert.calledOnce(comp.play);
sinon.assert.calledWithExactly(comp.play, "room-left");
});
sinon.assert.calledWithExactly(comp.play, "room-left");});
it("should play a sound when if there is a failure", function() {
it("should play a sound when if there is a failure", function () {
comp = createTestComponent(ROOM_STATES.HAS_PARTICIPANTS);
comp.setState({ roomState: ROOM_STATES.FAILED });
sinon.assert.calledOnce(comp.play);
sinon.assert.calledWithExactly(comp.play, "failure");
});
sinon.assert.calledWithExactly(comp.play, "failure");});
it("should play a sound when if the room is full", function() {
it("should play a sound when if the room is full", function () {
comp = createTestComponent(ROOM_STATES.READY);
comp.setState({ roomState: ROOM_STATES.FULL });
sinon.assert.calledOnce(comp.play);
sinon.assert.calledWithExactly(comp.play, "failure");
});
});
});
sinon.assert.calledWithExactly(comp.play, "failure");});});});

View File

@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.store.RemoteCursorStore", function() {
describe("loop.store.RemoteCursorStore", function () {
"use strict";
var expect = chai.expect;
@@ -9,212 +9,208 @@ describe("loop.store.RemoteCursorStore", function() {
var CURSOR_MESSAGE_TYPES = loop.shared.utils.CURSOR_MESSAGE_TYPES;
var sandbox, dispatcher, store, fakeSdkDriver;
beforeEach(function() {
beforeEach(function () {
sandbox = LoopMochaUtils.createSandbox();
LoopMochaUtils.stubLoopRequest({
GetLoopPref: sinon.stub()
});
LoopMochaUtils.stubLoopRequest({
GetLoopPref: sinon.stub() });
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
fakeSdkDriver = {
sendCursorMessage: sinon.stub()
};
fakeSdkDriver = {
sendCursorMessage: sinon.stub() };
store = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: fakeSdkDriver
});
});
afterEach(function() {
store = new loop.store.RemoteCursorStore(dispatcher, {
sdkDriver: fakeSdkDriver });});
afterEach(function () {
sandbox.restore();
LoopMochaUtils.restore();
});
LoopMochaUtils.restore();});
describe("#constructor", function() {
it("should throw an error if sdkDriver is missing", function() {
expect(function() {
new loop.store.RemoteCursorStore(dispatcher, {});
}).to.Throw(/sdkDriver/);
});
it("should add a event listeners", function() {
describe("#constructor", function () {
it("should throw an error if sdkDriver is missing", function () {
expect(function () {
new loop.store.RemoteCursorStore(dispatcher, {});}).
to.Throw(/sdkDriver/);});
it("should add a event listeners", function () {
sandbox.stub(loop, "subscribe");
new loop.store.RemoteCursorStore(dispatcher, { sdkDriver: fakeSdkDriver });
sinon.assert.calledTwice(loop.subscribe);
sinon.assert.calledWith(loop.subscribe, "CursorPositionChange");
sinon.assert.calledWith(loop.subscribe, "CursorClick");
});
});
sinon.assert.calledWith(loop.subscribe, "CursorClick");});});
describe("#cursor position change", function () {
it("should send cursor data through the sdk", function () {
var fakeEvent = {
ratioX: 10,
ratioY: 10 };
describe("#cursor position change", function() {
it("should send cursor data through the sdk", function() {
var fakeEvent = {
ratioX: 10,
ratioY: 10
};
LoopMochaUtils.publish("CursorPositionChange", fakeEvent);
sinon.assert.calledOnce(fakeSdkDriver.sendCursorMessage);
sinon.assert.calledWith(fakeSdkDriver.sendCursorMessage, {
type: CURSOR_MESSAGE_TYPES.POSITION,
ratioX: fakeEvent.ratioX,
ratioY: fakeEvent.ratioY
});
});
});
sinon.assert.calledWith(fakeSdkDriver.sendCursorMessage, {
type: CURSOR_MESSAGE_TYPES.POSITION,
ratioX: fakeEvent.ratioX,
ratioY: fakeEvent.ratioY });});});
describe("#cursor click", function() {
it("should send cursor data through the sdk", function() {
describe("#cursor click", function () {
it("should send cursor data through the sdk", function () {
var fakeClick = true;
LoopMochaUtils.publish("CursorClick", fakeClick);
sinon.assert.calledOnce(fakeSdkDriver.sendCursorMessage);
sinon.assert.calledWith(fakeSdkDriver.sendCursorMessage, {
type: CURSOR_MESSAGE_TYPES.CLICK
});
});
});
sinon.assert.calledWith(fakeSdkDriver.sendCursorMessage, {
type: CURSOR_MESSAGE_TYPES.CLICK });});});
describe("#sendCursorData", function () {
it("should do nothing if not a proper event", function () {
var fakeData = {
ratioX: 10,
ratioY: 10,
type: "not-a-position-event" };
describe("#sendCursorData", function() {
it("should do nothing if not a proper event", function() {
var fakeData = {
ratioX: 10,
ratioY: 10,
type: "not-a-position-event"
};
store.sendCursorData(new sharedActions.SendCursorData(fakeData));
sinon.assert.notCalled(fakeSdkDriver.sendCursorMessage);
});
sinon.assert.notCalled(fakeSdkDriver.sendCursorMessage);});
it("should send cursor data through the sdk", function () {
var fakeData = {
ratioX: 10,
ratioY: 10,
type: CURSOR_MESSAGE_TYPES.POSITION };
it("should send cursor data through the sdk", function() {
var fakeData = {
ratioX: 10,
ratioY: 10,
type: CURSOR_MESSAGE_TYPES.POSITION
};
store.sendCursorData(new sharedActions.SendCursorData(fakeData));
sinon.assert.calledOnce(fakeSdkDriver.sendCursorMessage);
sinon.assert.calledWith(fakeSdkDriver.sendCursorMessage, {
name: "sendCursorData",
type: fakeData.type,
ratioX: fakeData.ratioX,
ratioY: fakeData.ratioY
});
});
});
sinon.assert.calledWith(fakeSdkDriver.sendCursorMessage, {
name: "sendCursorData",
type: fakeData.type,
ratioX: fakeData.ratioX,
ratioY: fakeData.ratioY });});});
describe("#receivedCursorData", function() {
it("should do nothing if not a proper event", function() {
describe("#receivedCursorData", function () {
it("should do nothing if not a proper event", function () {
sandbox.stub(store, "setStoreState");
store.receivedCursorData(new sharedActions.ReceivedCursorData({
ratioX: 10,
ratioY: 10,
type: "not-a-position-event"
}));
store.receivedCursorData(new sharedActions.ReceivedCursorData({
ratioX: 10,
ratioY: 10,
type: "not-a-position-event" }));
sinon.assert.notCalled(store.setStoreState);
});
it("should save the state of the cursor position", function() {
store.receivedCursorData(new sharedActions.ReceivedCursorData({
type: CURSOR_MESSAGE_TYPES.POSITION,
ratioX: 10,
ratioY: 10
}));
sinon.assert.notCalled(store.setStoreState);});
expect(store.getStoreState().remoteCursorPosition).eql({
ratioX: 10,
ratioY: 10
});
});
it("should save the state of the cursor click", function() {
store.receivedCursorData(new sharedActions.ReceivedCursorData({
type: CURSOR_MESSAGE_TYPES.CLICK
}));
it("should save the state of the cursor position", function () {
store.receivedCursorData(new sharedActions.ReceivedCursorData({
type: CURSOR_MESSAGE_TYPES.POSITION,
ratioX: 10,
ratioY: 10 }));
expect(store.getStoreState().remoteCursorClick).eql(true);
});
});
describe("#videoDimensionsChanged", function() {
beforeEach(function() {
store.setStoreState({
realVideoSize: null
});
});
expect(store.getStoreState().remoteCursorPosition).eql({
ratioX: 10,
ratioY: 10 });});
it("should save the state", function() {
store.videoDimensionsChanged(new sharedActions.VideoDimensionsChanged({
isLocal: false,
videoType: "screen",
dimensions: {
height: 10,
width: 10
}
}));
expect(store.getStoreState().realVideoSize).eql({
height: 10,
width: 10
});
});
it("should not save the state if video type is not screen", function() {
store.videoDimensionsChanged(new sharedActions.VideoDimensionsChanged({
isLocal: false,
videoType: "camera",
dimensions: {
height: 10,
width: 10
}
}));
it("should save the state of the cursor click", function () {
store.receivedCursorData(new sharedActions.ReceivedCursorData({
type: CURSOR_MESSAGE_TYPES.CLICK }));
expect(store.getStoreState().realVideoSize).eql(null);
});
});
describe("#videoScreenStreamChanged", function() {
beforeEach(function() {
store.setStoreState({
remoteCursorPosition: {
ratioX: 1,
ratioY: 1
}
});
});
expect(store.getStoreState().remoteCursorClick).eql(true);});});
it("should remove cursor position if screen stream has no video", function() {
store.videoScreenStreamChanged(new sharedActions.VideoScreenStreamChanged({
hasVideo: false
}));
expect(store.getStoreState().remoteCursorPosition).eql(null);
});
it("should not remove cursor position if screen stream has video", function() {
describe("#videoDimensionsChanged", function () {
beforeEach(function () {
store.setStoreState({
realVideoSize: null });});
it("should save the state", function () {
store.videoDimensionsChanged(new sharedActions.VideoDimensionsChanged({
isLocal: false,
videoType: "screen",
dimensions: {
height: 10,
width: 10 } }));
expect(store.getStoreState().realVideoSize).eql({
height: 10,
width: 10 });});
it("should not save the state if video type is not screen", function () {
store.videoDimensionsChanged(new sharedActions.VideoDimensionsChanged({
isLocal: false,
videoType: "camera",
dimensions: {
height: 10,
width: 10 } }));
expect(store.getStoreState().realVideoSize).eql(null);});});
describe("#videoScreenStreamChanged", function () {
beforeEach(function () {
store.setStoreState({
remoteCursorPosition: {
ratioX: 1,
ratioY: 1 } });});
it("should remove cursor position if screen stream has no video", function () {
store.videoScreenStreamChanged(new sharedActions.VideoScreenStreamChanged({
hasVideo: false }));
expect(store.getStoreState().remoteCursorPosition).eql(null);});
it("should not remove cursor position if screen stream has video", function () {
sandbox.stub(store, "setStoreState");
store.videoScreenStreamChanged(new sharedActions.VideoScreenStreamChanged({
hasVideo: true
}));
store.videoScreenStreamChanged(new sharedActions.VideoScreenStreamChanged({
hasVideo: true }));
sinon.assert.notCalled(store.setStoreState);
expect(store.getStoreState().remoteCursorPosition).eql({
ratioX: 1,
ratioY: 1
});
});
});
});
expect(store.getStoreState().remoteCursorPosition).eql({
ratioX: 1,
ratioY: 1 });});});});

View File

@@ -1,24 +1,23 @@
/* 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"; /* 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) {
(function (window) {
"use strict";
if (!window.OT) {
window.OT = {};
}
window.OT = {};}
window.OT.checkSystemRequirements = function() {
return true;
};
window.OT.setLogLevel = function() {};
window.OT.checkSystemRequirements = function () {
return true;};
})(window);
window.OT.setLogLevel = function () {};})(
window);

View File

@@ -1,8 +1,8 @@
/* 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"; /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.store", function() {
describe("loop.store", function () {
"use strict";
var expect = chai.expect;
@@ -11,34 +11,34 @@ describe("loop.store", function() {
var sharedActions = loop.shared.actions;
var TestUtils = React.addons.TestUtils;
beforeEach(function() {
beforeEach(function () {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
});
dispatcher = new loop.Dispatcher();});
afterEach(function() {
sandbox.restore();
});
describe("loop.store.createStore", function() {
it("should create a store constructor", function() {
expect(loop.store.createStore({})).to.be.a("function");
});
afterEach(function () {
sandbox.restore();});
it("should implement Backbone.Events", function() {
expect(loop.store.createStore({}).prototype).to.include.keys(["on", "off"]);
});
describe("Store API", function() {
describe("#constructor", function() {
it("should require a dispatcher", function() {
describe("loop.store.createStore", function () {
it("should create a store constructor", function () {
expect(loop.store.createStore({})).to.be.a("function");});
it("should implement Backbone.Events", function () {
expect(loop.store.createStore({}).prototype).to.include.keys(["on", "off"]);});
describe("Store API", function () {
describe("#constructor", function () {
it("should require a dispatcher", function () {
var TestStore = loop.store.createStore({});
expect(function() {
new TestStore();
}).to.Throw(/required dispatcher/);
});
expect(function () {
new TestStore();}).
to.Throw(/required dispatcher/);});
it("should call initialize() when constructed, if defined", function() {
it("should call initialize() when constructed, if defined", function () {
var initialize = sandbox.spy();
var TestStore = loop.store.createStore({ initialize: initialize });
var options = { fake: true };
@@ -46,51 +46,51 @@ describe("loop.store", function() {
new TestStore(dispatcher, options);
sinon.assert.calledOnce(initialize);
sinon.assert.calledWithExactly(initialize, options);
});
sinon.assert.calledWithExactly(initialize, options);});
it("should register actions", function() {
it("should register actions", function () {
sandbox.stub(dispatcher, "register");
var TestStore = loop.store.createStore({
actions: ["a", "b"],
a: function() {},
b: function() {}
});
var TestStore = loop.store.createStore({
actions: ["a", "b"],
a: function a() {},
b: function b() {} });
var store = new TestStore(dispatcher);
sinon.assert.calledOnce(dispatcher.register);
sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]);
});
sinon.assert.calledWithExactly(dispatcher.register, store, ["a", "b"]);});
it("should throw if a registered action isn't implemented", function() {
var TestStore = loop.store.createStore({
actions: ["a", "b"],
a: function() {} // missing b
it("should throw if a registered action isn't implemented", function () {
var TestStore = loop.store.createStore({
actions: ["a", "b"],
a: function a() {} // missing b
});
expect(function() {
new TestStore(dispatcher);
}).to.Throw(/should implement an action handler for b/);
});
});
expect(function () {
new TestStore(dispatcher);}).
to.Throw(/should implement an action handler for b/);});});
describe("#getInitialStoreState", function () {
it("should set initial store state if provided", function () {
var TestStore = loop.store.createStore({
getInitialStoreState: function getInitialStoreState() {
return { foo: "bar" };} });
describe("#getInitialStoreState", function() {
it("should set initial store state if provided", function() {
var TestStore = loop.store.createStore({
getInitialStoreState: function() {
return { foo: "bar" };
}
});
var store = new TestStore(dispatcher);
expect(store.getStoreState()).eql({ foo: "bar" });
});
});
expect(store.getStoreState()).eql({ foo: "bar" });});});
describe("#dispatchAction", function() {
it("should dispatch an action", function() {
describe("#dispatchAction", function () {
it("should dispatch an action", function () {
sandbox.stub(dispatcher, "dispatch");
var TestStore = loop.store.createStore({});
var TestAction = sharedActions.Action.define("TestAction", {});
@@ -100,98 +100,98 @@ describe("loop.store", function() {
store.dispatchAction(action);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, action);
});
});
sinon.assert.calledWithExactly(dispatcher.dispatch, action);});});
describe("#getStoreState", function() {
describe("#getStoreState", function () {
var TestStore = loop.store.createStore({});
var store;
beforeEach(function() {
beforeEach(function () {
store = new TestStore(dispatcher);
store.setStoreState({ foo: "bar", bar: "baz" });
});
store.setStoreState({ foo: "bar", bar: "baz" });});
it("should retrieve the whole state by default", function() {
expect(store.getStoreState()).eql({ foo: "bar", bar: "baz" });
});
it("should retrieve a given property state", function() {
expect(store.getStoreState("bar")).eql("baz");
});
});
it("should retrieve the whole state by default", function () {
expect(store.getStoreState()).eql({ foo: "bar", bar: "baz" });});
describe("#setStoreState", function() {
it("should retrieve a given property state", function () {
expect(store.getStoreState("bar")).eql("baz");});});
describe("#setStoreState", function () {
var TestStore = loop.store.createStore({});
var store;
beforeEach(function() {
beforeEach(function () {
store = new TestStore(dispatcher);
store.setStoreState({ foo: "bar" });
});
store.setStoreState({ foo: "bar" });});
it("should update store state data", function() {
it("should update store state data", function () {
store.setStoreState({ foo: "baz" });
expect(store.getStoreState("foo")).eql("baz");
});
expect(store.getStoreState("foo")).eql("baz");});
it("should trigger a `change` event", function(done) {
store.once("change", function() {
done();
});
store.setStoreState({ foo: "baz" });
});
it("should trigger a `change` event", function (done) {
store.once("change", function () {
done();});
it("should trigger a `change:<prop>` event", function(done) {
store.once("change:foo", function() {
done();
});
store.setStoreState({ foo: "baz" });
});
});
});
});
store.setStoreState({ foo: "baz" });});
describe("loop.store.StoreMixin", function() {
it("should trigger a `change:<prop>` event", function (done) {
store.once("change:foo", function () {
done();});
store.setStoreState({ foo: "baz" });});});});});
describe("loop.store.StoreMixin", function () {
var view1, view2, store, storeClass, testComp;
beforeEach(function() {
beforeEach(function () {
storeClass = loop.store.createStore({});
store = new storeClass(dispatcher);
loop.store.StoreMixin.register({ store: store });
testComp = React.createClass({
mixins: [loop.store.StoreMixin("store")],
render: function() {
return React.DOM.div();
}
});
testComp = React.createClass({ displayName: "testComp",
mixins: [loop.store.StoreMixin("store")],
render: function render() {
return React.DOM.div();} });
view1 = TestUtils.renderIntoDocument(React.createElement(testComp));
});
it("should update the state when the store changes", function() {
view1 = TestUtils.renderIntoDocument(React.createElement(testComp));});
it("should update the state when the store changes", function () {
store.setStoreState({ test: true });
expect(view1.state).eql({ test: true });
});
expect(view1.state).eql({ test: true });});
it("should stop listening to state changes", function() {
it("should stop listening to state changes", function () {
// There's no easy way in TestUtils to unmount, so simulate it.
view1.componentWillUnmount();
store.setStoreState({ test2: true });
expect(view1.state).eql(null);
});
expect(view1.state).eql(null);});
it("should not stop listening to state changes on other components", function() {
it("should not stop listening to state changes on other components", function () {
view2 = TestUtils.renderIntoDocument(React.createElement(testComp));
// There's no easy way in TestUtils to unmount, so simulate it.
@@ -199,7 +199,4 @@ describe("loop.store", function() {
store.setStoreState({ test3: true });
expect(view2.state).eql({ test3: true });
});
});
});
expect(view2.state).eql({ test3: true });});});});

View File

@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.store.TextChatStore", function() {
describe("loop.store.TextChatStore", function () {
"use strict";
var expect = chai.expect;
@@ -11,354 +11,340 @@ describe("loop.store.TextChatStore", function() {
var dispatcher, fakeSdkDriver, sandbox, store;
beforeEach(function() {
beforeEach(function () {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
fakeSdkDriver = {
sendTextChatMessage: sinon.stub()
};
fakeSdkDriver = {
sendTextChatMessage: sinon.stub() };
store = new loop.store.TextChatStore(dispatcher, {
sdkDriver: fakeSdkDriver });
store = new loop.store.TextChatStore(dispatcher, {
sdkDriver: fakeSdkDriver
});
sandbox.stub(window, "dispatchEvent");
sandbox.stub(window, "CustomEvent", function(name) {
this.name = name;
});
});
sandbox.stub(window, "CustomEvent", function (name) {
this.name = name;});});
afterEach(function() {
sandbox.restore();
});
describe("#dataChannelsAvailable", function() {
it("should set textChatEnabled to the supplied state", function() {
store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
available: true
}));
expect(store.getStoreState("textChatEnabled")).eql(true);
});
afterEach(function () {
sandbox.restore();});
describe("#dataChannelsAvailable", function () {
it("should set textChatEnabled to the supplied state", function () {
store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
available: true }));
expect(store.getStoreState("textChatEnabled")).eql(true);});
it("should dispatch a LoopChatEnabled event", function () {
store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
available: true }));
it("should dispatch a LoopChatEnabled event", function() {
store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
available: true
}));
sinon.assert.calledOnce(window.dispatchEvent);
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatEnabled"));
});
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatEnabled"));});
it("should not dispatch a LoopChatEnabled event if available is false", function() {
store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
available: false
}));
sinon.assert.notCalled(window.dispatchEvent);
});
});
it("should not dispatch a LoopChatEnabled event if available is false", function () {
store.dataChannelsAvailable(new sharedActions.DataChannelsAvailable({
available: false }));
describe("#receivedTextChatMessage", function() {
it("should add the message to the list", function() {
sinon.assert.notCalled(window.dispatchEvent);});});
describe("#receivedTextChatMessage", function () {
it("should add the message to the list", function () {
var message = "Hello!";
store.receivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: message,
extraData: undefined,
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z"
});
store.receivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: message,
extraData: undefined,
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z" });
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.TEXT,
message: message,
extraData: undefined,
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z"
}]);
});
it("should add the context tile to the list", function() {
store.receivedTextChatMessage({
type: CHAT_MESSAGE_TYPES.SENT,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com"
},
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z"
});
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.TEXT,
message: message,
extraData: undefined,
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z" }]);});
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com"
},
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z"
}]);
});
it("should not add messages for unknown content types", function() {
store.receivedTextChatMessage({
contentType: "invalid type",
message: "Hi"
});
expect(store.getStoreState("messageList").length).eql(0);
});
it("should add the context tile to the list", function () {
store.receivedTextChatMessage({
type: CHAT_MESSAGE_TYPES.SENT,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com" },
it("should dispatch a LoopChatMessageAppended event", function() {
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z" });
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com" },
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "1970-01-01T00:00:00.000Z" }]);});
it("should not add messages for unknown content types", function () {
store.receivedTextChatMessage({
contentType: "invalid type",
message: "Hi" });
expect(store.getStoreState("messageList").length).eql(0);});
it("should dispatch a LoopChatMessageAppended event", function () {
store.setStoreState({ textChatEnabled: true });
store.receivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!"
});
store.receivedTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!" });
sinon.assert.calledOnce(window.dispatchEvent);
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatMessageAppended"));
});
});
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatMessageAppended"));});});
describe("#sendTextChatMessage", function () {
it("should send the message", function () {
var messageData = {
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Yes, that's what this is called." };
describe("#sendTextChatMessage", function() {
it("should send the message", function() {
var messageData = {
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Yes, that's what this is called."
};
store.sendTextChatMessage(messageData);
sinon.assert.calledOnce(fakeSdkDriver.sendTextChatMessage);
sinon.assert.calledWithExactly(fakeSdkDriver.sendTextChatMessage, messageData);
});
sinon.assert.calledWithExactly(fakeSdkDriver.sendTextChatMessage, messageData);});
it("should add the message to the list", function () {
var messageData = {
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "It's awesome!",
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "2015-06-24T23:58:53.848Z" };
it("should add the message to the list", function() {
var messageData = {
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "It's awesome!",
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "2015-06-24T23:58:53.848Z"
};
store.sendTextChatMessage(messageData);
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SENT,
contentType: messageData.contentType,
message: messageData.message,
extraData: undefined,
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "2015-06-24T23:58:53.848Z"
}]);
});
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SENT,
contentType: messageData.contentType,
message: messageData.message,
extraData: undefined,
sentTimestamp: "2015-06-24T23:58:53.848Z",
receivedTimestamp: "2015-06-24T23:58:53.848Z" }]);});
it("should dipatch a LoopChatMessageAppended event", function() {
it("should dipatch a LoopChatMessageAppended event", function () {
store.setStoreState({ textChatEnabled: true });
store.sendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!"
});
store.sendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!" });
sinon.assert.calledOnce(window.dispatchEvent);
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatMessageAppended"));
});
});
sinon.assert.calledWithExactly(window.dispatchEvent,
new CustomEvent("LoopChatMessageAppended"));});});
describe("#updateRoomInfo", function() {
it("should add the room name to the list", function() {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "Let's share!",
roomUrl: "fake"
}));
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
message: "Let's share!",
extraData: undefined,
sentTimestamp: undefined,
receivedTimestamp: undefined
}]);
});
it("should add the context to the list", function() {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "Let's share!",
roomUrl: "fake",
roomContextUrls: [{
description: "A wonderful event",
location: "http://wonderful.invalid",
thumbnail: "fake"
}]
}));
describe("#updateRoomInfo", function () {
it("should add the context to the list", function () {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "Let's share!",
roomUrl: "fake",
roomContextUrls: [{
description: "A wonderful event",
location: "http://wonderful.invalid",
thumbnail: "fake" }] }));
expect(store.getStoreState("messageList")).eql([
{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
message: "Let's share!",
extraData: undefined,
sentTimestamp: undefined,
receivedTimestamp: undefined
}, {
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: "A wonderful event",
sentTimestamp: undefined,
receivedTimestamp: undefined,
extraData: {
location: "http://wonderful.invalid",
thumbnail: "fake"
}
}
]);
});
{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: "A wonderful event",
sentTimestamp: undefined,
receivedTimestamp: undefined,
extraData: {
location: "http://wonderful.invalid",
thumbnail: "fake" } }]);});
it("should not add more than one context message", function() {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomUrl: "fake",
roomContextUrls: [{
description: "A wonderful event",
location: "http://wonderful.invalid",
thumbnail: "fake"
}]
}));
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: "A wonderful event",
sentTimestamp: undefined,
receivedTimestamp: undefined,
extraData: {
location: "http://wonderful.invalid",
thumbnail: "fake"
}
}]);
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomUrl: "fake",
roomContextUrls: [{
description: "A wonderful event2",
location: "http://wonderful.invalid2",
thumbnail: "fake2"
}]
}));
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: "A wonderful event2",
sentTimestamp: undefined,
receivedTimestamp: undefined,
extraData: {
location: "http://wonderful.invalid2",
thumbnail: "fake2"
}
}]);
});
it("should not dispatch a LoopChatMessageAppended event", function() {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "Let's share!",
roomUrl: "fake"
}));
it("should not add more than one context message", function () {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomUrl: "fake",
roomContextUrls: [{
description: "A wonderful event",
location: "http://wonderful.invalid",
thumbnail: "fake" }] }));
sinon.assert.notCalled(window.dispatchEvent);
});
});
describe("#updateRoomContext", function() {
beforeEach(function() {
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: "A wonderful event",
sentTimestamp: undefined,
receivedTimestamp: undefined,
extraData: {
location: "http://wonderful.invalid",
thumbnail: "fake" } }]);
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomUrl: "fake",
roomContextUrls: [{
description: "A wonderful event2",
location: "http://wonderful.invalid2",
thumbnail: "fake2" }] }));
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SPECIAL,
contentType: CHAT_CONTENT_TYPES.CONTEXT,
message: "A wonderful event2",
sentTimestamp: undefined,
receivedTimestamp: undefined,
extraData: {
location: "http://wonderful.invalid2",
thumbnail: "fake2" } }]);});
it("should not dispatch a LoopChatMessageAppended event", function () {
store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "Let's share!",
roomUrl: "fake" }));
sinon.assert.notCalled(window.dispatchEvent);});});
describe("#updateRoomContext", function () {
beforeEach(function () {
store.setStoreState({ messageList: [] });
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com",
roomToken: "fakeRoomToken"
}));
});
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com",
roomToken: "fakeRoomToken" }));});
it("should add the room context to the list", function() {
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SENT,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com"
},
sentTimestamp: "1970-01-01T00:00:00.000Z",
receivedTimestamp: undefined
}]);
});
it("should not add the room context if the last tile has the same domain", function() {
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com/test/same_domain",
roomToken: "fakeRoomToken"
}));
expect(store.getStoreState("messageList").length).eql(1);
});
it("should add the room context to the list", function () {
expect(store.getStoreState("messageList")).eql([{
type: CHAT_MESSAGE_TYPES.SENT,
contentType: CHAT_CONTENT_TYPES.CONTEXT_TILE,
message: "fake",
extraData: {
roomToken: "fakeRoomToken",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com" },
it("should add the room context if the last tile has not the same domain", function() {
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.myfakeurl.com",
roomToken: "fakeRoomToken"
}));
sentTimestamp: "1970-01-01T00:00:00.000Z",
receivedTimestamp: undefined }]);});
expect(store.getStoreState("messageList").length).eql(2);
});
});
describe("#remotePeerDisconnected", function() {
it("should append the right message when peer disconnected cleanly", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true
}));
it("should not add the room context if the last tile has the same domain", function () {
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.fakeurl.com/test/same_domain",
roomToken: "fakeRoomToken" }));
expect(store.getStoreState("messageList").length).eql(1);});
it("should add the room context if the last tile has not the same domain", function () {
store.updateRoomContext(new sharedActions.UpdateRoomContext({
newRoomDescription: "fake",
newRoomThumbnail: "favicon",
newRoomURL: "https://www.myfakeurl.com",
roomToken: "fakeRoomToken" }));
expect(store.getStoreState("messageList").length).eql(2);});});
describe("#remotePeerDisconnected", function () {
it("should append the right message when peer disconnected cleanly", function () {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: true }));
expect(store.getStoreState("messageList").length).eql(1);
expect(store.getStoreState("messageList")[0].contentType).eql(
CHAT_CONTENT_TYPES.NOTIFICATION
);
expect(store.getStoreState("messageList")[0].message).eql("peer_left_session");
});
CHAT_CONTENT_TYPES.NOTIFICATION);
expect(store.getStoreState("messageList")[0].message).eql("peer_left_session");});
it("should append the right message when peer disconnected unexpectedly", function () {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: false }));
it("should append the right message when peer disconnected unexpectedly", function() {
store.remotePeerDisconnected(new sharedActions.RemotePeerDisconnected({
peerHungup: false
}));
expect(store.getStoreState("messageList").length).eql(1);
expect(store.getStoreState("messageList")[0].contentType).eql(
CHAT_CONTENT_TYPES.NOTIFICATION
);
expect(store.getStoreState("messageList")[0].message).eql("peer_unexpected_quit");
});
});
});
CHAT_CONTENT_TYPES.NOTIFICATION);
expect(store.getStoreState("messageList")[0].message).eql("peer_unexpected_quit");});});
describe("#remotePeerConnected", function () {
it("should append the right message when peer connected", function () {
store.remotePeerConnected(new sharedActions.RemotePeerConnected());
expect(store.getStoreState("messageList").length).eql(1);
expect(store.getStoreState("messageList")[0].contentType).eql(
CHAT_CONTENT_TYPES.NOTIFICATION);
expect(store.getStoreState("messageList")[0].message).eql("peer_join_session");});});});

View File

@@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict"; /* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("Validator", function() {
describe("Validator", function () {
"use strict";
var expect = chai.expect;
@@ -9,79 +9,76 @@ describe("Validator", function() {
// test helpers
function create(dependencies, values) {
var validator = new loop.validate.Validator(dependencies);
return validator.validate.bind(validator, values);
}
return validator.validate.bind(validator, values);}
// test types
function X() {}
function Y() {}
describe("#validate", function() {
describe("#validate", function () {
function RTCSessionDescription() {}
var rtcsd;
beforeEach(function() {
rtcsd = new RTCSessionDescription();
});
beforeEach(function () {
rtcsd = new RTCSessionDescription();});
it("should check for a single required dependency when no option passed",
function() {
expect(create({ x: Number }, {}))
.to.Throw(TypeError, /missing required x$/);
});
it("should check for a missing required dependency, undefined passed",
function() {
expect(create({ x: Number }, { x: undefined }))
.to.Throw(TypeError, /missing required x$/);
});
it("should check for a single required dependency when no option passed",
function () {
expect(create({ x: Number }, {})).
to.Throw(TypeError, /missing required x$/);});
it("should check for multiple missing required dependencies", function() {
expect(create({ x: Number, y: String }, {}))
.to.Throw(TypeError, /missing required x, y$/);
});
it("should check for required dependency types", function() {
it("should check for a missing required dependency, undefined passed",
function () {
expect(create({ x: Number }, { x: undefined })).
to.Throw(TypeError, /missing required x$/);});
it("should check for multiple missing required dependencies", function () {
expect(create({ x: Number, y: String }, {})).
to.Throw(TypeError, /missing required x, y$/);});
it("should check for required dependency types", function () {
expect(create({ x: Number }, { x: "woops" })).to.Throw(
TypeError, /invalid dependency: x; expected Number, got String$/);
});
TypeError, /invalid dependency: x; expected Number, got String$/);});
it("should check for a dependency to match at least one of passed types",
function() {
expect(create({ x: [X, Y] }, { x: 42 })).to.Throw(
TypeError, /invalid dependency: x; expected X, Y, got Number$/);
expect(create({ x: [X, Y] }, { x: new Y() })).to.not.Throw();
});
it("should skip type check if required dependency type is undefined",
function() {
expect(create({ x: undefined }, { x: /whatever/ })).not.to.Throw();
});
it("should check for a dependency to match at least one of passed types",
function () {
expect(create({ x: [X, Y] }, { x: 42 })).to.Throw(
TypeError, /invalid dependency: x; expected X, Y, got Number$/);
expect(create({ x: [X, Y] }, { x: new Y() })).to.not.Throw();});
it("should check for a String dependency", function() {
it("should skip type check if required dependency type is undefined",
function () {
expect(create({ x: undefined }, { x: /whatever/ })).not.to.Throw();});
it("should check for a String dependency", function () {
expect(create({ foo: String }, { foo: 42 })).to.Throw(
TypeError, /invalid dependency: foo/);
});
TypeError, /invalid dependency: foo/);});
it("should check for a Number dependency", function() {
it("should check for a Number dependency", function () {
expect(create({ foo: Number }, { foo: "x" })).to.Throw(
TypeError, /invalid dependency: foo/);
});
TypeError, /invalid dependency: foo/);});
it("should check for a custom constructor dependency", function() {
it("should check for a custom constructor dependency", function () {
expect(create({ foo: X }, { foo: null })).to.Throw(
TypeError, /invalid dependency: foo; expected X, got null$/);
});
TypeError, /invalid dependency: foo; expected X, got null$/);});
it("should check for a native constructor dependency", function() {
expect(create({ foo: rtcsd }, { foo: "x" }))
.to.Throw(TypeError,
/invalid dependency: foo; expected RTCSessionDescription/);
});
it("should check for a null dependency", function() {
it("should check for a native constructor dependency", function () {
expect(create({ foo: rtcsd }, { foo: "x" })).
to.Throw(TypeError,
/invalid dependency: foo; expected RTCSessionDescription/);});
it("should check for a null dependency", function () {
expect(create({ foo: null }, { foo: "x" })).to.Throw(
TypeError, /invalid dependency: foo; expected null, got String$/);
});
});
});
TypeError, /invalid dependency: foo; expected null, got String$/);});});});

View File

@@ -0,0 +1,42 @@
/**
* ReactDOMServer v15.0.2
*
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
// Based off https://github.com/ForbesLindesay/umd/blob/master/template.js
;(function(f) {
// CommonJS
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f(require('react'));
// RequireJS
} else if (typeof define === "function" && define.amd) {
define(['react'], f);
// <script>
} else {
var g;
if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
// works providing we're not in "use strict";
// needed for Java 8 Nashorn
// see https://github.com/facebook/react/issues/3037
g = this;
}
g.ReactDOMServer = f(g.React);
}
})(function(React) {
return React.__SECRET_DOM_SERVER_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
});

View File

@@ -0,0 +1,12 @@
/**
* ReactDOM v15.0.2
*
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e(require("react"));else if("function"==typeof define&&define.amd)define(["react"],e);else{var f;f="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,f.ReactDOM=e(f.React)}}(function(e){return e.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED});

View File

@@ -0,0 +1,42 @@
/**
* ReactDOM v15.0.2
*
* Copyright 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
// Based off https://github.com/ForbesLindesay/umd/blob/master/template.js
;(function(f) {
// CommonJS
if (typeof exports === "object" && typeof module !== "undefined") {
module.exports = f(require('react'));
// RequireJS
} else if (typeof define === "function" && define.amd) {
define(['react'], f);
// <script>
} else {
var g;
if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
// works providing we're not in "use strict";
// needed for Java 8 Nashorn
// see https://github.com/facebook/react/issues/3037
g = this;
}
g.ReactDOM = f(g.React);
}
})(function(React) {
return React.__SECRET_DOM_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
});

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,10 +4,11 @@
# Panel Strings
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
loopMenuItem_label=Aniciar una conversación…
loopMenuItem_accesskey=A
## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
## These are displayed together at the top of the panel when a user is needed to
@@ -15,112 +16,211 @@
## and this is displayed in slightly larger font. Please arrange as necessary for
## your locale.
## {{clientShortname2}} will be replaced by the brand name for either string.
sign_in_again_title_line_one=Volvi tentalo pa
sign_in_again_title_line_two2=siguir usando {{clientShortname2}}, por favor
sign_in_again_button=Aniciar sesión
## LOCALIZATION_NOTE(sign_in_again_use_as_guest_button2): {{clientSuperShortname}}
## will be replaced by the super short brandname.
sign_in_again_use_as_guest_button2=Usar {{clientSuperShortname}} como Convidáu
panel_browse_with_friend_button=Restolar esta páxina con un collaciu
panel_disconnect_button=Desconeutase
## LOCALIZATION_NOTE(first_time_experience_subheading2): Message inviting the
## LOCALIZATION_NOTE(first_time_experience_subheading2, first_time_experience_subheading_button_above): Message inviting the
## user to create his or her first conversation.
first_time_experience_subheading2=Primi'l botón de Hello pa restolar páxines web con un collaciu.
first_time_experience_subheading_button_above=Primi'l botón d'enriba pa restolar páxines web con un collaciu.
## LOCALIZATION_NOTE(first_time_experience_content): Message describing
## LOCALIZATION_NOTE(first_time_experience_content, first_time_experience_content2): Message describing
## ways to use Hello project.
first_time_experience_content=Úsalu pa planiar, tabayar, rir en compaña.
first_time_experience_content2=Úsalu pa facer coses: planiar, rir, trabayar en compaña.
first_time_experience_button_label2=Mira cómo furrula
## First Time Experience Slides
fte_slide_1_title=Restola páxines web con un collaciu
## LOCALIZATION_NOTE(fte_slide_1_copy): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_1_copy=Si tas planiando un viaxe o mercar un regalu, {{clientShortname2}} déxate facer decisiones más rápides en tiempu real.
fte_slide_2_title2=Fechu pa compartir la web
## LOCALIZATION_NOTE(fte_slide_2_copy2): {{clientShortname2}}
## will be replaced by the short name 2.
fte_slide_2_copy2=Agora al convidar un collaciu a una sesión, {{clientShortname2}} compartirá automáticamente cualesquier páxina web que teas viendo. Plania. Merca. Decidi. En compaña.
fte_slide_3_title=Convida un collaciu unviándo-y un enllaz
## LOCALIZATION_NOTE(fte_slide_3_copy): {{clientSuperShortname}}
## will be replaced by the super short brand name.
fte_slide_3_copy={{clientSuperShortname}} furrula cola mayoría de restoladores. Nun se precisen cuentes y toos conéutense de baldre.
## LOCALIZATION_NOTE(fte_slide_4_title): {{clientSuperShortname}}
## will be replaced by the super short brand name.
fte_slide_4_title=Alcuentra l'iconu de {{clientSuperShortname}} pa entamar
## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
## will be replaced by the brand short name.
fte_slide_4_copy=Namái alcuentres una páxina que quieras discutir, primi l'iconu en {{brandShortname}} pa crear un enllaz. ¡Dempués únvia-ylu al to collaciu como meyor te paeza!
invite_header_text_bold2=¡Convida un collaciu a xunise a ti!
invite_header_text4=Comparti esti enllaz pa qu'asina podáis entamar a restolar xuntos la web.
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
## an iconic button for the invite view.
# Status text
invite_copy_link_button=Copiar enllaz
invite_copied_link_button=¡Copiáu!
invite_email_link_button=Unviar enllaz per corréu
invite_facebook_button3=Facebook
invite_your_link=El to enllaz:
# Error bars
## LOCALIZATION NOTE(session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
## These may be displayed at the top of the panel.
session_expired_error_description=Caducó la sesión. Yá nun furrularán toles URLs qu'enantes se crearen o compartieren.
could_not_authenticate=Nun pudo autenticase
password_changed_question=¿Camudesti la to contraseña?
try_again_later=Volvi tentalo más sero, por favor
could_not_connect=Nun pudo coneutase col sirvidor
check_internet_connection=Comprueba la to conexón d'internet, por favor
login_expired=Caducó'l to aniciu de sesión
service_not_available=Serviciu non disponible nesti momentu
problem_accessing_account=Hebo un fallu accediendo a la to cuenta
## LOCALIZATION NOTE(retry_button): Displayed when there is an error to retry
## the appropriate action.
retry_button=Retentar
share_email_subject7=La to invitación pa restolar la web en compaña
## LOCALIZATION NOTE (share_email_body7): In this item, don't translate the
## part between {{..}} and leave the \n\n part alone
share_email_body7=Un collaciu ta esperándote en Firefox Hello. Primi l'enllaz pa coneutate y restolar xuntos la web: {{callUrl}}
## LOCALIZATION NOTE (share_email_body_context3): In this item, don't translate
## the part between {{..}} and leave the \n\n part alone.
share_email_body_context3=Un collaciu ta esperándote en Firefox Hello. Primi l'enllaz pa coneutate y restolar xuntos {{title}}: {{callUrl}}
## LOCALIZATION NOTE (share_email_footer2): Common footer content for both email types
share_email_footer2=\n\n____________\nFirefox Hello Déxate restolar la web colos tos collacios. Úsalu cuando quieras facer coses: planiar, trabayar, rir en compaña. Depriendi más en http://www.firefox.com/hello
## LOCALIZATION NOTE (share_tweeet): In this item, don't translate the part
## between {{..}}. Please keep the text below 117 characters to make sure it fits
## in a tweet.
share_add_service_button=Amestar un serviciu
## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Copiar enllaz
email_link_menuitem=Unviar enllaz per corréu
edit_name_menuitem=Editar nome
delete_conversation_menuitem2=Desaniciar
panel_footer_signin_or_signup_link=Aniciar sesión o rexistrase
settings_menu_item_account=Cuenta
settings_menu_item_settings=Axustes
settings_menu_item_signout=Zarrar sesión
settings_menu_item_signin=Aniciar sesión
settings_menu_item_turnnotificationson=Activar avisos
settings_menu_item_turnnotificationsoff=Desactivar avisos
settings_menu_item_feedback=Unviar feedback
settings_menu_button_tooltip=Axustes
# Conversation Window Strings
initiate_call_button_label2=Ready to start your conversation?
hangup_button_title=Hang up
hangup_button_caption2=Exit
initiate_call_button_label2=¿Preparáu p'aniciar la to conversación?
incoming_call_title2=Solicitú de conversación
incoming_call_block_button=Bloquiar
hangup_button_title=Colingar
hangup_button_caption2=Colar
## LOCALIZATION NOTE (call_with_contact_title): The title displayed
## when calling a contact. Don't translate the part between {{..}} because
## this will be replaced by the contact's name.
call_with_contact_title=Conversation with {{incomingCallIdentity}}
call_with_contact_title=Conversación con {{contactName}}
# Outgoing conversation
outgoing_call_title=Start conversation?
initiate_audio_video_call_button2=Start
initiate_audio_video_call_tooltip2=Start a video conversation
initiate_audio_call_button2=Voice conversation
outgoing_call_title=¿Aniciar conversación?
initiate_audio_video_call_button2=Aniciar
initiate_audio_video_call_tooltip2=Anicia una conversación de videu
initiate_audio_call_button2=Conversación de voz
peer_ended_conversation2=The person you were calling has ended the conversation.
restart_call=Rejoin
peer_ended_conversation2=Finó la conversación la persona a la que tas llamando.
restart_call=Rexunise
## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
## contact is offline.
contact_offline_title=La persona nun ta en llinia
## LOCALIZATION NOTE (call_timeout_notification_text): Title which is displayed
## when the call didn't go through.
call_timeout_notification_text=Your call did not go through.
## LOCALIZATION NOTE (cancel_button):
## This button is displayed when a call has failed.
cancel_button=Encaboxar
rejoin_button=Rexunise a la conversación
network_disconnected=The network connection terminated abruptly.
connection_error_see_console_notification=Call failed; see console for details.
cannot_start_call_session_not_ready=Nun pue aniciase la llamada, la sesión nun ta preparada.
network_disconnected=La conexón de rede finó de sutaque.
connection_error_see_console_notification=Falló la llamada, mira la consola pa detalles.
no_media_failure_message=Nun s'alcontro cámara o micrófonu dalos.
ice_failure_message=Falló la conexón. Quiciabes el to tornafuéu tea bloquiando les llamaes.
## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
## parts between {{..}} because these will be replaced with links with the labels
## from legal_text_tos and legal_text_privacy. clientShortname will be replaced
## by the brand name.
legal_text_and_links3=Usando {{clientShortname}} tas acordies colos {{terms_of_use}} y {{privacy_notice}}.
legal_text_tos=Términos d'usu
legal_text_privacy=Avisu de privacidá
## LOCALIZATION NOTE (powered_by_beforeLogo, powered_by_afterLogo):
## These 2 strings are displayed before and after a 'Telefonica'
## logo.
powered_by_beforeLogo=Cola potencia de
powered_by_afterLogo=
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
feedback_rejoin_button=Rejoin
feedback_rejoin_button=Rexunise
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
feedback_report_user_button=Informar usuariu
feedback_window_heading=¿Cómo foi la to conversación?
feedback_request_button=Dexar feedback
tour_label=Tour
tour_label=Percorríu
rooms_list_recently_browsed2=Restolao apocayá
rooms_list_currently_browsing2=Anguaño restolando
rooms_signout_alert=Zarraránse les conversaciones abiertes
room_name_untitled_page=Páxina ensin títulu
## LOCALIZATION NOTE (door_hanger_return, door_hanger_prompt_name, door_hanger_button): Dialog message on leaving conversation
door_hanger_bye=¡Ta llueu!
door_hanger_return2=Pues tornar a esta sesión compartida en cualesquier momentu pente'l panel de Hello. ¿Prestaríate da-y un nome que seya cenciellu de recordar?
door_hanger_current=Nome actual:
door_hanger_button2=¡Val!
# Infobar strings
infobar_screenshare_no_guest_message=Namái se xuna'l to collaciu, sedrá a ver cualesquier llingüeta que primas.
infobar_screenshare_browser_message2=Tas compartiendo les tos llingüetes. Los tos collacios verán cualesquier llingüeta na que primas
infobar_screenshare_browser_message3=Agora tas compartiendo les tos llingüetes. El to collaciu verá cualesquier llingüeta na que primas.
infobar_screenshare_stop_sharing_message2=Yá nun tas compartiendo les tos llingüetes.
infobar_screenshare_stop_no_guest_message=Paresti de compartir les tos llingüetes. Al xunise un collaciu de to, nun sedrá a ver nada fasta que reanicies la compartición.
infobar_button_restart_label2=Reaniciar compartición
infobar_button_restart_accesskey=R
infobar_button_stop_label2=Parar de compartir
infobar_button_stop_accesskey=P
infobar_button_disconnect_label=Desconeutase
infobar_button_disconnect_accesskey=D
# Context in conversation strings
# Copy panel strings
## LOCALIZATION NOTE(no_conversations_message_heading2): Title shown when user
## has no conversations available.
## LOCALIZATION NOTE(no_conversations_start_message2): Subheading inviting the
## user to start a new conversation.
copy_panel_message=¿Precises compartir esta páxina web? Comparti la so llingüeta con un collaciu.
copy_panel_dont_show_again_label=Nun amosar esto de nueves
copy_panel_cancel_button_label=Agora non
copy_panel_accept_button_label=Sí, amuésame cómo
# E10s not supported strings
e10s_not_supported_button_label=Llanzar ventana nueva
e10s_not_supported_subheading={{brandShortname}} nun furrula nuna ventana multi-procesu.
# 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/.
@@ -128,39 +228,46 @@ tour_label=Tour
## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
# Text chat strings
chat_textbox_placeholder=Type here
chat_textbox_placeholder=Teclexa equí
## LOCALIZATION NOTE(clientShortname2): This should not be localized and
## should remain "Firefox Hello" for all locales.
clientShortname2=Firefox Hello
conversation_has_ended=Your conversation has ended.
conversation_has_ended=La to conversación finó.
generic_failure_message=Tamos teniendo dificultaes téuniques…
generic_failure_no_reason2=Would you like to try again?
generic_failure_no_reason2=¿Prestaríate tentalo de nueves?
help_label=Help
help_label=Ayuda
mute_local_audio_button_title=Mute your audio
unmute_local_audio_button_title=Unmute your audio
mute_local_video_button_title2=Deshabilitar videu
unmute_local_video_button_title2=Habilitar videu
## LOCALIZATION NOTE (retry_call_button):
## This button is displayed when a call has failed.
retry_call_button=Retry
retry_call_button=Retentar
rooms_leave_button_label=Leave
rooms_leave_button_label=Colar
rooms_panel_title=Choose a conversation or start a new one
rooms_panel_title=Escueyi una conversación o entama una nueva
rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
rooms_room_full_label=There are already two people in this conversation.
rooms_room_join_label=Join the conversation
rooms_room_joined_label=Someone has joined the conversation!
rooms_room_full_call_to_action_label=Depriendi más tocante a {{clientShortname}} »
rooms_room_full_call_to_action_nonFx_label=Baxa {{brandShortname}} p'aniciar la de to
rooms_room_full_label=Yá hai dos persones nesta conversación.
rooms_room_join_label=Xúnite a la conversación
rooms_room_joined_owner_connected_label2=Agora'l to collaciu ta coneutáu y sedrá a ver les tos llingüetes.
rooms_room_joined_owner_not_connected_label=El to collaciu ta esperando pa restolar {{roomURLHostname}} contigo.
self_view_hidden_message=Self-view hidden but still being sent; resize window to show
peer_left_session=El to collaciu coló.
peer_unexpected_quit=El to collaciu desconeutóse de mou inesperáu.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.
tos_failure_message={{clientShortname}} nun ta disponible nel to país.
display_name_guest=Convidáu
## LOCALIZATION NOTE(clientSuperShortname): This should not be localized and
## should remain "Hello" for all locales.

View File

@@ -203,6 +203,8 @@ door_hanger_button2=Tamam!
infobar_screenshare_no_guest_message=Yoldaşınız daxil olduqda üzərinə kliklədiyiniz hər bir vərəqi görəcəklər.
infobar_screenshare_browser_message2=Vərəqlərinizi paylaşırsınız. Üzərinə kliklədiyiniz hər vərəq yoldaşlarınız tərəfindən görünəcək
infobar_screenshare_browser_message3=Artıq vərəqlərinizi paylaşırsınız. Yoldaşınız üzərinə kliklədiyiniz hər bir vərəqi görə biləcək.
infobar_screenshare_stop_sharing_message2=Artıq vərəqlərinizi paylaşmırsız.
infobar_screenshare_stop_no_guest_message=Vərəqlərinizin paylaşımını dayandırmısız. Yoldaşlarınız qoşulduqda siz təkrar paylaşana qədər heç nə görməyəcəklər.
infobar_button_restart_label2=Paylaşmanı yenidən başlat
infobar_button_restart_accesskey=e
infobar_button_stop_label2=Paylaşmanı dayandır

View File

@@ -33,7 +33,7 @@ first_time_experience_subheading_button_above=Cliciwch ar y botwn uchod er mwyn
## LOCALIZATION_NOTE(first_time_experience_content, first_time_experience_content2): Message describing
## ways to use Hello project.
first_time_experience_content=Ei ddefnyddio i gynllunio gyda'n gilydd, yn gweithio gyda'n gilydd, yn chwerthin gyda'n gilydd.
first_time_experience_content=Ei ddefnyddio i gynllunio gyda'n gilydd, gweithio gyda'n gilydd, chwerthin gyda'n gilydd.
first_time_experience_content2=Defnyddiwch Hello i wneud pethau: cynllunio, chwerthin, gweithio gyda'ch gilydd.
first_time_experience_button_label2=Gweld sut mae'n gweithio
@@ -55,7 +55,7 @@ fte_slide_3_copy=Mae {{clientSuperShortname}} y gweithio gyda'r rhan fwyaf o bor
fte_slide_4_title=Chwiliwch am eicon {{clientSuperShortname}} er mwyn cychwyn arni
## LOCALIZATION_NOTE(fte_slide_4_copy): {{brandShortname}}
## will be replaced by the brand short name.
fte_slide_4_copy=Unwaith i chi ddarganfod tudalen rydych am ei thrafod, cliciwch eicon {{brandShortname}} er mwyn creu dolen. Yna gallwch ei anfon at eich ffrind yma ma bynnag ffordd ag yr hoffech chi!
fte_slide_4_copy=Unwaith i chi ddarganfod tudalen rydych am ei thrafod, cliciwch eicon {{brandShortname}} er mwyn creu dolen. Yna gallwch ei anfon at eich ffrind sut bynnag yr hoffech chi!
invite_header_text_bold2=Gwahoddwch ffrind i ymuno â chi!
invite_header_text4=Rhannwch y ddolen fel bod modd i chi bori'r We gyda'ch gilydd.
@@ -193,12 +193,18 @@ rooms_signout_alert=Bydd sgyrsiau agored yn cael eu cau
room_name_untitled_page=Tudalen Ddideitl
## LOCALIZATION NOTE (door_hanger_return, door_hanger_prompt_name, door_hanger_button): Dialog message on leaving conversation
door_hanger_bye=Hwyl am y tro!
door_hanger_return2=Mae modd i chi ddod nôl i'r sesiwn rhannu yma ar unrhyw adeg drwy' banel Hello. Hoffech chi roi enwn hawdd ei gofio iddo?
door_hanger_current=Enw cyfredol:
door_hanger_button2=Iawn!
# Infobar strings
infobar_screenshare_no_guest_message=Cyn gynted a bo'ch ffrind yn ymuno, bydd modd iddyn nhw weld unrhyw dab rydych yn clicio arno.
infobar_screenshare_browser_message2=Rydych yn rhannu eich tabiau. Mae modd i'ch ffrindiau weld unrhyw dab rydych yn clicio arno
infobar_screenshare_browser_message3=Rydych nawr yn rhannu eich tabiau. Bydd eich ffrind yn gweld unrhyw dab fyddwch chi'n clicio arno.
infobar_screenshare_stop_sharing_message2=Nid ydych yn rhannu eich tabiau bellach.
infobar_screenshare_stop_no_guest_message=Rydych wedi peidio â rhannu eich tabiau. Pan fydd eich ffrind yn ymuno, fyddan nhw ddim yn gweld dim nes i chi ailgychwyn rhannu.
infobar_button_restart_label2=Ail gychwyn rhannu
infobar_button_restart_accesskey=A
infobar_button_stop_label2=Peidio rhannu
@@ -208,6 +214,10 @@ infobar_button_disconnect_accesskey=D
# Copy panel strings
copy_panel_message=Angen rhannu 'r dudalen Gwe hon? Rhannwch dab eich porwr gyda'ch ffrind.
copy_panel_dont_show_again_label=Peidio dangos hwn eto
copy_panel_cancel_button_label=Nid nawr
copy_panel_accept_button_label=Iawn, dangoswch i mi sut
# E10s not supported strings
@@ -257,6 +267,7 @@ self_view_hidden_message=Golwg o'ch hun yn cael ei guddio ond yn dal i gael ei a
peer_left_session=Mae eich ffrind wedi gadael.
peer_unexpected_quit=Mae eich ffrind wedi datgysylltu'n annisgwyl.
peer_join_session=Mae eich ffrind wedi ymuno
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Billedet fra eget kamera er skjult, men sendes stadig t
peer_left_session=Din ven har forladt samtalen.
peer_unexpected_quit=Din ven har uventet afbrudt forbindelsen.
peer_join_session=Din ven har sluttet sig til.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -268,6 +268,7 @@ self_view_hidden_message=Eigenes Kamerabild ist ausgeblendet, wird aber gesendet
peer_left_session=Ihr Freund hat das Gespräch verlassen.
peer_unexpected_quit=Die Verbindung zu Ihrem Freund wurde unerwartet getrennt.
peer_join_session=Ihr Freund hat das Gespräch betreten.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Samonaglěd schowany, ale sćelo se hyšći; změńśo
peer_left_session=Waš pśijaśel jo wótešeł.
peer_unexpected_quit=Zwisk z wašym pśijaśelom jo se źělił.
peer_join_session=Waš pśijaśel jo se pśizamknuł.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Self-view hidden but still being sent; resize window to
peer_left_session=Your friend has left.
peer_unexpected_quit=Your friend has unexpectedly disconnected.
peer_join_session=Your friend has joined.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Se está enviando la vista propia aunque esté oculta;
peer_left_session=Tu amigo se ha ido.
peer_unexpected_quit=Tu amigo se ha desconectado de forma inesperada.
peer_join_session=Tu amigo se ha unido.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Lähettämäsi kuva on piilotettu, mutta sitä lähetet
peer_left_session=Kaverisi lähti.
peer_unexpected_quit=Kaverisi yhteys katkesi yllättäen.
peer_join_session=Kaverisi on liittynyt keskusteluun.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Retour vidéo masqué, mais la vidéo est toujours tran
peer_left_session=Lautre personne a quitté la conversation.
peer_unexpected_quit=Lautre personne a été brusquement déconnectée.
peer_join_session=Lautre personne a rejoint la conversation.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Eigen werjefte ferburgen, mar wurdt noch hieltyd ferstj
peer_left_session=Jo freon hat it petear ferlitten.
peer_unexpected_quit=Jo freon hat ûnferwacht de ferbining ferbrutsen.
peer_join_session=Jo freon hat oan it petear dielnommen.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Eigen werjefte ferburgen, mar wurdt noch hieltyd ferstj
peer_left_session=Jo freon hat it petear ferlitten.
peer_unexpected_quit=Jo freon hat ûnferwacht de ferbining ferbrutsen.
peer_join_session=Jo freon hat oan it petear dielnommen.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Samonapohlad schowany, ale sćele so hišće; změńće
peer_left_session=Waš přećel je wotešoł.
peer_unexpected_quit=Zwisk z wašim přećelo je so njenadźicy dzělił.
peer_join_session=Waš přećel je so přidružił.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=A saját kamera képe elrejtve, de elküldésre kerül.
peer_left_session=Ismerőse távozott.
peer_unexpected_quit=Ismerőse váratlanul szétkapcsolódott.
peer_join_session=Ismerőse csatlakozott.
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=Ինքնադիտումը թաքցված է, բայց դ
peer_left_session=Ձեր ընկերը հեռացել է:
peer_unexpected_quit=Ձեր ընկերը անսպասելի կապախզվել է:
peer_join_session=Ձեր ընկերը միացել է:
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -267,6 +267,7 @@ self_view_hidden_message=セルフビューは隠れていますが送信され
peer_left_session=友達が退出しました。
peer_unexpected_quit=友達の接続が予期せず終了しました。
peer_join_session=友達が参加しました。
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname}}
## as this will be replaced by clientShortname2.

View File

@@ -7,6 +7,8 @@
## LOCALIZATION_NOTE(loopMenuItem_label): Label of the menu item that is placed
## inside the browser 'Tools' menu. Use the unicode ellipsis char, \u2026, or
## use "..." if \u2026 doesn't suit traditions in your locale.
loopMenuItem_label=Pradėti pokalbį…
loopMenuItem_accesskey=r
## LOCALIZATION_NOTE(sign_in_again_title_line_one, sign_in_again_title_line_two2):
## These are displayed together at the top of the panel when a user is needed to
@@ -14,9 +16,14 @@
## and this is displayed in slightly larger font. Please arrange as necessary for
## your locale.
## {{clientShortname2}} will be replaced by the brand name for either string.
sign_in_again_title_line_one=Prašome prisijungti iš naujo,
sign_in_again_title_line_two2=jei norite toliau naudotis „{{clientShortname2}}“
sign_in_again_button=Prisijungti
## LOCALIZATION_NOTE(sign_in_again_use_as_guest_button2): {{clientSuperShortname}}
## will be replaced by the super short brandname.
sign_in_again_use_as_guest_button2=Naudotis „{{clientSuperShortname}}“ kaip svečiui
panel_browse_with_friend_button=Naršyti šį tinklalapį su draugu
## LOCALIZATION_NOTE(first_time_experience_subheading2, first_time_experience_subheading_button_above): Message inviting the
## user to create his or her first conversation.
@@ -39,6 +46,7 @@
## LOCALIZATION_NOTE(invite_copy_link_button, invite_copied_link_button,
## invite_email_link_button, invite_facebook_button2): These labels appear under
## an iconic button for the invite view.
invite_facebook_button3=Facebook
# Error bars
## LOCALIZATION NOTE(session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
@@ -46,6 +54,7 @@
## LOCALIZATION NOTE(retry_button): Displayed when there is an error to retry
## the appropriate action.
retry_button=Bandyti dar kartą
## LOCALIZATION NOTE (share_email_body7): In this item, don't translate the
## part between {{..}} and leave the \n\n part alone
@@ -59,12 +68,19 @@
## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Kopijuoti saitą
email_link_menuitem=Išsiųsti saitą el. paštu
delete_conversation_menuitem2=Šalinti
settings_menu_item_account=Paskyra
settings_menu_item_signout=Atsijungti
settings_menu_item_signin=Prisijungti
# Conversation Window Strings
incoming_call_block_button=Atmesti
hangup_button_title=Baigti pokalbį
hangup_button_caption2=Baigti
@@ -75,6 +91,9 @@ hangup_button_caption2=Baigti
# Outgoing conversation
initiate_audio_video_call_button2=Pradėti
initiate_audio_video_call_tooltip2=Pradėti vaizdo pokalbį
initiate_audio_call_button2=Balso pokalbis
## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
@@ -84,6 +103,7 @@ hangup_button_caption2=Baigti
## LOCALIZATION NOTE (cancel_button):
## This button is displayed when a call has failed.
cancel_button=Atsisakyti
## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
@@ -97,6 +117,7 @@ hangup_button_caption2=Baigti
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
feedback_rejoin_button=Prisijungti iš naujo
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.

View File

@@ -60,6 +60,11 @@ invite_your_link=Jūsu saite:
# Error bars
## LOCALIZATION NOTE(session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
## These may be displayed at the top of the panel.
try_again_later=Lūdzu mēģiniet vēlreiz nedaudz vēlāk
could_not_connect=Nevarēja pieslēgties serverim
check_internet_connection=Lūdzu pārbaudiet savu interneta savienojumu
login_expired=Jūsu pieteikšanās vairs nav derīga
service_not_available=Pakalpojums šobrīd nav pieejams
problem_accessing_account=Radās problēma piekļūstot jūsu kontam
## LOCALIZATION NOTE(retry_button): Displayed when there is an error to retry
@@ -76,9 +81,13 @@ share_email_subject7=Jūsu uzaicinājums lai pārlūkotu tīmekli kopā
## between {{..}}. Please keep the text below 117 characters to make sure it fits
## in a tweet.
share_add_service_button=Pievienot pakalpojumu
## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
## These menu items are displayed from a panel's context menu for a conversation.
copy_link_menuitem=Kopēt saiti
email_link_menuitem=Nosūtīt saiti
edit_name_menuitem=Rediģēt vārdu
delete_conversation_menuitem2=Dzēst
panel_footer_signin_or_signup_link=Pieslēdzieties vai reģistrējieties
@@ -105,6 +114,7 @@ hangup_button_caption2=Iziet
## LOCALIZATION NOTE (call_with_contact_title): The title displayed
## when calling a contact. Don't translate the part between {{..}} because
## this will be replaced by the contact's name.
call_with_contact_title=Saruna ar {{contactName}}
# Outgoing conversation
@@ -140,6 +150,7 @@ legal_text_privacy=Privātuma paziņojums
## These 2 strings are displayed before and after a 'Telefonica'
## logo.
powered_by_beforeLogo=Nodrošina
powered_by_afterLogo=
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.

Some files were not shown because too many files have changed in this diff Show More