Merge fx-team to central, a=merge

This commit is contained in:
Wes Kocher 2015-10-09 13:38:52 -07:00
commit 6e6909afd7
63 changed files with 509 additions and 6868 deletions

View File

@ -1707,7 +1707,6 @@ pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
pref("loop.feedback.product", "Loop");
pref("loop.debug.loglevel", "Error");
pref("loop.debug.dispatcher", false);
pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
pref("loop.debug.twoWayMediaTelemetry", false);
pref("loop.feedback.dateLastSeenSec", 0);

View File

@ -31,7 +31,6 @@
<script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
<script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
<script type="text/javascript" src="loop/shared/js/store.js"></script>
<script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
<script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/js/feedbackViews.js"></script>
@ -39,11 +38,7 @@
<script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
<script type="text/javascript" src="loop/shared/js/linkifiedTextView.js"></script>
<script type="text/javascript" src="loop/shared/js/urlRegExps.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/js/roomStore.js"></script>
<script type="text/javascript" src="loop/js/roomViews.js"></script>
<script type="text/javascript" src="loop/js/conversation.js"></script>

View File

@ -1,119 +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.Client = (function() {
"use strict";
// THe expected properties to be returned from the POST /calls request.
var expectedPostCallProperties = [
"apiKey", "callId", "progressURL",
"sessionId", "sessionToken", "websocketToken"
];
/**
* Loop server client.
*
* @param {Object} settings Settings object.
*/
function Client(settings) {
if (!settings) {
settings = {};
}
// allowing an |in| test rather than a more type || allows us to dependency
// inject a non-existent mozLoop
if ("mozLoop" in settings) {
this.mozLoop = settings.mozLoop;
} else {
this.mozLoop = navigator.mozLoop;
}
this.settings = settings;
}
Client.prototype = {
/**
* Validates a data object to confirm it has the specified properties.
*
* @param {Object} The data object to verify
* @param {Array} The list of properties to verify within the object
* @return This returns either the specific property if only one
* property is specified, or it returns all properties
*/
_validate: function(data, properties) {
if (typeof data !== "object") {
throw new Error("Invalid data received from server");
}
properties.forEach(function (property) {
if (!data.hasOwnProperty(property)) {
throw new Error("Invalid data received from server - missing " +
property);
}
});
if (properties.length === 1) {
return data[properties[0]];
}
return data;
},
/**
* Generic handler for XHR failures.
*
* @param {Function} cb Callback(err)
* @param {Object} error See MozLoopAPI.hawkRequest
*/
_failureHandler: function(cb, error) {
var message = "HTTP " + error.code + " " + error.error + "; " + error.message;
console.error(message);
cb(error);
},
/**
* Sets up an outgoing call, getting the relevant data from the server.
*
* Callback parameters:
* - err null on successful registration, non-null otherwise.
* - result an object of the obtained data for starting the call, if successful
*
* @param {Array} calleeIds an array of emails and phone numbers.
* @param {String} callType the type of call.
* @param {Function} cb Callback(err, result)
*/
setupOutgoingCall: function(calleeIds, callType, cb) {
// For direct calls, we only ever use the logged-in session. Direct
// calls by guests aren't valid.
this.mozLoop.hawkRequest(this.mozLoop.LOOP_SESSION_TYPE.FXA,
"/calls", "POST", {
calleeId: calleeIds,
callType: callType,
channel: this.mozLoop.appVersionInfo ?
this.mozLoop.appVersionInfo.channel : "unknown"
},
function (err, responseText) {
if (err) {
this._failureHandler(cb, err);
return;
}
try {
var postData = JSON.parse(responseText);
var outgoingCallData = this._validate(postData,
expectedPostCallProperties);
cb(null, outgoingCallData);
} catch (ex) {
console.log("Error requesting call info", ex);
cb(ex);
}
}.bind(this)
);
}
};
return Client;
})();

View File

@ -8,11 +8,11 @@ loop.conversation = (function(mozL10n) {
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var CallControllerView = loop.conversationViews.CallControllerView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
var FeedbackView = loop.feedbackViews.FeedbackView;
var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
var RoomFailureView = loop.roomViews.RoomFailureView;
/**
* Master controller view for handling if incoming or outgoing calls are
@ -68,15 +68,6 @@ loop.conversation = (function(mozL10n) {
}
switch(this.state.windowType) {
// CallControllerView is used for both.
case "incoming":
case "outgoing": {
return (React.createElement(CallControllerView, {
chatWindowDetached: this.state.chatWindowDetached,
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop,
onCallTerminated: this.handleCallTerminated}));
}
case "room": {
return (React.createElement(DesktopRoomConversationView, {
chatWindowDetached: this.state.chatWindowDetached,
@ -86,11 +77,10 @@ loop.conversation = (function(mozL10n) {
roomStore: this.props.roomStore}));
}
case "failed": {
return (React.createElement(DirectCallFailureView, {
contact: {},
return (React.createElement(RoomFailureView, {
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop,
outgoing: false}));
failureReason: FAILURE_DETAILS.UNKNOWN,
mozLoop: this.props.mozLoop}));
}
default: {
// If we don't have a windowType, we don't know what we are yet,
@ -127,7 +117,6 @@ loop.conversation = (function(mozL10n) {
var useDataChannels = loop.shared.utils.getBoolPreference("textChat.enabled");
var dispatcher = new loop.Dispatcher();
var client = new loop.Client();
var sdkDriver = new loop.OTSdkDriver({
isDesktop: true,
useDataChannels: useDataChannels,
@ -140,12 +129,6 @@ loop.conversation = (function(mozL10n) {
loop.conversation._sdkDriver = sdkDriver;
// Create the stores.
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: client,
isDesktop: true,
mozLoop: navigator.mozLoop,
sdkDriver: sdkDriver
});
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
isDesktop: true,
mozLoop: navigator.mozLoop,
@ -166,7 +149,6 @@ loop.conversation = (function(mozL10n) {
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
conversationStore: conversationStore,
textChatStore: textChatStore
});

View File

@ -8,11 +8,11 @@ loop.conversation = (function(mozL10n) {
var sharedMixins = loop.shared.mixins;
var sharedActions = loop.shared.actions;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var CallControllerView = loop.conversationViews.CallControllerView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
var FeedbackView = loop.feedbackViews.FeedbackView;
var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
var RoomFailureView = loop.roomViews.RoomFailureView;
/**
* Master controller view for handling if incoming or outgoing calls are
@ -68,15 +68,6 @@ loop.conversation = (function(mozL10n) {
}
switch(this.state.windowType) {
// CallControllerView is used for both.
case "incoming":
case "outgoing": {
return (<CallControllerView
chatWindowDetached={this.state.chatWindowDetached}
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop}
onCallTerminated={this.handleCallTerminated} />);
}
case "room": {
return (<DesktopRoomConversationView
chatWindowDetached={this.state.chatWindowDetached}
@ -86,11 +77,10 @@ loop.conversation = (function(mozL10n) {
roomStore={this.props.roomStore} />);
}
case "failed": {
return (<DirectCallFailureView
contact={{}}
return (<RoomFailureView
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop}
outgoing={false} />);
failureReason={FAILURE_DETAILS.UNKNOWN}
mozLoop={this.props.mozLoop} />);
}
default: {
// If we don't have a windowType, we don't know what we are yet,
@ -127,7 +117,6 @@ loop.conversation = (function(mozL10n) {
var useDataChannels = loop.shared.utils.getBoolPreference("textChat.enabled");
var dispatcher = new loop.Dispatcher();
var client = new loop.Client();
var sdkDriver = new loop.OTSdkDriver({
isDesktop: true,
useDataChannels: useDataChannels,
@ -140,12 +129,6 @@ loop.conversation = (function(mozL10n) {
loop.conversation._sdkDriver = sdkDriver;
// Create the stores.
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: client,
isDesktop: true,
mozLoop: navigator.mozLoop,
sdkDriver: sdkDriver
});
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
isDesktop: true,
mozLoop: navigator.mozLoop,
@ -166,7 +149,6 @@ loop.conversation = (function(mozL10n) {
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
conversationStore: conversationStore,
textChatStore: textChatStore
});

View File

@ -142,10 +142,6 @@ loop.store.ConversationAppStore = (function() {
*/
LoopHangupNowHandler: function() {
switch (this.getStoreState().windowType) {
case "incoming":
case "outgoing":
this._dispatcher.dispatch(new loop.shared.actions.HangupCall());
break;
case "room":
if (this._activeRoomStore.getStoreState().used &&
!this._storeState.showFeedbackForm) {

View File

@ -1,870 +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.conversationViews = (function(mozL10n) {
"use strict";
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
// finding a logical place for them to be shared.
// XXXdmose this code is already out of sync with the code in contacts.jsx
// which, unlike this code, now has unit tests. We should totally do the
// above.
function _getPreferredEmail(contact) {
// A contact may not contain email addresses, but only a phone number.
if (!contact.email || contact.email.length === 0) {
return { value: "" };
}
return contact.email.find(function find(e) { return e.pref; }) || contact.email[0];
}
function _getContactDisplayName(contact) {
if (contact.name && contact.name[0]) {
return contact.name[0];
}
return _getPreferredEmail(contact).value;
}
/**
* Displays information about the call
* Caller avatar, name & conversation creation date
*/
var CallIdentifierView = React.createClass({displayName: "CallIdentifierView",
propTypes: {
peerIdentifier: React.PropTypes.string,
showIcons: React.PropTypes.bool.isRequired,
urlCreationDate: React.PropTypes.string,
video: React.PropTypes.bool
},
getDefaultProps: function() {
return {
peerIdentifier: "",
showLinkDetail: true,
urlCreationDate: "",
video: true
};
},
getInitialState: function() {
return {timestamp: 0};
},
/**
* Gets and formats the incoming call creation date
*/
formatCreationDate: function() {
if (!this.props.urlCreationDate) {
return "";
}
var timestamp = this.props.urlCreationDate;
return "(" + loop.shared.utils.formatDate(timestamp) + ")";
},
render: function() {
var iconVideoClasses = React.addons.classSet({
"fx-embedded-tiny-video-icon": true,
"muted": !this.props.video
});
var callDetailClasses = React.addons.classSet({
"fx-embedded-call-detail": true,
"hide": !this.props.showIcons
});
return (
React.createElement("div", {className: "fx-embedded-call-identifier"},
React.createElement("div", {className: "fx-embedded-call-identifier-avatar fx-embedded-call-identifier-item"}),
React.createElement("div", {className: "fx-embedded-call-identifier-info fx-embedded-call-identifier-item"},
React.createElement("div", {className: "fx-embedded-call-identifier-text overflow-text-ellipsis"},
this.props.peerIdentifier
),
React.createElement("div", {className: callDetailClasses},
React.createElement("span", {className: "fx-embedded-tiny-audio-icon"}),
React.createElement("span", {className: iconVideoClasses}),
React.createElement("span", {className: "fx-embedded-conversation-timestamp"},
this.formatCreationDate()
)
)
)
)
);
}
});
/**
* Displays details of the incoming/outgoing conversation
* (name, link, audio/video type etc).
*
* Allows the view to be extended with different buttons and progress
* via children properties.
*/
var ConversationDetailView = React.createClass({displayName: "ConversationDetailView",
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.arrayOf(React.PropTypes.element)
]).isRequired,
contact: React.PropTypes.object
},
render: function() {
var contactName = _getContactDisplayName(this.props.contact);
return (
React.createElement("div", {className: "call-window"},
React.createElement(CallIdentifierView, {
peerIdentifier: contactName,
showIcons: false}),
React.createElement("div", null, this.props.children)
)
);
}
});
// Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var AcceptCallView = React.createClass({displayName: "AcceptCallView",
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
callType: React.PropTypes.string.isRequired,
callerId: React.PropTypes.string.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired,
// Only for use by the ui-showcase
showMenu: React.PropTypes.bool
},
getDefaultProps: function() {
return {
showMenu: false
};
},
componentDidMount: function() {
this.props.mozLoop.startAlerting();
},
componentWillUnmount: function() {
this.props.mozLoop.stopAlerting();
},
clickHandler: function(e) {
var target = e.target;
if (!target.classList.contains("btn-chevron")) {
this._hideDeclineMenu();
}
},
_handleAccept: function(callType) {
return function() {
this.props.dispatcher.dispatch(new sharedActions.AcceptCall({
callType: callType
}));
}.bind(this);
},
_handleDecline: function() {
this.props.dispatcher.dispatch(new sharedActions.DeclineCall({
blockCaller: false
}));
},
_handleDeclineBlock: function(e) {
this.props.dispatcher.dispatch(new sharedActions.DeclineCall({
blockCaller: true
}));
/* Prevent event propagation
* stop the click from reaching parent element */
e.stopPropagation();
e.preventDefault();
},
/*
* Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video
* answer button primarily, an audio call will flip them.
**/
_answerModeProps: function() {
var videoButton = {
handler: this._handleAccept(CALL_TYPES.AUDIO_VIDEO),
className: "fx-embedded-btn-icon-video",
tooltip: "incoming_call_accept_audio_video_tooltip"
};
var audioButton = {
handler: this._handleAccept(CALL_TYPES.AUDIO_ONLY),
className: "fx-embedded-btn-audio-small",
tooltip: "incoming_call_accept_audio_only_tooltip"
};
var props = {};
props.primary = videoButton;
props.secondary = audioButton;
// When video is not enabled on this call, we swap the buttons around.
if (this.props.callType === CALL_TYPES.AUDIO_ONLY) {
audioButton.className = "fx-embedded-btn-icon-audio";
videoButton.className = "fx-embedded-btn-video-small";
props.primary = audioButton;
props.secondary = videoButton;
}
return props;
},
render: function() {
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showMenu
});
return (
React.createElement("div", {className: "call-window"},
React.createElement(CallIdentifierView, {
peerIdentifier: this.props.callerId,
showIcons: true,
video: this.props.callType === CALL_TYPES.AUDIO_VIDEO}),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("div", {className: "fx-embedded-call-button-spacer"}),
React.createElement("div", {className: "btn-chevron-menu-group"},
React.createElement("div", {className: "btn-group-chevron"},
React.createElement("div", {className: "btn-group"},
React.createElement("button", {className: "btn btn-decline",
onClick: this._handleDecline},
mozL10n.get("incoming_call_cancel_button")
),
React.createElement("div", {className: "btn-chevron",
onClick: this.toggleDropdownMenu,
ref: "menu-button"})
),
React.createElement("ul", {className: dropdownMenuClassesDecline},
React.createElement("li", {className: "btn-block", onClick: this._handleDeclineBlock},
mozL10n.get("incoming_call_cancel_and_block_button")
)
)
)
),
React.createElement("div", {className: "fx-embedded-call-button-spacer"}),
React.createElement(AcceptCallButton, {mode: this._answerModeProps()}),
React.createElement("div", {className: "fx-embedded-call-button-spacer"})
)
)
);
}
});
/**
* Incoming call view accept button, renders different primary actions
* (answer with video / with audio only) based on the props received
**/
var AcceptCallButton = React.createClass({displayName: "AcceptCallButton",
propTypes: {
mode: React.PropTypes.object.isRequired
},
render: function() {
var mode = this.props.mode;
return (
React.createElement("div", {className: "btn-chevron-menu-group"},
React.createElement("div", {className: "btn-group"},
React.createElement("button", {className: "btn btn-accept",
onClick: mode.primary.handler,
title: mozL10n.get(mode.primary.tooltip)},
React.createElement("span", {className: "fx-embedded-answer-btn-text"},
mozL10n.get("incoming_call_accept_button")
),
React.createElement("span", {className: mode.primary.className})
),
React.createElement("div", {className: mode.secondary.className,
onClick: mode.secondary.handler,
title: mozL10n.get(mode.secondary.tooltip)}
)
)
)
);
}
});
/**
* View for pending conversations. Displays a cancel button and appropriate
* pending/ringing strings.
*/
var PendingConversationView = React.createClass({displayName: "PendingConversationView",
mixins: [sharedMixins.AudioMixin],
propTypes: {
callState: React.PropTypes.string,
contact: React.PropTypes.object,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
enableCancelButton: React.PropTypes.bool
},
getDefaultProps: function() {
return {
enableCancelButton: false
};
},
componentDidMount: function() {
this.play("ringtone", {loop: true});
},
cancelCall: function() {
this.props.dispatcher.dispatch(new sharedActions.CancelCall());
},
render: function() {
var cx = React.addons.classSet;
var pendingStateString;
if (this.props.callState === CALL_STATES.ALERTING) {
pendingStateString = mozL10n.get("call_progress_ringing_description");
} else {
pendingStateString = mozL10n.get("call_progress_connecting_description");
}
var btnCancelStyles = cx({
"btn": true,
"btn-cancel": true,
"disabled": !this.props.enableCancelButton
});
return (
React.createElement(ConversationDetailView, {contact: this.props.contact},
React.createElement("p", {className: "btn-label"}, pendingStateString),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("button", {className: btnCancelStyles,
onClick: this.cancelCall},
mozL10n.get("initiate_call_cancel_button")
)
)
)
);
}
});
/**
* Used to display errors in direct calls and rooms to the user.
*/
var FailureInfoView = React.createClass({displayName: "FailureInfoView",
propTypes: {
contact: React.PropTypes.object,
extraFailureMessage: React.PropTypes.string,
extraMessage: React.PropTypes.string,
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() {
switch (this.props.failureReason) {
case FAILURE_DETAILS.USER_UNAVAILABLE:
var contactDisplayName = _getContactDisplayName(this.props.contact);
if (contactDisplayName.length) {
return mozL10n.get(
"contact_unavailable_title",
{"contactName": contactDisplayName});
}
return mozL10n.get("generic_contact_unavailable_title");
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") });
case FAILURE_DETAILS.ICE_FAILED:
return mozL10n.get("ice_failure_message");
default:
return mozL10n.get("generic_failure_message");
}
},
_renderExtraMessage: function() {
if (this.props.extraMessage) {
return React.createElement("p", {className: "failure-info-extra"}, this.props.extraMessage);
}
return null;
},
_renderExtraFailureMessage: function() {
if (this.props.extraFailureMessage) {
return React.createElement("p", {className: "failure-info-extra-failure"}, this.props.extraFailureMessage);
}
return null;
},
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()),
this._renderExtraMessage(),
this._renderExtraFailureMessage()
)
);
}
});
/**
* Direct Call failure view. Displayed when a call fails.
*/
var DirectCallFailureView = React.createClass({displayName: "DirectCallFailureView",
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationStore"),
sharedMixins.AudioMixin,
sharedMixins.WindowCloseMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// This is used by the UI showcase.
emailLinkError: React.PropTypes.bool,
mozLoop: React.PropTypes.object.isRequired,
outgoing: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return _.extend({
emailLinkError: this.props.emailLinkError,
emailLinkButtonDisabled: false
}, this.getStoreState());
},
componentDidMount: function() {
this.play("failure");
this.listenTo(this.getStore(), "change:emailLink",
this._onEmailLinkReceived);
this.listenTo(this.getStore(), "error:emailLink",
this._onEmailLinkError);
},
componentWillUnmount: function() {
this.stopListening(this.getStore());
},
_onEmailLinkReceived: function() {
var emailLink = this.getStoreState().emailLink;
var contactEmail = _getPreferredEmail(this.state.contact).value;
sharedUtils.composeCallUrlEmail(emailLink, contactEmail, null, "callfailed");
this.closeWindow();
},
_onEmailLinkError: function() {
this.setState({
emailLinkError: true,
emailLinkButtonDisabled: false
});
},
retryCall: function() {
this.props.dispatcher.dispatch(new sharedActions.RetryCall());
},
cancelCall: function() {
this.props.dispatcher.dispatch(new sharedActions.CancelCall());
},
emailLink: function() {
this.setState({
emailLinkError: false,
emailLinkButtonDisabled: true
});
this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
roomName: _getContactDisplayName(this.state.contact)
}));
},
render: function() {
var cx = React.addons.classSet;
var retryClasses = cx({
btn: true,
"btn-info": true,
"btn-retry": true,
hide: !this.props.outgoing
});
var emailClasses = cx({
btn: true,
"btn-info": true,
"btn-email": true,
hide: !this.props.outgoing
});
var settingsMenuItems = [
{ id: "feedback" },
{ id: "help" }
];
var extraMessage;
if (this.props.outgoing) {
extraMessage = mozL10n.get("generic_failure_with_reason2");
}
var extraFailureMessage;
if (this.state.emailLinkError) {
extraFailureMessage = mozL10n.get("unable_retrieve_url");
}
return (
React.createElement("div", {className: "direct-call-failure"},
React.createElement(FailureInfoView, {
contact: this.state.contact,
extraFailureMessage: extraFailureMessage,
extraMessage: extraMessage,
failureReason: this.getStoreState().callStateReason}),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("button", {className: "btn btn-cancel",
onClick: this.cancelCall},
mozL10n.get("cancel_button")
),
React.createElement("button", {className: retryClasses,
onClick: this.retryCall},
mozL10n.get("retry_call_button")
),
React.createElement("button", {className: emailClasses,
disabled: this.state.emailLinkButtonDisabled,
onClick: this.emailLink},
mozL10n.get("share_button3")
)
),
React.createElement(loop.shared.views.SettingsControlButton, {
menuBelow: true,
menuItems: settingsMenuItems,
mozLoop: this.props.mozLoop})
)
);
}
});
var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
mixins: [
sharedMixins.MediaSetupMixin
],
propTypes: {
// local
audio: React.PropTypes.object,
chatWindowDetached: React.PropTypes.bool.isRequired,
// We pass conversationStore here rather than use the mixin, to allow
// easy configurability for the ui-showcase.
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
// This is used from the props rather than the state to make it easier for
// the ui-showcase.
mediaConnected: React.PropTypes.bool,
mozLoop: React.PropTypes.object,
remotePosterUrl: React.PropTypes.string,
remoteVideoEnabled: React.PropTypes.bool,
// local
video: React.PropTypes.object
},
getDefaultProps: function() {
return {
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true}
};
},
getInitialState: function() {
return this.props.conversationStore.getStoreState();
},
componentWillMount: function() {
this.props.conversationStore.on("change", function() {
this.setState(this.props.conversationStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.props.conversationStore.off("change", null, this);
},
componentDidMount: function() {
// 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.
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
})
}));
},
/**
* Hangs up the call.
*/
hangup: function() {
this.props.dispatcher.dispatch(
new sharedActions.HangupCall());
},
/**
* Used to control publishing a stream - i.e. to mute a stream
*
* @param {String} type The type of stream, e.g. "audio" or "video".
* @param {Boolean} enabled True to enable the stream, false otherwise.
*/
publishStream: function(type, enabled) {
this.props.dispatcher.dispatch(
new sharedActions.SetMute({
type: type,
enabled: enabled
}));
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a local
* stream is on its way from the camera?
*
* @returns {boolean}
* @private
*/
_isLocalLoading: function () {
return !this.state.localSrcMediaElement && !this.props.localPosterUrl;
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a remote
* stream is on its way from the other user?
*
* @returns {boolean}
* @private
*/
_isRemoteLoading: function() {
return !!(!this.state.remoteSrcMediaElement &&
!this.props.remotePosterUrl &&
!this.state.mediaConnected);
},
shouldRenderRemoteVideo: function() {
if (this.props.mediaConnected) {
// If remote video is not enabled, we're muted, so we'll show an avatar
// instead.
return this.props.remoteVideoEnabled;
}
// We're not yet connected, but we don't want to show the avatar, and in
// the common case, we'll just transition to the video.
return true;
},
render: function() {
// 'visible' and 'enabled' are true by default.
var settingsMenuItems = [
{
id: "edit",
visible: false,
enabled: false
},
{ id: "feedback" },
{ id: "help" }
];
return (
React.createElement("div", {className: "desktop-call-wrapper"},
React.createElement(sharedViews.MediaLayoutView, {
dispatcher: this.props.dispatcher,
displayScreenShare: false,
isLocalLoading: this._isLocalLoading(),
isRemoteLoading: this._isRemoteLoading(),
isScreenShareLoading: false,
localPosterUrl: this.props.localPosterUrl,
localSrcMediaElement: this.state.localSrcMediaElement,
localVideoMuted: !this.props.video.enabled,
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,
showContextRoomName: false,
useDesktopPaths: true},
React.createElement(sharedViews.ConversationToolbar, {
audio: this.props.audio,
dispatcher: this.props.dispatcher,
hangup: this.hangup,
mozLoop: this.props.mozLoop,
publishStream: this.publishStream,
settingsMenuItems: settingsMenuItems,
show: true,
showHangup: this.props.chatWindowDetached,
video: this.props.video})
)
)
);
}
});
/**
* Master View Controller for outgoing calls. This manages
* the different views that need displaying.
*/
var CallControllerView = React.createClass({displayName: "CallControllerView",
mixins: [
sharedMixins.AudioMixin,
sharedMixins.DocumentTitleMixin,
loop.store.StoreMixin("conversationStore"),
Backbone.Events
],
propTypes: {
chatWindowDetached: React.PropTypes.bool.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired,
onCallTerminated: React.PropTypes.func.isRequired
},
getInitialState: function() {
return this.getStoreState();
},
_closeWindow: function() {
window.close();
},
/**
* Returns true if the call is in a cancellable state, during call setup.
*/
_isCancellable: function() {
return this.state.callState !== CALL_STATES.INIT &&
this.state.callState !== CALL_STATES.GATHER;
},
_renderViewFromCallType: function() {
// For outgoing calls we can display the pending conversation view
// for any state that render() doesn't manage.
if (this.state.outgoing) {
return (React.createElement(PendingConversationView, {
callState: this.state.callState,
contact: this.state.contact,
dispatcher: this.props.dispatcher,
enableCancelButton: this._isCancellable()}));
}
// For incoming calls that are in accepting state, display the
// accept call view.
if (this.state.callState === CALL_STATES.ALERTING) {
return (React.createElement(AcceptCallView, {
callType: this.state.callType,
callerId: this.state.callerId,
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop}
));
}
// Otherwise we're still gathering or connecting, so
// don't display anything.
return null;
},
componentDidUpdate: function(prevProps, prevState) {
// Handle timestamp and window closing only when the call has terminated.
if (prevState.callState === CALL_STATES.ONGOING &&
this.state.callState === CALL_STATES.FINISHED) {
this.props.onCallTerminated();
}
},
render: function() {
// Set the default title to the contact name or the callerId, note
// that views may override this, e.g. the feedback view.
if (this.state.contact) {
this.setTitle(_getContactDisplayName(this.state.contact));
} else {
this.setTitle(this.state.callerId || "");
}
switch (this.state.callState) {
case CALL_STATES.CLOSE: {
this._closeWindow();
return null;
}
case CALL_STATES.TERMINATED: {
return (React.createElement(DirectCallFailureView, {
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop,
outgoing: this.state.outgoing}));
}
case CALL_STATES.ONGOING: {
return (React.createElement(OngoingConversationView, {
audio: { enabled: !this.state.audioMuted, visible: true},
chatWindowDetached: this.props.chatWindowDetached,
conversationStore: this.getStore(),
dispatcher: this.props.dispatcher,
mediaConnected: this.state.mediaConnected,
mozLoop: this.props.mozLoop,
remoteSrcMediaElement: this.state.remoteSrcMediaElement,
remoteVideoEnabled: this.state.remoteVideoEnabled,
video: { enabled: !this.state.videoMuted, visible: true}})
);
}
case CALL_STATES.FINISHED: {
this.play("terminated");
// When conversation ended we either display a feedback form or
// close the window. This is decided in the AppControllerView.
return null;
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.
return null;
}
default: {
return this._renderViewFromCallType();
}
}
}
});
return {
PendingConversationView: PendingConversationView,
CallIdentifierView: CallIdentifierView,
ConversationDetailView: ConversationDetailView,
_getContactDisplayName: _getContactDisplayName,
FailureInfoView: FailureInfoView,
DirectCallFailureView: DirectCallFailureView,
AcceptCallView: AcceptCallView,
OngoingConversationView: OngoingConversationView,
CallControllerView: CallControllerView
};
})(document.mozL10n || navigator.mozL10n);

View File

@ -1,870 +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.conversationViews = (function(mozL10n) {
"use strict";
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
// finding a logical place for them to be shared.
// XXXdmose this code is already out of sync with the code in contacts.jsx
// which, unlike this code, now has unit tests. We should totally do the
// above.
function _getPreferredEmail(contact) {
// A contact may not contain email addresses, but only a phone number.
if (!contact.email || contact.email.length === 0) {
return { value: "" };
}
return contact.email.find(function find(e) { return e.pref; }) || contact.email[0];
}
function _getContactDisplayName(contact) {
if (contact.name && contact.name[0]) {
return contact.name[0];
}
return _getPreferredEmail(contact).value;
}
/**
* Displays information about the call
* Caller avatar, name & conversation creation date
*/
var CallIdentifierView = React.createClass({
propTypes: {
peerIdentifier: React.PropTypes.string,
showIcons: React.PropTypes.bool.isRequired,
urlCreationDate: React.PropTypes.string,
video: React.PropTypes.bool
},
getDefaultProps: function() {
return {
peerIdentifier: "",
showLinkDetail: true,
urlCreationDate: "",
video: true
};
},
getInitialState: function() {
return {timestamp: 0};
},
/**
* Gets and formats the incoming call creation date
*/
formatCreationDate: function() {
if (!this.props.urlCreationDate) {
return "";
}
var timestamp = this.props.urlCreationDate;
return "(" + loop.shared.utils.formatDate(timestamp) + ")";
},
render: function() {
var iconVideoClasses = React.addons.classSet({
"fx-embedded-tiny-video-icon": true,
"muted": !this.props.video
});
var callDetailClasses = React.addons.classSet({
"fx-embedded-call-detail": true,
"hide": !this.props.showIcons
});
return (
<div className="fx-embedded-call-identifier">
<div className="fx-embedded-call-identifier-avatar fx-embedded-call-identifier-item"/>
<div className="fx-embedded-call-identifier-info fx-embedded-call-identifier-item">
<div className="fx-embedded-call-identifier-text overflow-text-ellipsis">
{this.props.peerIdentifier}
</div>
<div className={callDetailClasses}>
<span className="fx-embedded-tiny-audio-icon"></span>
<span className={iconVideoClasses}></span>
<span className="fx-embedded-conversation-timestamp">
{this.formatCreationDate()}
</span>
</div>
</div>
</div>
);
}
});
/**
* Displays details of the incoming/outgoing conversation
* (name, link, audio/video type etc).
*
* Allows the view to be extended with different buttons and progress
* via children properties.
*/
var ConversationDetailView = React.createClass({
propTypes: {
children: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.arrayOf(React.PropTypes.element)
]).isRequired,
contact: React.PropTypes.object
},
render: function() {
var contactName = _getContactDisplayName(this.props.contact);
return (
<div className="call-window">
<CallIdentifierView
peerIdentifier={contactName}
showIcons={false} />
<div>{this.props.children}</div>
</div>
);
}
});
// Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var AcceptCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin()],
propTypes: {
callType: React.PropTypes.string.isRequired,
callerId: React.PropTypes.string.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired,
// Only for use by the ui-showcase
showMenu: React.PropTypes.bool
},
getDefaultProps: function() {
return {
showMenu: false
};
},
componentDidMount: function() {
this.props.mozLoop.startAlerting();
},
componentWillUnmount: function() {
this.props.mozLoop.stopAlerting();
},
clickHandler: function(e) {
var target = e.target;
if (!target.classList.contains("btn-chevron")) {
this._hideDeclineMenu();
}
},
_handleAccept: function(callType) {
return function() {
this.props.dispatcher.dispatch(new sharedActions.AcceptCall({
callType: callType
}));
}.bind(this);
},
_handleDecline: function() {
this.props.dispatcher.dispatch(new sharedActions.DeclineCall({
blockCaller: false
}));
},
_handleDeclineBlock: function(e) {
this.props.dispatcher.dispatch(new sharedActions.DeclineCall({
blockCaller: true
}));
/* Prevent event propagation
* stop the click from reaching parent element */
e.stopPropagation();
e.preventDefault();
},
/*
* Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video
* answer button primarily, an audio call will flip them.
**/
_answerModeProps: function() {
var videoButton = {
handler: this._handleAccept(CALL_TYPES.AUDIO_VIDEO),
className: "fx-embedded-btn-icon-video",
tooltip: "incoming_call_accept_audio_video_tooltip"
};
var audioButton = {
handler: this._handleAccept(CALL_TYPES.AUDIO_ONLY),
className: "fx-embedded-btn-audio-small",
tooltip: "incoming_call_accept_audio_only_tooltip"
};
var props = {};
props.primary = videoButton;
props.secondary = audioButton;
// When video is not enabled on this call, we swap the buttons around.
if (this.props.callType === CALL_TYPES.AUDIO_ONLY) {
audioButton.className = "fx-embedded-btn-icon-audio";
videoButton.className = "fx-embedded-btn-video-small";
props.primary = audioButton;
props.secondary = videoButton;
}
return props;
},
render: function() {
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showMenu
});
return (
<div className="call-window">
<CallIdentifierView
peerIdentifier={this.props.callerId}
showIcons={true}
video={this.props.callType === CALL_TYPES.AUDIO_VIDEO} />
<div className="btn-group call-action-group">
<div className="fx-embedded-call-button-spacer"></div>
<div className="btn-chevron-menu-group">
<div className="btn-group-chevron">
<div className="btn-group">
<button className="btn btn-decline"
onClick={this._handleDecline}>
{mozL10n.get("incoming_call_cancel_button")}
</button>
<div className="btn-chevron"
onClick={this.toggleDropdownMenu}
ref="menu-button" />
</div>
<ul className={dropdownMenuClassesDecline}>
<li className="btn-block" onClick={this._handleDeclineBlock}>
{mozL10n.get("incoming_call_cancel_and_block_button")}
</li>
</ul>
</div>
</div>
<div className="fx-embedded-call-button-spacer"></div>
<AcceptCallButton mode={this._answerModeProps()} />
<div className="fx-embedded-call-button-spacer"></div>
</div>
</div>
);
}
});
/**
* Incoming call view accept button, renders different primary actions
* (answer with video / with audio only) based on the props received
**/
var AcceptCallButton = React.createClass({
propTypes: {
mode: React.PropTypes.object.isRequired
},
render: function() {
var mode = this.props.mode;
return (
<div className="btn-chevron-menu-group">
<div className="btn-group">
<button className="btn btn-accept"
onClick={mode.primary.handler}
title={mozL10n.get(mode.primary.tooltip)}>
<span className="fx-embedded-answer-btn-text">
{mozL10n.get("incoming_call_accept_button")}
</span>
<span className={mode.primary.className}></span>
</button>
<div className={mode.secondary.className}
onClick={mode.secondary.handler}
title={mozL10n.get(mode.secondary.tooltip)}>
</div>
</div>
</div>
);
}
});
/**
* View for pending conversations. Displays a cancel button and appropriate
* pending/ringing strings.
*/
var PendingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: {
callState: React.PropTypes.string,
contact: React.PropTypes.object,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
enableCancelButton: React.PropTypes.bool
},
getDefaultProps: function() {
return {
enableCancelButton: false
};
},
componentDidMount: function() {
this.play("ringtone", {loop: true});
},
cancelCall: function() {
this.props.dispatcher.dispatch(new sharedActions.CancelCall());
},
render: function() {
var cx = React.addons.classSet;
var pendingStateString;
if (this.props.callState === CALL_STATES.ALERTING) {
pendingStateString = mozL10n.get("call_progress_ringing_description");
} else {
pendingStateString = mozL10n.get("call_progress_connecting_description");
}
var btnCancelStyles = cx({
"btn": true,
"btn-cancel": true,
"disabled": !this.props.enableCancelButton
});
return (
<ConversationDetailView contact={this.props.contact}>
<p className="btn-label">{pendingStateString}</p>
<div className="btn-group call-action-group">
<button className={btnCancelStyles}
onClick={this.cancelCall}>
{mozL10n.get("initiate_call_cancel_button")}
</button>
</div>
</ConversationDetailView>
);
}
});
/**
* Used to display errors in direct calls and rooms to the user.
*/
var FailureInfoView = React.createClass({
propTypes: {
contact: React.PropTypes.object,
extraFailureMessage: React.PropTypes.string,
extraMessage: React.PropTypes.string,
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() {
switch (this.props.failureReason) {
case FAILURE_DETAILS.USER_UNAVAILABLE:
var contactDisplayName = _getContactDisplayName(this.props.contact);
if (contactDisplayName.length) {
return mozL10n.get(
"contact_unavailable_title",
{"contactName": contactDisplayName});
}
return mozL10n.get("generic_contact_unavailable_title");
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") });
case FAILURE_DETAILS.ICE_FAILED:
return mozL10n.get("ice_failure_message");
default:
return mozL10n.get("generic_failure_message");
}
},
_renderExtraMessage: function() {
if (this.props.extraMessage) {
return <p className="failure-info-extra">{this.props.extraMessage}</p>;
}
return null;
},
_renderExtraFailureMessage: function() {
if (this.props.extraFailureMessage) {
return <p className="failure-info-extra-failure">{this.props.extraFailureMessage}</p>;
}
return null;
},
render: function() {
return (
<div className="failure-info">
<div className="failure-info-logo" />
<h2 className="failure-info-message">{this._getMessage()}</h2>
{this._renderExtraMessage()}
{this._renderExtraFailureMessage()}
</div>
);
}
});
/**
* Direct Call failure view. Displayed when a call fails.
*/
var DirectCallFailureView = React.createClass({
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationStore"),
sharedMixins.AudioMixin,
sharedMixins.WindowCloseMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// This is used by the UI showcase.
emailLinkError: React.PropTypes.bool,
mozLoop: React.PropTypes.object.isRequired,
outgoing: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return _.extend({
emailLinkError: this.props.emailLinkError,
emailLinkButtonDisabled: false
}, this.getStoreState());
},
componentDidMount: function() {
this.play("failure");
this.listenTo(this.getStore(), "change:emailLink",
this._onEmailLinkReceived);
this.listenTo(this.getStore(), "error:emailLink",
this._onEmailLinkError);
},
componentWillUnmount: function() {
this.stopListening(this.getStore());
},
_onEmailLinkReceived: function() {
var emailLink = this.getStoreState().emailLink;
var contactEmail = _getPreferredEmail(this.state.contact).value;
sharedUtils.composeCallUrlEmail(emailLink, contactEmail, null, "callfailed");
this.closeWindow();
},
_onEmailLinkError: function() {
this.setState({
emailLinkError: true,
emailLinkButtonDisabled: false
});
},
retryCall: function() {
this.props.dispatcher.dispatch(new sharedActions.RetryCall());
},
cancelCall: function() {
this.props.dispatcher.dispatch(new sharedActions.CancelCall());
},
emailLink: function() {
this.setState({
emailLinkError: false,
emailLinkButtonDisabled: true
});
this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
roomName: _getContactDisplayName(this.state.contact)
}));
},
render: function() {
var cx = React.addons.classSet;
var retryClasses = cx({
btn: true,
"btn-info": true,
"btn-retry": true,
hide: !this.props.outgoing
});
var emailClasses = cx({
btn: true,
"btn-info": true,
"btn-email": true,
hide: !this.props.outgoing
});
var settingsMenuItems = [
{ id: "feedback" },
{ id: "help" }
];
var extraMessage;
if (this.props.outgoing) {
extraMessage = mozL10n.get("generic_failure_with_reason2");
}
var extraFailureMessage;
if (this.state.emailLinkError) {
extraFailureMessage = mozL10n.get("unable_retrieve_url");
}
return (
<div className="direct-call-failure">
<FailureInfoView
contact={this.state.contact}
extraFailureMessage={extraFailureMessage}
extraMessage={extraMessage}
failureReason={this.getStoreState().callStateReason}/>
<div className="btn-group call-action-group">
<button className="btn btn-cancel"
onClick={this.cancelCall}>
{mozL10n.get("cancel_button")}
</button>
<button className={retryClasses}
onClick={this.retryCall}>
{mozL10n.get("retry_call_button")}
</button>
<button className={emailClasses}
disabled={this.state.emailLinkButtonDisabled}
onClick={this.emailLink}>
{mozL10n.get("share_button3")}
</button>
</div>
<loop.shared.views.SettingsControlButton
menuBelow={true}
menuItems={settingsMenuItems}
mozLoop={this.props.mozLoop} />
</div>
);
}
});
var OngoingConversationView = React.createClass({
mixins: [
sharedMixins.MediaSetupMixin
],
propTypes: {
// local
audio: React.PropTypes.object,
chatWindowDetached: React.PropTypes.bool.isRequired,
// We pass conversationStore here rather than use the mixin, to allow
// easy configurability for the ui-showcase.
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
// This is used from the props rather than the state to make it easier for
// the ui-showcase.
mediaConnected: React.PropTypes.bool,
mozLoop: React.PropTypes.object,
remotePosterUrl: React.PropTypes.string,
remoteVideoEnabled: React.PropTypes.bool,
// local
video: React.PropTypes.object
},
getDefaultProps: function() {
return {
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true}
};
},
getInitialState: function() {
return this.props.conversationStore.getStoreState();
},
componentWillMount: function() {
this.props.conversationStore.on("change", function() {
this.setState(this.props.conversationStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.props.conversationStore.off("change", null, this);
},
componentDidMount: function() {
// 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.
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
})
}));
},
/**
* Hangs up the call.
*/
hangup: function() {
this.props.dispatcher.dispatch(
new sharedActions.HangupCall());
},
/**
* Used to control publishing a stream - i.e. to mute a stream
*
* @param {String} type The type of stream, e.g. "audio" or "video".
* @param {Boolean} enabled True to enable the stream, false otherwise.
*/
publishStream: function(type, enabled) {
this.props.dispatcher.dispatch(
new sharedActions.SetMute({
type: type,
enabled: enabled
}));
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a local
* stream is on its way from the camera?
*
* @returns {boolean}
* @private
*/
_isLocalLoading: function () {
return !this.state.localSrcMediaElement && !this.props.localPosterUrl;
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a remote
* stream is on its way from the other user?
*
* @returns {boolean}
* @private
*/
_isRemoteLoading: function() {
return !!(!this.state.remoteSrcMediaElement &&
!this.props.remotePosterUrl &&
!this.state.mediaConnected);
},
shouldRenderRemoteVideo: function() {
if (this.props.mediaConnected) {
// If remote video is not enabled, we're muted, so we'll show an avatar
// instead.
return this.props.remoteVideoEnabled;
}
// We're not yet connected, but we don't want to show the avatar, and in
// the common case, we'll just transition to the video.
return true;
},
render: function() {
// 'visible' and 'enabled' are true by default.
var settingsMenuItems = [
{
id: "edit",
visible: false,
enabled: false
},
{ id: "feedback" },
{ id: "help" }
];
return (
<div className="desktop-call-wrapper">
<sharedViews.MediaLayoutView
dispatcher={this.props.dispatcher}
displayScreenShare={false}
isLocalLoading={this._isLocalLoading()}
isRemoteLoading={this._isRemoteLoading()}
isScreenShareLoading={false}
localPosterUrl={this.props.localPosterUrl}
localSrcMediaElement={this.state.localSrcMediaElement}
localVideoMuted={!this.props.video.enabled}
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}
showContextRoomName={false}
useDesktopPaths={true}>
<sharedViews.ConversationToolbar
audio={this.props.audio}
dispatcher={this.props.dispatcher}
hangup={this.hangup}
mozLoop={this.props.mozLoop}
publishStream={this.publishStream}
settingsMenuItems={settingsMenuItems}
show={true}
showHangup={this.props.chatWindowDetached}
video={this.props.video} />
</sharedViews.MediaLayoutView>
</div>
);
}
});
/**
* Master View Controller for outgoing calls. This manages
* the different views that need displaying.
*/
var CallControllerView = React.createClass({
mixins: [
sharedMixins.AudioMixin,
sharedMixins.DocumentTitleMixin,
loop.store.StoreMixin("conversationStore"),
Backbone.Events
],
propTypes: {
chatWindowDetached: React.PropTypes.bool.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired,
onCallTerminated: React.PropTypes.func.isRequired
},
getInitialState: function() {
return this.getStoreState();
},
_closeWindow: function() {
window.close();
},
/**
* Returns true if the call is in a cancellable state, during call setup.
*/
_isCancellable: function() {
return this.state.callState !== CALL_STATES.INIT &&
this.state.callState !== CALL_STATES.GATHER;
},
_renderViewFromCallType: function() {
// For outgoing calls we can display the pending conversation view
// for any state that render() doesn't manage.
if (this.state.outgoing) {
return (<PendingConversationView
callState={this.state.callState}
contact={this.state.contact}
dispatcher={this.props.dispatcher}
enableCancelButton={this._isCancellable()} />);
}
// For incoming calls that are in accepting state, display the
// accept call view.
if (this.state.callState === CALL_STATES.ALERTING) {
return (<AcceptCallView
callType={this.state.callType}
callerId={this.state.callerId}
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop}
/>);
}
// Otherwise we're still gathering or connecting, so
// don't display anything.
return null;
},
componentDidUpdate: function(prevProps, prevState) {
// Handle timestamp and window closing only when the call has terminated.
if (prevState.callState === CALL_STATES.ONGOING &&
this.state.callState === CALL_STATES.FINISHED) {
this.props.onCallTerminated();
}
},
render: function() {
// Set the default title to the contact name or the callerId, note
// that views may override this, e.g. the feedback view.
if (this.state.contact) {
this.setTitle(_getContactDisplayName(this.state.contact));
} else {
this.setTitle(this.state.callerId || "");
}
switch (this.state.callState) {
case CALL_STATES.CLOSE: {
this._closeWindow();
return null;
}
case CALL_STATES.TERMINATED: {
return (<DirectCallFailureView
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop}
outgoing={this.state.outgoing} />);
}
case CALL_STATES.ONGOING: {
return (<OngoingConversationView
audio={{ enabled: !this.state.audioMuted, visible: true }}
chatWindowDetached={this.props.chatWindowDetached}
conversationStore={this.getStore()}
dispatcher={this.props.dispatcher}
mediaConnected={this.state.mediaConnected}
mozLoop={this.props.mozLoop}
remoteSrcMediaElement={this.state.remoteSrcMediaElement}
remoteVideoEnabled={this.state.remoteVideoEnabled}
video={{ enabled: !this.state.videoMuted, visible: true }} />
);
}
case CALL_STATES.FINISHED: {
this.play("terminated");
// When conversation ended we either display a feedback form or
// close the window. This is decided in the AppControllerView.
return null;
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.
return null;
}
default: {
return this._renderViewFromCallType();
}
}
}
});
return {
PendingConversationView: PendingConversationView,
CallIdentifierView: CallIdentifierView,
ConversationDetailView: ConversationDetailView,
_getContactDisplayName: _getContactDisplayName,
FailureInfoView: FailureInfoView,
DirectCallFailureView: DirectCallFailureView,
AcceptCallView: AcceptCallView,
OngoingConversationView: OngoingConversationView,
CallControllerView: CallControllerView
};
})(document.mozL10n || navigator.mozL10n);

View File

@ -75,6 +75,44 @@ loop.roomViews = (function(mozL10n) {
}
};
/**
* Used to display errors in direct calls and rooms to the user.
*/
var FailureInfoView = React.createClass({displayName: "FailureInfoView",
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() {
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") });
case FAILURE_DETAILS.ICE_FAILED:
return mozL10n.get("ice_failure_message");
default:
return mozL10n.get("generic_failure_message");
}
},
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.
*/
@ -110,8 +148,7 @@ loop.roomViews = (function(mozL10n) {
return (
React.createElement("div", {className: "room-failure"},
React.createElement(loop.conversationViews.FailureInfoView, {
failureReason: this.props.failureReason}),
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},
@ -820,6 +857,7 @@ loop.roomViews = (function(mozL10n) {
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
FailureInfoView: FailureInfoView,
RoomFailureView: RoomFailureView,
SocialShareDropdown: SocialShareDropdown,
DesktopRoomEditContextView: DesktopRoomEditContextView,

View File

@ -75,6 +75,44 @@ loop.roomViews = (function(mozL10n) {
}
};
/**
* Used to display errors in direct calls and rooms to the user.
*/
var FailureInfoView = React.createClass({
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() {
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") });
case FAILURE_DETAILS.ICE_FAILED:
return mozL10n.get("ice_failure_message");
default:
return mozL10n.get("generic_failure_message");
}
},
render: function() {
return (
<div className="failure-info">
<div className="failure-info-logo" />
<h2 className="failure-info-message">{this._getMessage()}</h2>
</div>
);
}
});
/**
* Something went wrong view. Displayed when there's a big problem.
*/
@ -110,8 +148,7 @@ loop.roomViews = (function(mozL10n) {
return (
<div className="room-failure">
<loop.conversationViews.FailureInfoView
failureReason={this.props.failureReason} />
<FailureInfoView failureReason={this.props.failureReason} />
<div className="btn-group call-action-group">
<button className="btn btn-info btn-rejoin"
onClick={this.handleRejoinCall}>
@ -820,6 +857,7 @@ loop.roomViews = (function(mozL10n) {
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
FailureInfoView: FailureInfoView,
RoomFailureView: RoomFailureView,
SocialShareDropdown: SocialShareDropdown,
DesktopRoomEditContextView: DesktopRoomEditContextView,

View File

@ -197,22 +197,19 @@ p {
}
.btn-accept,
.btn-success,
.btn-accept + .btn-chevron {
.btn-success {
background-color: #56b397;
border: 1px solid #56b397;
}
.btn-accept:hover,
.btn-success:hover,
.btn-accept + .btn-chevron:hover {
.btn-success:hover {
background-color: #50e3c2;
border: 1px solid #50e3c2;
}
.btn-accept:active,
.btn-success:active,
.btn-accept + .btn-chevron:active {
.btn-success:active {
background-color: #3aa689;
border: 1px solid #3aa689;
}
@ -221,81 +218,24 @@ p {
background-color: #f0ad4e;
}
.btn-cancel,
.btn-error,
.btn-decline,
.btn-hangup,
.btn-decline + .btn-chevron,
.btn-error + .btn-chevron {
.btn-hangup {
background-color: #d74345;
border: 1px solid #d74345;
}
.btn-cancel:hover,
.btn-error:hover,
.btn-decline:hover,
.btn-hangup:hover,
.btn-decline + .btn-chevron:hover,
.btn-error + .btn-chevron:hover {
.btn-hangup:hover {
background-color: #c53436;
border-color: #c53436;
}
.btn-cancel:active,
.btn-error:active,
.btn-decline:active,
.btn-hangup:active,
.btn-decline + .btn-chevron:active,
.btn-error + .btn-chevron:active {
.btn-hangup:active {
background-color: #ae2325;
border-color: #ae2325;
}
.btn-chevron {
width: 26px;
height: 26px;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
/* Groups together a button and a chevron */
.btn-group-chevron {
display: flex;
flex-direction: column;
flex: 1;
}
/* Groups together a button-group-chevron
* and the dropdown menu */
.btn-chevron-menu-group {
display: flex;
justify-content: space-between;
flex: 8;
}
.btn-group-chevron .btn {
border-radius: 2px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
flex: 2;
}
.btn + .btn-chevron,
.btn + .btn-chevron:hover,
.btn + .btn-chevron:active {
border-left: 1px solid rgba(255,255,255,.4);
background-image: url("../img/dropdown-inverse.png");
background-repeat: no-repeat;
background-position: center;
background-size: 10px;
}
@media (min-resolution: 2dppx) {
.btn-chevron {
background-image: url(../img/dropdown-inverse@2x.png);
}
}
.disabled, button[disabled] {
cursor: not-allowed;
pointer-events: none;
@ -308,14 +248,6 @@ p {
justify-content: center;
}
.btn-chevron-menu-group .btn {
flex: 1;
border-radius: 2px;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
line-height: 0.9rem;
}
/* Alerts/Notifications */
.notificationContainer {
border-bottom: 2px solid #E9E9E9;
@ -371,9 +303,7 @@ p {
/* Misc */
.call-url,
.overflow-text-ellipsis,
.standalone-call-btn-text,
.fx-embedded-answer-btn-text {
.standalone-call-btn-text {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
@ -409,9 +339,7 @@ p {
}
.icon,
.icon-small,
.icon-audio,
.icon-video {
.icon-small {
background-size: 20px;
background-repeat: no-repeat;
vertical-align: top;
@ -432,24 +360,6 @@ p {
background-size: 10px;
}
.icon-video {
background-image: url("../img/video-inverse-14x14.png");
}
.icon-audio {
background-image: url("../img/audio-default-16x16@1.5x.png");
}
@media (min-resolution: 2dppx) {
.icon-video {
background-image: url("../img/video-inverse-14x14@2x.png");
}
.icon-audio {
background-image: url("../img/audio-default-16x16@2x.png");
}
}
/*
* Platform specific styles
* The UI should match the user OS

View File

@ -103,57 +103,8 @@ html[dir="rtl"] .conversation-toolbar-btn-box.btn-edit-entry {
float: left;
}
.fx-embedded-answer-btn-text {
/* always leave space for the icon (width and margin) */
max-width: calc(100% - 24px - .2rem);
height: 22px;
line-height: 22px;
}
.fx-embedded-btn-icon-video,
.fx-embedded-btn-icon-audio {
width: 24px;
height: 24px;
display: inline-block;
background-repeat: no-repeat;
background-position: center;
background-size: 14px 14px;
border-radius: 50%;
-moz-margin-start: .2rem;
}
/* conversationViews.jsx */
.fx-embedded-btn-icon-video,
.fx-embedded-btn-video-small,
.fx-embedded-tiny-video-icon {
background-image: url("../img/video-inverse-14x14.png");
}
.fx-embedded-btn-icon-audio,
.fx-embedded-btn-audio-small,
.fx-embedded-tiny-audio-icon {
background-image: url("../img/audio-inverse-14x14.png");
}
.fx-embedded-btn-audio-small,
.fx-embedded-btn-video-small {
width: 26px;
height: 26px;
border-left: 1px solid rgba(255,255,255,.4);
background-color: #56b397;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
background-position: center;
background-repeat: no-repeat;
cursor: pointer;
}
.fx-embedded-btn-video-small:hover,
.fx-embedded-btn-audio-small:hover {
background-color: #50e3c2;
}
.conversation-toolbar .btn-hangup {
background-image: url("../img/svg/exit.svg");
border: 0;
@ -229,23 +180,6 @@ html[dir="rtl"] .conversation-toolbar-btn-box.btn-edit-entry {
/* General Call (incoming or outgoing). */
/* XXX call-window currently relates to multiple things like the AcceptCallView,
DirectCallFailureView, PendingConversationView. It doesn't relate to the direct
call media views. We should probably make this more explicit at some stage. */
/*
* Height matches the height of the docked window
* but the UI breaks when you pop out
* Bug 1040985
*/
.call-window {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
min-height: 230px;
}
.call-action-group {
display: flex;
padding: 0 4px;
@ -310,7 +244,6 @@ html[dir="rtl"] .conversation-toolbar-btn-box.btn-edit-entry {
display: block;
}
.direct-call-failure,
.room-failure {
/* This flex allows us to not calculate the height of the logo area
versus the buttons */
@ -322,25 +255,21 @@ html[dir="rtl"] .conversation-toolbar-btn-box.btn-edit-entry {
height: 100%;
}
.direct-call-failure > .call-action-group,
.room-failure > .call-action-group {
flex: none;
margin: 1rem 0 2rem;
}
.direct-call-failure > .failure-info,
.room-failure > .failure-info {
flex: auto;
}
.direct-call-failure > .settings-control,
.room-failure > .settings-control {
position: absolute;
top: 1rem;
right: .5rem;
}
html[dir="rtl"] .direct-call-failure > .settings-control,
html[dir="rtl"] .room-failure > .settings-control {
left: .5rem;
right: auto;
@ -390,51 +319,6 @@ html[dir="rtl"] .room-failure > .settings-control {
color: #f00;
}
.fx-embedded-call-button-spacer {
display: flex;
flex: 1;
}
/*
* Dropdown menu hidden behind a chevron
*
* .native-dropdown-menu Generic class, contains common styles
* .conversation-window-dropdown Dropdown menu for answer/decline/block options
*/
.native-dropdown-menu {
/* Should match a native select menu */
padding: 0;
position: absolute; /* element can be wider than the parent */
background: #fff;
margin: 0;
box-shadow: 0 4px 5px rgba(30,30,30,.3);
border-style: solid;
border-width: 1px 1px 1px 2px;
border-color: #aaa #111 #111 #aaa;
}
.native-dropdown-menu li {
list-style: none;
cursor: pointer;
color: #000;
}
.native-dropdown-menu li:hover {
color: #fff;
background-color: #111;
}
.conversation-window-dropdown {
bottom: 4rem;
}
.conversation-window-dropdown > li {
padding: .2rem;
font-size: 1rem;
white-space: nowrap;
}
.screen-share-menu.dropdown-menu,
.settings-menu.dropdown-menu {
bottom: 3.1rem;
@ -567,79 +451,11 @@ html[dir="rtl"] .settings-menu.dropdown-menu {
opacity: 0.25;
}
.fx-embedded-call-identifier {
display: inline;
width: 100%;
padding: 1.2em;
}
.fx-embedded-call-identifier-item {
height: 50px;
}
.fx-embedded-call-identifier-avatar {
max-width: 50px;
min-width: 50px;
background: #ccc;
border-radius: 50%;
background-image: url("../img/audio-call-avatar.svg");
background-repeat: no-repeat;
background-color: #4ba6e7;
background-size: contain;
overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(255,255,255,.3);
float: left;
-moz-margin-end: 1em;
}
.fx-embedded-call-identifier-text {
font-weight: bold;
}
.fx-embedded-call-identifier-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
-moz-margin-start: 1em;
}
.fx-embedded-conversation-timestamp {
font-size: .6rem;
line-height: 17px;
display: inline-block;
vertical-align: top;
}
.fx-embedded-call-detail {
padding-top: 0.6em;
}
.fx-embedded-tiny-video-icon {
margin: 0 .8em;
}
.fx-embedded-tiny-audio-icon,
.fx-embedded-tiny-video-icon {
width: 24px;
height: 24px;
background-color: #00a9dc;
display: inline-block;
background-repeat: no-repeat;
background-position: center;
border-radius: 50%;
}
.fx-embedded-tiny-video-icon.muted {
background-color: rgba(0,0,0,.2)
}
/* Force full height on all parents up to the video elements
* this way we can ensure the aspect ratio and use height 100%
* on the video element
* */
html, .fx-embedded, #main,
.desktop-call-wrapper,
.desktop-room-wrapper {
height: 100%;
}
@ -943,7 +759,6 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
height: calc(100% - 300px);
}
.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
.desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
height: calc(100% - 150px);
}
@ -1070,7 +885,6 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
}
.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
.desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
/* This is temp, to echo the .media-wrapper > .text-chat-view above */
height: 30%;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 B

View File

@ -1,676 +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.store = loop.store || {};
(function() {
"use strict";
var sharedActions = loop.shared.actions;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
/**
* Websocket states taken from:
* https://docs.services.mozilla.com/loop/apis.html#call-progress-state-change-progress
*/
var WS_STATES = loop.store.WS_STATES = {
// The call is starting, and the remote party is not yet being alerted.
INIT: "init",
// The called party is being alerted.
ALERTING: "alerting",
// The call is no longer being set up and has been aborted for some reason.
TERMINATED: "terminated",
// The called party has indicated that he has answered the call,
// but the media is not yet confirmed.
CONNECTING: "connecting",
// One of the two parties has indicated successful media set up,
// but the other has not yet.
HALF_CONNECTED: "half-connected",
// Both endpoints have reported successfully establishing media.
CONNECTED: "connected"
};
var CALL_STATES = loop.store.CALL_STATES = {
// The initial state of the view.
INIT: "cs-init",
// The store is gathering the call data from the server.
GATHER: "cs-gather",
// The initial data has been gathered, the websocket is connecting, or has
// connected, and waiting for the other side to connect to the server.
CONNECTING: "cs-connecting",
// The websocket has received information that we're now alerting
// the peer.
ALERTING: "cs-alerting",
// The call is ongoing.
ONGOING: "cs-ongoing",
// The call ended successfully.
FINISHED: "cs-finished",
// The user has finished with the window.
CLOSE: "cs-close",
// The call was terminated due to an issue during connection.
TERMINATED: "cs-terminated"
};
/**
* Conversation store.
*
* @param {loop.Dispatcher} dispatcher The dispatcher for dispatching actions
* and registering to consume actions.
* @param {Object} options Options object:
* - {client} client The client object.
* - {mozLoop} mozLoop The MozLoop API object.
* - {loop.OTSdkDriver} loop.OTSdkDriver The SDK Driver
*/
loop.store.ConversationStore = loop.store.createStore({
// Further actions are registered in setupWindowData when
// we know what window type this is.
actions: [
"setupWindowData"
],
getInitialStoreState: function() {
return {
// The id of the window. Currently used for getting the window id.
windowId: undefined,
// The current state of the call
callState: CALL_STATES.INIT,
// The reason if a call was terminated
callStateReason: undefined,
// True if the call is outgoing, false if not, undefined if unknown
outgoing: undefined,
// The contact being called for outgoing calls
contact: undefined,
// The call type for the call.
// XXX Don't hard-code, this comes from the data in bug 1072323
callType: CALL_TYPES.AUDIO_VIDEO,
// A link for emailing once obtained from the server
emailLink: undefined,
// Call Connection information
// The call id from the loop-server
callId: undefined,
// The caller id of the contacting side
callerId: undefined,
// True if media has been connected both-ways.
mediaConnected: false,
// The connection progress url to connect the websocket
progressURL: undefined,
// The websocket token that allows connection to the progress url
websocketToken: undefined,
// SDK API key
apiKey: undefined,
// SDK session ID
sessionId: undefined,
// SDK session token
sessionToken: undefined,
// If the local audio is muted
audioMuted: false,
// If the local video is muted
videoMuted: false,
remoteVideoEnabled: false
};
},
/**
* Handles initialisation of the store.
*
* @param {Object} options Options object.
*/
initialize: function(options) {
options = options || {};
if (!options.client) {
throw new Error("Missing option client");
}
if (!options.sdkDriver) {
throw new Error("Missing option sdkDriver");
}
if (!options.mozLoop) {
throw new Error("Missing option mozLoop");
}
this.client = options.client;
this.sdkDriver = options.sdkDriver;
this.mozLoop = options.mozLoop;
this._isDesktop = options.isDesktop || false;
},
/**
* Handles the connection failure action, setting the state to
* terminated.
*
* @param {sharedActions.ConnectionFailure} actionData The action data.
*/
connectionFailure: function(actionData) {
this._endSession();
this.setStoreState({
callState: CALL_STATES.TERMINATED,
callStateReason: actionData.reason
});
},
/**
* Handles the connection progress action, setting the next state
* appropriately.
*
* @param {sharedActions.ConnectionProgress} actionData The action data.
*/
connectionProgress: function(actionData) {
var state = this.getStoreState();
switch(actionData.wsState) {
case WS_STATES.INIT: {
if (state.callState === CALL_STATES.GATHER) {
this.setStoreState({callState: CALL_STATES.CONNECTING});
}
break;
}
case WS_STATES.ALERTING: {
this.setStoreState({callState: CALL_STATES.ALERTING});
break;
}
case WS_STATES.CONNECTING: {
if (state.outgoing) {
// We can start the connection right away if we're the outgoing
// call because this is the only way we know the other peer has
// accepted.
this._startCallConnection();
} else if (state.callState !== CALL_STATES.ONGOING) {
console.error("Websocket unexpectedly changed to next state whilst waiting for call acceptance.");
// Abort and close the window.
this.declineCall(new sharedActions.DeclineCall({blockCaller: false}));
}
break;
}
case WS_STATES.HALF_CONNECTED:
case WS_STATES.CONNECTED: {
if (this.getStoreState("callState") !== CALL_STATES.ONGOING) {
console.error("Unexpected websocket state received in wrong callState");
this.setStoreState({callState: CALL_STATES.ONGOING});
}
break;
}
default: {
console.error("Unexpected websocket state passed to connectionProgress:",
actionData.wsState);
}
}
},
setupWindowData: function(actionData) {
var windowType = actionData.type;
if (windowType !== "outgoing" &&
windowType !== "incoming") {
// Not for this store, don't do anything.
return;
}
this.dispatcher.register(this, [
"connectionFailure",
"connectionProgress",
"acceptCall",
"declineCall",
"connectCall",
"hangupCall",
"remotePeerDisconnected",
"cancelCall",
"retryCall",
"mediaConnected",
"setMute",
"fetchRoomEmailLink",
"mediaStreamCreated",
"mediaStreamDestroyed",
"remoteVideoStatus",
"windowUnload"
]);
this.setStoreState({
apiKey: actionData.apiKey,
callerId: actionData.callerId,
callId: actionData.callId,
callState: CALL_STATES.GATHER,
callType: actionData.callType,
contact: actionData.contact,
outgoing: windowType === "outgoing",
progressURL: actionData.progressURL,
sessionId: actionData.sessionId,
sessionToken: actionData.sessionToken,
videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY,
websocketToken: actionData.websocketToken,
windowId: actionData.windowId
});
if (this.getStoreState("outgoing")) {
this._setupOutgoingCall();
} else {
this._setupIncomingCall();
}
},
/**
* Handles starting a call connection - connecting the session on the
* sdk, and setting the state appropriately.
*/
_startCallConnection: function() {
var state = this.getStoreState();
this.sdkDriver.connectSession({
apiKey: state.apiKey,
sessionId: state.sessionId,
sessionToken: state.sessionToken,
sendTwoWayMediaTelemetry: state.outgoing // only one side of the call
});
this.mozLoop.addConversationContext(
state.windowId,
state.sessionId,
state.callId);
this.setStoreState({callState: CALL_STATES.ONGOING});
},
/**
* Accepts an incoming call.
*
* @param {sharedActions.AcceptCall} actionData
*/
acceptCall: function(actionData) {
if (this.getStoreState("outgoing")) {
console.error("Received AcceptCall action in outgoing call state");
return;
}
this.setStoreState({
callType: actionData.callType,
videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY
});
this._websocket.accept();
this._startCallConnection();
},
/**
* Declines an incoming call.
*
* @param {sharedActions.DeclineCall} actionData
*/
declineCall: function(actionData) {
if (actionData.blockCaller) {
this.mozLoop.calls.blockDirectCaller(this.getStoreState("callerId"),
function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
}
this._websocket.decline();
// Now we've declined, end the session and close the window.
this._endSession();
this.setStoreState({callState: CALL_STATES.CLOSE});
},
/**
* Handles the connect call action, this saves the appropriate
* data and starts the connection for the websocket to notify the
* server of progress.
*
* @param {sharedActions.ConnectCall} actionData The action data.
*/
connectCall: function(actionData) {
this.setStoreState(actionData.sessionData);
this._connectWebSocket();
},
/**
* Hangs up an ongoing call.
*/
hangupCall: function() {
if (this._websocket) {
// Let the server know the user has hung up.
this._websocket.mediaFail();
}
this._endSession();
this.setStoreState({
callState: this._storeState.callState === CALL_STATES.ONGOING ?
CALL_STATES.FINISHED : CALL_STATES.CLOSE
});
},
/**
* The remote peer disconnected from the session.
*
* @param {sharedActions.RemotePeerDisconnected} actionData
*/
remotePeerDisconnected: function(actionData) {
this._endSession();
// If the peer hungup, we end normally, otherwise
// we treat this as a call failure.
if (actionData.peerHungup) {
this.setStoreState({callState: CALL_STATES.FINISHED});
} else {
this.setStoreState({
callState: CALL_STATES.TERMINATED,
callStateReason: "peerNetworkDisconnected"
});
}
},
/**
* Cancels a call. This can happen for incoming or outgoing calls.
* Although the user doesn't "cancel" an incoming call, it may be that
* the remote peer cancelled theirs before the incoming call was accepted.
*/
cancelCall: function() {
if (this.getStoreState("outgoing")) {
var callState = this.getStoreState("callState");
if (this._websocket &&
(callState === CALL_STATES.CONNECTING ||
callState === CALL_STATES.ALERTING)) {
// Let the server know the user has hung up.
this._websocket.cancel();
}
}
this._endSession();
this.setStoreState({callState: CALL_STATES.CLOSE});
},
/**
* Retries a call
*/
retryCall: function() {
var callState = this.getStoreState("callState");
if (callState !== CALL_STATES.TERMINATED) {
console.error("Unexpected retry in state", callState);
return;
}
this.setStoreState({callState: CALL_STATES.GATHER});
if (this.getStoreState("outgoing")) {
this._setupOutgoingCall();
}
},
/**
* Notifies that all media is now connected
*/
mediaConnected: function() {
this._websocket.mediaUp();
this.setStoreState({mediaConnected: true});
},
/**
* Records the mute state for the stream.
*
* @param {sharedActions.setMute} actionData The mute state for the stream type.
*/
setMute: function(actionData) {
var newState = {};
newState[actionData.type + "Muted"] = !actionData.enabled;
this.setStoreState(newState);
},
/**
* Fetches a new room URL intended to be sent over email when a contact
* can't be reached.
*/
fetchRoomEmailLink: function(actionData) {
this.mozLoop.rooms.create({
decryptedContext: {
roomName: actionData.roomName
},
maxSize: loop.store.MAX_ROOM_CREATION_SIZE,
expiresIn: loop.store.DEFAULT_EXPIRES_IN
}, function(err, createdRoomData) {
var buckets = this.mozLoop.ROOM_CREATE;
if (err) {
this.trigger("error:emailLink");
this.mozLoop.telemetryAddValue("LOOP_ROOM_CREATE", buckets.CREATE_FAIL);
return;
}
this.setStoreState({"emailLink": createdRoomData.roomUrl});
this.mozLoop.telemetryAddValue("LOOP_ROOM_CREATE", buckets.CREATE_SUCCESS);
}.bind(this));
},
/**
* Handles a media stream being created. This may be a local or a remote stream.
*
* @param {sharedActions.MediaStreamCreated} actionData
*/
mediaStreamCreated: function(actionData) {
if (actionData.isLocal) {
this.setStoreState({
localVideoEnabled: actionData.hasVideo,
localSrcMediaElement: actionData.srcMediaElement
});
return;
}
this.setStoreState({
remoteVideoEnabled: actionData.hasVideo,
remoteSrcMediaElement: actionData.srcMediaElement
});
},
/**
* Handles a media stream being destroyed. This may be a local or a remote stream.
*
* @param {sharedActions.MediaStreamDestroyed} actionData
*/
mediaStreamDestroyed: function(actionData) {
if (actionData.isLocal) {
this.setStoreState({
localSrcMediaElement: null
});
return;
}
this.setStoreState({
remoteSrcMediaElement: null
});
},
/**
* Handles a remote stream having video enabled or disabled.
*
* @param {sharedActions.RemoteVideoStatus} actionData
*/
remoteVideoStatus: function(actionData) {
this.setStoreState({
remoteVideoEnabled: actionData.videoEnabled
});
},
/**
* Called when the window is unloaded, either by code, or by the user
* explicitly closing it. Expected to do any necessary housekeeping, such
* as shutting down the call cleanly and adding any relevant telemetry data.
*/
windowUnload: function() {
if (!this.getStoreState("outgoing") &&
this.getStoreState("callState") === CALL_STATES.ALERTING &&
this._websocket) {
this._websocket.decline();
}
this._endSession();
},
/**
* Sets up an incoming call. All we really need to do here is
* to connect the websocket, as we've already got all the information
* when the window opened.
*/
_setupIncomingCall: function() {
this._connectWebSocket();
},
/**
* Obtains the outgoing call data from the server and handles the
* result.
*/
_setupOutgoingCall: function() {
var contactAddresses = [];
var contact = this.getStoreState("contact");
this.mozLoop.calls.setCallInProgress(this.getStoreState("windowId"));
function appendContactValues(property, strip) {
if (contact.hasOwnProperty(property)) {
contact[property].forEach(function(item) {
if (strip) {
contactAddresses.push(item.value
.replace(/^(\+)?(.*)$/g, function(m, prefix, number) {
return (prefix || "") + number.replace(/[\D]+/g, "");
}));
} else {
contactAddresses.push(item.value);
}
});
}
}
appendContactValues("email");
appendContactValues("tel", true);
this.client.setupOutgoingCall(contactAddresses,
this.getStoreState("callType"),
function(err, result) {
if (err) {
console.error("Failed to get outgoing call data", err);
var failureReason = FAILURE_DETAILS.UNKNOWN;
if (err.errno === REST_ERRNOS.USER_UNAVAILABLE) {
failureReason = FAILURE_DETAILS.USER_UNAVAILABLE;
}
this.dispatcher.dispatch(
new sharedActions.ConnectionFailure({reason: failureReason}));
return;
}
// Success, dispatch a new action.
this.dispatcher.dispatch(
new sharedActions.ConnectCall({sessionData: result}));
}.bind(this)
);
},
/**
* Sets up and connects the websocket to the server. The websocket
* deals with sending and obtaining status via the server about the
* setup of the call.
*/
_connectWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.getStoreState("progressURL"),
callId: this.getStoreState("callId"),
websocketToken: this.getStoreState("websocketToken")
});
this._websocket.promiseConnect().then(
function(progressState) {
this.dispatcher.dispatch(new sharedActions.ConnectionProgress({
// This is the websocket call state, i.e. waiting for the
// other end to connect to the server.
wsState: progressState
}));
}.bind(this),
function(error) {
console.error("Websocket failed to connect", error);
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: "websocket-setup"
}));
}.bind(this)
);
this.listenTo(this._websocket, "progress", this._handleWebSocketProgress);
},
/**
* Ensures the session is ended and the websocket is disconnected.
*/
_endSession: function(nextState) {
this.sdkDriver.disconnectSession();
if (this._websocket) {
this.stopListening(this._websocket);
// Now close the websocket.
this._websocket.close();
delete this._websocket;
}
this.mozLoop.calls.clearCallInProgress(
this.getStoreState("windowId"));
},
/**
* If we hit any of the termination reasons, and the user hasn't accepted
* then it seems reasonable to close the window/abort the incoming call.
*
* If the user has accepted the call, and something's happened, display
* the call failed view.
*
* https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
*
* For outgoing calls, we treat all terminations as failures.
*
* @param {Object} progressData The progress data received from the websocket.
* @param {String} previousState The previous state the websocket was in.
*/
_handleWebSocketStateTerminated: function(progressData, previousState) {
if (this.getStoreState("outgoing") ||
(previousState !== WS_STATES.INIT &&
previousState !== WS_STATES.ALERTING)) {
// For outgoing calls we can treat everything as connection failure.
// XXX We currently fallback to the websocket reason, but really these should
// be fully migrated to use FAILURE_DETAILS, so as not to expose websocket
// states outside of this store. Bug 1124384 should help to fix this.
var reason = progressData.reason;
if (reason === WEBSOCKET_REASONS.REJECT ||
reason === WEBSOCKET_REASONS.BUSY) {
reason = FAILURE_DETAILS.USER_UNAVAILABLE;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: reason
}));
return;
}
this.dispatcher.dispatch(new sharedActions.CancelCall());
},
/**
* Used to handle any progressed received from the websocket. This will
* dispatch new actions so that the data can be handled appropriately.
*
* @param {Object} progressData The progress data received from the websocket.
* @param {String} previousState The previous state the websocket was in.
*/
_handleWebSocketProgress: function(progressData, previousState) {
switch(progressData.state) {
case WS_STATES.TERMINATED: {
this._handleWebSocketStateTerminated(progressData, previousState);
break;
}
default: {
this.dispatcher.dispatch(new sharedActions.ConnectionProgress({
wsState: progressData.state
}));
break;
}
}
}
});
})();

View File

@ -62,16 +62,6 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
ROOM_FULL: 202
};
var WEBSOCKET_REASONS = {
ANSWERED_ELSEWHERE: "answered-elsewhere",
BUSY: "busy",
CANCEL: "cancel",
CLOSED: "closed",
MEDIA_FAIL: "media-fail",
REJECT: "reject",
TIMEOUT: "timeout"
};
var FAILURE_DETAILS = {
MEDIA_DENIED: "reason-media-denied",
NO_MEDIA: "reason-no-media",
@ -786,7 +776,6 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
CHAT_CONTENT_TYPES: CHAT_CONTENT_TYPES,
FAILURE_DETAILS: FAILURE_DETAILS,
REST_ERRNOS: REST_ERRNOS,
WEBSOCKET_REASONS: WEBSOCKET_REASONS,
STREAM_PROPERTIES: STREAM_PROPERTIES,
SCREEN_SHARE_STATES: SCREEN_SHARE_STATES,
ROOM_INFO_FAILURES: ROOM_INFO_FAILURES,

View File

@ -1118,7 +1118,6 @@ loop.shared.views = (function(_, mozL10n) {
this.state.localMediaAboslutelyPositioned ?
this.renderLocalVideo() : null,
this.props.children
),
React.createElement("div", {className: screenShareStreamClasses},
React.createElement(MediaView, {displayAvatar: false,

View File

@ -1118,7 +1118,6 @@ loop.shared.views = (function(_, mozL10n) {
{ this.state.localMediaAboslutelyPositioned ?
this.renderLocalVideo() : null }
{ this.props.children }
</div>
<div className={screenShareStreamClasses}>
<MediaView displayAvatar={false}

View File

@ -1,299 +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.CallConnectionWebSocket = (function() {
"use strict";
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
// Response timeout is 5 seconds as per API.
var kResponseTimeout = 5000;
/**
* Handles a websocket specifically for a call connection.
*
* There should be one of these created for each call connection.
*
* options items:
* - url The url of the websocket to connect to.
* - callId The call id for the call
* - websocketToken The authentication token for the websocket
*
* @param {Object} options The options for this websocket.
*/
function CallConnectionWebSocket(options) {
this.options = options || {};
if (!this.options.url) {
throw new Error("No url in options");
}
if (!this.options.callId) {
throw new Error("No callId in options");
}
if (!this.options.websocketToken) {
throw new Error("No websocketToken in options");
}
this._lastServerState = "init";
// Set loop.debug.sdk to true in the browser, or standalone:
// localStorage.setItem("debug.websocket", true);
this._debugWebSocket =
loop.shared.utils.getBoolPreference("debug.websocket");
_.extend(this, Backbone.Events);
}
CallConnectionWebSocket.prototype = {
/**
* Start the connection to the websocket.
*
* @return {Promise} A promise that resolves when the websocket
* server connection is open and "hello"s have been
* exchanged. It is rejected if there is a failure in
* connection or the initial exchange of "hello"s.
*/
promiseConnect: function() {
var promise = new Promise(
function(resolve, reject) {
this.socket = new WebSocket(this.options.url);
this.socket.onopen = this._onopen.bind(this);
this.socket.onmessage = this._onmessage.bind(this);
this.socket.onerror = this._onerror.bind(this);
this.socket.onclose = this._onclose.bind(this);
var timeout = setTimeout(function() {
if (this.connectDetails && this.connectDetails.reject) {
this.connectDetails.reject(WEBSOCKET_REASONS.TIMEOUT);
this._clearConnectionFlags();
}
}.bind(this), kResponseTimeout);
this.connectDetails = {
resolve: resolve,
reject: reject,
timeout: timeout
};
}.bind(this));
return promise;
},
/**
* Closes the websocket. This shouldn't be the normal action as the server
* will normally close the socket. Only in bad error cases, or where we need
* to close the socket just before closing the window (to avoid an error)
* should we call this.
*/
close: function() {
if (this.socket) {
this.socket.close();
}
},
_clearConnectionFlags: function() {
clearTimeout(this.connectDetails.timeout);
delete this.connectDetails;
},
/**
* Internal function called to resolve the connection promise.
*
* It will log an error if no promise is found.
*
* @param {String} progressState The current state of progress of the
* websocket.
*/
_completeConnection: function(progressState) {
if (this.connectDetails && this.connectDetails.resolve) {
this.connectDetails.resolve(progressState);
this._clearConnectionFlags();
return;
}
console.error("Failed to complete connection promise - no promise available");
},
/**
* Checks if the websocket is connecting, and rejects the connection
* promise if appropriate.
*
* @param {Object} event The event to reject the promise with if
* appropriate.
*/
_checkConnectionFailed: function(event) {
if (this.connectDetails && this.connectDetails.reject) {
this.connectDetails.reject(event);
this._clearConnectionFlags();
return true;
}
return false;
},
/**
* Notifies the server that the user has declined the call.
*/
decline: function() {
this._send({
messageType: "action",
event: "terminate",
reason: WEBSOCKET_REASONS.REJECT
});
},
/**
* Notifies the server that the user has accepted the call.
*/
accept: function() {
this._send({
messageType: "action",
event: "accept"
});
},
/**
* Notifies the server that the outgoing media is up, and the
* incoming media is being received.
*/
mediaUp: function() {
this._send({
messageType: "action",
event: "media-up"
});
},
/**
* Notifies the server that the outgoing call is cancelled by the
* user.
*/
cancel: function() {
this._send({
messageType: "action",
event: "terminate",
reason: WEBSOCKET_REASONS.CANCEL
});
},
/**
* Notifies the server that something failed during setup.
*/
mediaFail: function() {
this._send({
messageType: "action",
event: "terminate",
reason: WEBSOCKET_REASONS.MEDIA_FAIL
});
},
/**
* Sends data on the websocket.
*
* @param {Object} data The data to send.
*/
_send: function(data) {
this._log("WS Sending", data);
this.socket.send(JSON.stringify(data));
},
/**
* Used to determine if the server state is in a completed state, i.e.
* the server has determined the connection is terminated or connected.
*
* @return True if the last received state is terminated or connected.
*/
get _stateIsCompleted() {
return this._lastServerState === "terminated" ||
this._lastServerState === "connected";
},
/**
* Called when the socket is open. Automatically sends a "hello"
* message to the server.
*/
_onopen: function() {
// Auto-register with the server.
this._send({
messageType: "hello",
callId: this.options.callId,
auth: this.options.websocketToken
});
},
/**
* Called when a message is received from the server.
*
* @param {Object} event The websocket onmessage event.
*/
_onmessage: function(event) {
var msgData;
try {
msgData = JSON.parse(event.data);
} catch (x) {
console.error("Error parsing received message:", x);
return;
}
this._log("WS Receiving", event.data);
var previousState = this._lastServerState;
this._lastServerState = msgData.state;
switch(msgData.messageType) {
case "hello":
this._completeConnection(msgData.state);
break;
case "progress":
this.trigger("progress:" + msgData.state);
this.trigger("progress", msgData, previousState);
break;
}
},
/**
* Called when there is an error on the websocket.
*
* @param {Object} event A simple error event.
*/
_onerror: function(event) {
this._log("WS Error", event);
if (!this._stateIsCompleted &&
!this._checkConnectionFailed(event)) {
this.trigger("error", event);
}
},
/**
* Called when the websocket is closed.
*
* @param {CloseEvent} event The details of the websocket closing.
*/
_onclose: function(event) {
this._log("WS Close", event);
// If the websocket goes away when we're not in a completed state
// then its an error. So we either pass it back via the connection
// promise, or trigger the closed event.
if (!this._stateIsCompleted &&
!this._checkConnectionFailed(event)) {
this.trigger("closed", event);
}
},
/**
* Logs debug to the console.
*
* Parameters: same as console.log
*/
_log: function() {
if (this._debugWebSocket) {
console.log.apply(console, arguments);
}
}
};
return CallConnectionWebSocket;
})();

View File

@ -11,13 +11,11 @@ browser.jar:
content/browser/loop/libs/l10n.js (content/libs/l10n.js)
# Desktop script
content/browser/loop/js/client.js (content/js/client.js)
content/browser/loop/js/conversation.js (content/js/conversation.js)
content/browser/loop/js/conversationAppStore.js (content/js/conversationAppStore.js)
content/browser/loop/js/otconfig.js (content/js/otconfig.js)
content/browser/loop/js/panel.js (content/js/panel.js)
content/browser/loop/js/contacts.js (content/js/contacts.js)
content/browser/loop/js/conversationViews.js (content/js/conversationViews.js)
content/browser/loop/js/roomStore.js (content/js/roomStore.js)
content/browser/loop/js/roomViews.js (content/js/roomViews.js)
content/browser/loop/js/feedbackViews.js (content/js/feedbackViews.js)
@ -43,18 +41,12 @@ browser.jar:
content/browser/loop/shared/img/sad_hello_icon_64x64.svg (content/shared/img/sad_hello_icon_64x64.svg)
content/browser/loop/shared/img/chatbubble-arrow-left.svg (content/shared/img/chatbubble-arrow-left.svg)
content/browser/loop/shared/img/chatbubble-arrow-right.svg (content/shared/img/chatbubble-arrow-right.svg)
content/browser/loop/shared/img/audio-inverse-14x14.png (content/shared/img/audio-inverse-14x14.png)
content/browser/loop/shared/img/audio-inverse-14x14@2x.png (content/shared/img/audio-inverse-14x14@2x.png)
content/browser/loop/shared/img/facemute-14x14.png (content/shared/img/facemute-14x14.png)
content/browser/loop/shared/img/facemute-14x14@2x.png (content/shared/img/facemute-14x14@2x.png)
content/browser/loop/shared/img/hangup-inverse-14x14.png (content/shared/img/hangup-inverse-14x14.png)
content/browser/loop/shared/img/hangup-inverse-14x14@2x.png (content/shared/img/hangup-inverse-14x14@2x.png)
content/browser/loop/shared/img/mute-inverse-14x14.png (content/shared/img/mute-inverse-14x14.png)
content/browser/loop/shared/img/mute-inverse-14x14@2x.png (content/shared/img/mute-inverse-14x14@2x.png)
content/browser/loop/shared/img/video-inverse-14x14.png (content/shared/img/video-inverse-14x14.png)
content/browser/loop/shared/img/video-inverse-14x14@2x.png (content/shared/img/video-inverse-14x14@2x.png)
content/browser/loop/shared/img/dropdown-inverse.png (content/shared/img/dropdown-inverse.png)
content/browser/loop/shared/img/dropdown-inverse@2x.png (content/shared/img/dropdown-inverse@2x.png)
content/browser/loop/shared/img/svg/glyph-email-16x16.svg (content/shared/img/svg/glyph-email-16x16.svg)
content/browser/loop/shared/img/svg/glyph-facebook-16x16.svg (content/shared/img/svg/glyph-facebook-16x16.svg)
content/browser/loop/shared/img/svg/glyph-help-16x16.svg (content/shared/img/svg/glyph-help-16x16.svg)
@ -103,7 +95,6 @@ browser.jar:
# Shared scripts
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
content/browser/loop/shared/js/store.js (content/shared/js/store.js)
content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
@ -117,7 +108,6 @@ browser.jar:
content/browser/loop/shared/js/urlRegExps.js (content/shared/js/urlRegExps.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
# Shared libs
#ifdef DEBUG

View File

@ -49,8 +49,16 @@ TESTS="
browser/base/content/test/general/browser_parsable_css.js
"
./mach mochitest $TESTS
if [ "$1" != "--skip-e10s" ]; then
./mach mochitest --e10s $TESTS
fi
# Due to bug 1209463, we need to split these up and run them individually to
# ensure we stop and report that there's an error.
for test in $TESTS
do
./mach mochitest $test
# UITour & get user media aren't compatible with e10s currenly.
if [ "$1" != "--skip-e10s" ] && \
[ "$test" != "browser/components/uitour/test/browser_UITour_loop.js" ] && \
[ "$test" != "browser/base/content/test/general/browser_devices_get_user_media_about_urls.js" ];
then
./mach mochitest --e10s $test
fi
done

View File

@ -1,7 +1,6 @@
## LOCALIZATION NOTE: In this file, don't translate the part between {{..}}
conversation_has_ended=Your conversation has ended.
generic_failure_message=We're having technical difficulties…
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname2}}
## as this will be replaced by the shortname.
@ -29,7 +28,6 @@ unsupported_platform_blackberry=Blackberry
unsupported_platform_learn_more_link=Learn more about why your platform doesn't support {{clientShortname}}
promote_firefox_hello_heading=Download {{brandShortname}} to make free audio and video calls!
get_firefox_button=Get {{brandShortname}}
initiate_call_cancel_button=Cancel
legal_text_and_links=By using {{clientShortname}} you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}.
terms_of_use_link_text=Terms of use
privacy_notice_link_text=Privacy notice
@ -47,8 +45,6 @@ clientShortname2=Firefox Hello
vendorShortname=Mozilla
call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
help_label=Help

View File

@ -40,7 +40,6 @@ require("script!shared/js/mixins.js");
require("script!shared/js/actions.js");
require("script!shared/js/validate.js");
require("script!shared/js/dispatcher.js");
require("script!shared/js/websocket.js");
require("script!shared/js/otSdkDriver.js");
require("script!shared/js/store.js");
require("script!shared/js/activeRoomStore.js");

View File

@ -1,133 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.Client", function() {
"use strict";
var expect = chai.expect;
var sandbox,
callback,
client,
mozLoop,
fakeToken,
hawkRequestStub;
var fakeErrorRes = {
code: 400,
errno: 400,
error: "Request Failed",
message: "invalid token"
};
beforeEach(function() {
sandbox = sinon.sandbox.create();
callback = sinon.spy();
fakeToken = "fakeTokenText";
mozLoop = {
getLoopPref: sandbox.stub()
.returns(null)
.withArgs("hawk-session-token")
.returns(fakeToken),
hawkRequest: sinon.stub(),
LOOP_SESSION_TYPE: {
GUEST: 1,
FXA: 2
},
userProfile: null,
telemetryAdd: sinon.spy()
};
// Alias for clearer tests.
hawkRequestStub = mozLoop.hawkRequest;
client = new loop.Client({
mozLoop: mozLoop
});
});
afterEach(function() {
sandbox.restore();
});
describe("loop.Client", function() {
describe("#setupOutgoingCall", function() {
var calleeIds, callType;
beforeEach(function() {
calleeIds = [
"fakeemail", "fake phone"
];
callType = "audio";
});
it("should make a POST call to /calls", function() {
client.setupOutgoingCall(calleeIds, callType);
sinon.assert.calledOnce(hawkRequestStub);
sinon.assert.calledWith(hawkRequestStub,
mozLoop.LOOP_SESSION_TYPE.FXA,
"/calls",
"POST",
{ calleeId: calleeIds, callType: callType, channel: "unknown" }
);
});
it("should include the channel when defined", function() {
mozLoop.appVersionInfo = {
channel: "beta"
};
client.setupOutgoingCall(calleeIds, callType);
sinon.assert.calledOnce(hawkRequestStub);
sinon.assert.calledWith(hawkRequestStub,
mozLoop.LOOP_SESSION_TYPE.FXA,
"/calls",
"POST",
{ calleeId: calleeIds, callType: callType, channel: "beta" }
);
});
it("should call the callback if the request is successful", function() {
var requestData = {
apiKey: "fake",
callId: "fakeCall",
progressURL: "fakeurl",
sessionId: "12345678",
sessionToken: "15263748",
websocketToken: "13572468"
};
hawkRequestStub.callsArgWith(4, null, JSON.stringify(requestData));
client.setupOutgoingCall(calleeIds, callType, callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithExactly(callback, null, requestData);
});
it("should send an error when the request fails", function() {
hawkRequestStub.callsArgWith(4, fakeErrorRes);
client.setupOutgoingCall(calleeIds, callType, callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithExactly(callback, sinon.match(function(err) {
return err.code === 400 && err.message === "invalid token";
}));
});
it("should send an error if the data is not valid", function() {
// Sets up the hawkRequest stub to trigger the callback with
// an error
hawkRequestStub.callsArgWith(4, null, "{}");
client.setupOutgoingCall(calleeIds, callType, callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /Invalid data received/.test(err.message);
}));
});
});
});
});

View File

@ -77,8 +77,8 @@ describe("loop.store.ConversationAppStore", function () {
beforeEach(function() {
fakeWindowData = {
type: "incoming",
callId: "123456"
type: "room",
roomToken: "123456"
};
fakeGetWindowData = {
@ -113,7 +113,7 @@ describe("loop.store.ConversationAppStore", function () {
it("should fetch the window type from the mozLoop API", function() {
store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
expect(store.getStoreState().windowType).eql("incoming");
expect(store.getStoreState().windowType).eql("room");
});
it("should have the feedback period in initial state", function() {
@ -219,26 +219,6 @@ describe("loop.store.ConversationAppStore", function () {
sandbox.stub(loop.shared.mixins.WindowCloseMixin, "closeWindow");
});
it("should dispatch the correct action for windowType 'incoming'", function() {
store.setStoreState({ windowType: "incoming" });
store.LoopHangupNowHandler();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.HangupCall());
sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
it("should dispatch the correct action for windowType 'outgoing'", function() {
store.setStoreState({ windowType: "outgoing" });
store.LoopHangupNowHandler();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.HangupCall());
sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
});
it("should dispatch the correct action when a room was used", function() {
store.setStoreState({ windowType: "room" });
roomUsed = true;

View File

@ -1,965 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
describe("loop.conversationViews", function () {
"use strict";
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sandbox, view, dispatcher, contact, fakeAudioXHR, conversationStore;
var fakeMozLoop, fakeWindow, fakeClock;
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
beforeEach(function() {
sandbox = sinon.sandbox.create();
fakeClock = sandbox.useFakeTimers();
sandbox.stub(document.mozL10n, "get", function(x) {
return x;
});
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
contact = {
name: [ "mrsmith" ],
email: [{
type: "home",
value: "fakeEmail",
pref: true
}]
};
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
abort: function() {},
getResponseHeader: function(header) {
if (header === "Content-Type") {
return "audio/ogg";
}
},
responseType: null,
response: new ArrayBuffer(10),
onload: null
};
fakeMozLoop = navigator.mozLoop = {
SHARING_ROOM_URL: {
EMAIL_FROM_CALLFAILED: 2,
EMAIL_FROM_CONVERSATION: 3
},
// Dummy function, stubbed below.
getLoopPref: function() {},
setLoopPref: sandbox.stub(),
calls: {
clearCallInProgress: sinon.stub()
},
composeEmail: sinon.spy(),
get appVersionInfo() {
return {
version: "42",
channel: "test",
platform: "test"
};
},
getAudioBlob: sinon.spy(function(name, callback) {
callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
}),
startAlerting: sinon.stub(),
stopAlerting: sinon.stub(),
userProfile: {
email: "bob@invalid.tld"
}
};
sinon.stub(fakeMozLoop, "getLoopPref", function(pref) {
if (pref === "fake") {
return "http://fakeurl";
}
return false;
});
fakeWindow = {
navigator: { mozLoop: fakeMozLoop },
close: sinon.stub(),
document: {},
addEventListener: function() {},
removeEventListener: function() {}
};
loop.shared.mixins.setRootObject(fakeWindow);
conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: fakeMozLoop,
sdkDriver: {}
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: {}
});
loop.store.StoreMixin.register({
conversationStore: conversationStore,
textChatStore: textChatStore
});
});
afterEach(function() {
loop.shared.mixins.setRootObject(window);
view = undefined;
delete navigator.mozLoop;
sandbox.restore();
});
describe("CallIdentifierView", function() {
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.CallIdentifierView, props));
}
it("should set display the peer identifer", function() {
view = mountTestComponent({
showIcons: false,
peerIdentifier: "mrssmith"
});
expect(TestUtils.findRenderedDOMComponentWithClass(
view, "fx-embedded-call-identifier-text").props.children).eql("mrssmith");
});
it("should not display the icons if showIcons is false", function() {
view = mountTestComponent({
showIcons: false,
peerIdentifier: "mrssmith"
});
expect(TestUtils.findRenderedDOMComponentWithClass(
view, "fx-embedded-call-detail").props.className).to.contain("hide");
});
it("should display the icons if showIcons is true", function() {
view = mountTestComponent({
showIcons: true,
peerIdentifier: "mrssmith"
});
expect(TestUtils.findRenderedDOMComponentWithClass(
view, "fx-embedded-call-detail").props.className).to.not.contain("hide");
});
it("should display the url timestamp", function() {
sandbox.stub(loop.shared.utils, "formatDate").returns(("October 9, 2014"));
view = mountTestComponent({
showIcons: true,
peerIdentifier: "mrssmith",
urlCreationDate: (new Date() / 1000).toString()
});
expect(TestUtils.findRenderedDOMComponentWithClass(
view, "fx-embedded-conversation-timestamp").props.children).eql("(October 9, 2014)");
});
it("should show video as muted if video is false", function() {
view = mountTestComponent({
showIcons: true,
peerIdentifier: "mrssmith",
video: false
});
expect(TestUtils.findRenderedDOMComponentWithClass(
view, "fx-embedded-tiny-video-icon").props.className).to.contain("muted");
});
});
describe("PendingConversationView", function() {
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.PendingConversationView, props));
}
it("should set display connecting string when the state is not alerting",
function() {
view = mountTestComponent({
callState: CALL_STATES.CONNECTING,
contact: contact,
dispatcher: dispatcher
});
var label = TestUtils.findRenderedDOMComponentWithClass(
view, "btn-label").props.children;
expect(label).to.have.string("connecting");
});
it("should set display ringing string when the state is alerting",
function() {
view = mountTestComponent({
callState: CALL_STATES.ALERTING,
contact: contact,
dispatcher: dispatcher
});
var label = TestUtils.findRenderedDOMComponentWithClass(
view, "btn-label").props.children;
expect(label).to.have.string("ringing");
});
it("should disable the cancel button if enableCancelButton is false",
function() {
view = mountTestComponent({
callState: CALL_STATES.CONNECTING,
contact: contact,
dispatcher: dispatcher,
enableCancelButton: false
});
var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
expect(cancelBtn.classList.contains("disabled")).eql(true);
});
it("should enable the cancel button if enableCancelButton is false",
function() {
view = mountTestComponent({
callState: CALL_STATES.CONNECTING,
contact: contact,
dispatcher: dispatcher,
enableCancelButton: true
});
var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
expect(cancelBtn.classList.contains("disabled")).eql(false);
});
it("should dispatch a cancelCall action when the cancel button is pressed",
function() {
view = mountTestComponent({
callState: CALL_STATES.CONNECTING,
contact: contact,
dispatcher: dispatcher
});
var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
React.addons.TestUtils.Simulate.click(cancelBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "cancelCall"));
});
});
describe("FailureInfoView", function() {
function mountTestComponent(options) {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.FailureInfoView, options));
}
it("should display a generic failure message by default", function() {
view = mountTestComponent({
failureReason: "fake"
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("generic_failure_message");
});
it("should display a no media message for the no media reason", function() {
view = mountTestComponent({
failureReason: FAILURE_DETAILS.NO_MEDIA
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("no_media_failure_message");
});
it("should display a no media message for the unable to publish reason", function() {
view = mountTestComponent({
failureReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("no_media_failure_message");
});
it("should display a user unavailable message for the unavailable reason", function() {
view = mountTestComponent({
contact: {email: [{value: "test@test.tld"}]},
failureReason: FAILURE_DETAILS.USER_UNAVAILABLE
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("contact_unavailable_title");
});
it("should display a ToS failure message for the ToS failure reason", function() {
view = mountTestComponent({
failureReason: FAILURE_DETAILS.TOS_FAILURE
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("tos_failure_message");
});
it("should display a generic unavailable message if the contact doesn't have a display name", function() {
view = mountTestComponent({
contact: {
tel: [{"pref": true, type: "work", value: ""}]
},
failureReason: FAILURE_DETAILS.USER_UNAVAILABLE
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("generic_contact_unavailable_title");
});
it("should display an extra message", function() {
view = mountTestComponent({
extraMessage: "Fake message",
failureReason: FAILURE_DETAILS.UNKNOWN
});
var extraMessage = view.getDOMNode().querySelector(".failure-info-extra");
expect(extraMessage.textContent).eql("Fake message");
});
it("should display an extra failure message", function() {
view = mountTestComponent({
extraFailureMessage: "Fake failure message",
failureReason: FAILURE_DETAILS.UNKNOWN
});
var extraFailureMessage = view.getDOMNode().querySelector(".failure-info-extra-failure");
expect(extraFailureMessage.textContent).eql("Fake failure message");
});
it("should display an ICE failure message", function() {
view = mountTestComponent({
failureReason: FAILURE_DETAILS.ICE_FAILED
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("ice_failure_message");
});
});
describe("DirectCallFailureView", function() {
var fakeAudio, composeCallUrlEmail;
var fakeContact = {email: [{value: "test@test.tld"}]};
function mountTestComponent(options) {
var props = _.extend({
dispatcher: dispatcher,
mozLoop: fakeMozLoop,
outgoing: true
}, options);
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.DirectCallFailureView, props));
}
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
conversationStore.setStoreState({
callStateReason: FAILURE_DETAILS.UNKNOWN,
contact: fakeContact
});
});
it("should not display the retry button for incoming calls", function() {
view = mountTestComponent({outgoing: false});
var retryBtn = view.getDOMNode().querySelector(".btn-retry");
expect(retryBtn.classList.contains("hide")).eql(true);
});
it("should not display the email button for incoming calls", function() {
view = mountTestComponent({outgoing: false});
var retryBtn = view.getDOMNode().querySelector(".btn-email");
expect(retryBtn.classList.contains("hide")).eql(true);
});
it("should dispatch a retryCall action when the retry button is pressed",
function() {
view = mountTestComponent();
var retryBtn = view.getDOMNode().querySelector(".btn-retry");
React.addons.TestUtils.Simulate.click(retryBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "retryCall"));
});
it("should dispatch a cancelCall action when the cancel button is pressed",
function() {
view = mountTestComponent();
var cancelBtn = view.getDOMNode().querySelector(".btn-cancel");
React.addons.TestUtils.Simulate.click(cancelBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "cancelCall"));
});
it("should dispatch a fetchRoomEmailLink action when the email button is pressed",
function() {
view = mountTestComponent();
var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
React.addons.TestUtils.Simulate.click(emailLinkBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "fetchRoomEmailLink"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("roomName", "test@test.tld"));
});
it("should name the created room using the contact name when available",
function() {
conversationStore.setStoreState({
contact: {
email: [{value: "test@test.tld"}],
name: ["Mr Fake ContactName"]
}
});
view = mountTestComponent();
var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
React.addons.TestUtils.Simulate.click(emailLinkBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("roomName", "Mr Fake ContactName"));
});
it("should disable the email link button once the action is dispatched",
function() {
view = mountTestComponent();
var emailLinkBtn = view.getDOMNode().querySelector(".btn-email");
React.addons.TestUtils.Simulate.click(emailLinkBtn);
expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
});
it("should compose an email once the email link is received", function() {
view = mountTestComponent();
conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
sinon.assert.calledOnce(composeCallUrlEmail);
sinon.assert.calledWithExactly(composeCallUrlEmail,
"http://fake.invalid/", "test@test.tld", null, "callfailed");
});
it("should close the conversation window once the email link is received",
function() {
view = mountTestComponent();
conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
sinon.assert.calledOnce(fakeWindow.close);
});
it("should display an error message in case email link retrieval failed",
function() {
view = mountTestComponent();
conversationStore.trigger("error:emailLink");
expect(view.getDOMNode().querySelector(".failure-info-extra-failure")).not.eql(null);
});
it("should allow retrying to get a call url if it failed previously",
function() {
view = mountTestComponent();
conversationStore.trigger("error:emailLink");
expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
});
it("should play a failure sound, once", function() {
view = mountTestComponent();
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"failure", sinon.match.func);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(false);
});
it("should display an additional message for outgoing calls", function() {
view = mountTestComponent({
outgoing: true
});
var extraMessage = view.getDOMNode().querySelector(".failure-info-extra");
expect(extraMessage.textContent).eql("generic_failure_with_reason2");
});
});
describe("OngoingConversationView", function() {
function mountTestComponent(extraProps) {
var props = _.extend({
chatWindowDetached: false,
conversationStore: conversationStore,
dispatcher: dispatcher,
mozLoop: {},
matchMedia: window.matchMedia
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.OngoingConversationView, props));
}
it("should dispatch a setupStreamElements action when the view is created",
function() {
view = mountTestComponent();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "setupStreamElements"));
});
it("should display the remote video when the stream is enabled", function() {
conversationStore.setStoreState({
remoteSrcMediaElement: { fake: 1 }
});
view = mountTestComponent({
mediaConnected: true,
remoteVideoEnabled: true
});
expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
});
it("should display the local video when the stream is enabled", function() {
conversationStore.setStoreState({
localSrcMediaElement: { fake: 1 }
});
view = mountTestComponent({
video: {
enabled: true
}
});
expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
});
it("should dispatch a setMute action when the audio mute button is pressed",
function() {
view = mountTestComponent({
audio: {enabled: false}
});
var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
React.addons.TestUtils.Simulate.click(muteBtn);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "setMute"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("enabled", true));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("type", "audio"));
});
it("should dispatch a setMute action when the video mute button is pressed",
function() {
view = mountTestComponent({
video: {enabled: true}
});
var muteBtn = view.getDOMNode().querySelector(".btn-mute-video");
React.addons.TestUtils.Simulate.click(muteBtn);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "setMute"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("enabled", false));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("type", "video"));
});
it("should set the mute button as mute off", function() {
view = mountTestComponent({
video: {enabled: true}
});
var muteBtn = view.getDOMNode().querySelector(".btn-mute-video");
expect(muteBtn.classList.contains("muted")).eql(false);
});
it("should set the mute button as mute on", function() {
view = mountTestComponent({
audio: {enabled: false}
});
var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
expect(muteBtn.classList.contains("muted")).eql(true);
});
});
describe("CallControllerView", function() {
var onCallTerminatedStub;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.CallControllerView, {
chatWindowDetached: false,
dispatcher: dispatcher,
mozLoop: fakeMozLoop,
onCallTerminated: onCallTerminatedStub
}));
}
beforeEach(function() {
onCallTerminatedStub = sandbox.stub();
});
afterEach(function() {
sandbox.restore();
});
it("should set the document title to the callerId", function() {
conversationStore.setStoreState({
contact: contact
});
mountTestComponent();
expect(fakeWindow.document.title).eql("mrsmith");
});
it("should fallback to the contact email if the contact name is not defined", function() {
delete contact.name;
conversationStore.setStoreState({
contact: contact
});
mountTestComponent({contact: contact});
expect(fakeWindow.document.title).eql("fakeEmail");
});
it("should fallback to the caller id if no contact is defined", function() {
conversationStore.setStoreState({
callerId: "fakeId"
});
mountTestComponent({contact: contact});
expect(fakeWindow.document.title).eql("fakeId");
});
it("should render the DirectCallFailureView when the call state is 'terminated'",
function() {
conversationStore.setStoreState({
callState: CALL_STATES.TERMINATED,
contact: contact,
callStateReason: WEBSOCKET_REASONS.CLOSED,
outgoing: true
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.DirectCallFailureView);
});
it("should render the PendingConversationView for outgoing calls when the call state is 'gather'",
function() {
conversationStore.setStoreState({
callState: CALL_STATES.GATHER,
contact: contact,
outgoing: true
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.PendingConversationView);
});
it("should render the AcceptCallView for incoming calls when the call state is 'alerting'", function() {
conversationStore.setStoreState({
callState: CALL_STATES.ALERTING,
outgoing: false,
callerId: "fake@invalid.com"
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.AcceptCallView);
});
it("should render the OngoingConversationView when the call state is 'ongoing'",
function() {
conversationStore.setStoreState({callState: CALL_STATES.ONGOING});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.OngoingConversationView);
});
it("should play the terminated sound when the call state is 'finished'",
function() {
var fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
view = mountTestComponent();
sinon.assert.calledOnce(fakeAudio.play);
});
it("should update the rendered views when the state is changed.",
function() {
conversationStore.setStoreState({
callState: CALL_STATES.GATHER,
contact: contact,
outgoing: true
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.PendingConversationView);
conversationStore.setStoreState({
callState: CALL_STATES.TERMINATED,
callStateReason: WEBSOCKET_REASONS.CLOSED
});
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.DirectCallFailureView);
});
it("should call onCallTerminated when the call is finished", function() {
conversationStore.setStoreState({
callState: CALL_STATES.ONGOING
});
view = mountTestComponent({
callState: CALL_STATES.FINISHED
});
// Force a state change so that it triggers componentDidUpdate.
view.setState({ callState: CALL_STATES.FINISHED });
sinon.assert.calledOnce(onCallTerminatedStub);
sinon.assert.calledWithExactly(onCallTerminatedStub);
});
});
describe("AcceptCallView", function() {
var callView;
function mountTestComponent(extraProps) {
var props = _.extend({dispatcher: dispatcher, mozLoop: fakeMozLoop}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.AcceptCallView, props));
}
afterEach(function() {
callView = null;
});
it("should start alerting on display", function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "fake@invalid.com"
});
sinon.assert.calledOnce(fakeMozLoop.startAlerting);
});
it("should stop alerting when removed from the display", function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "fake@invalid.com"
});
callView.componentWillUnmount();
sinon.assert.calledOnce(fakeMozLoop.stopAlerting);
});
describe("default answer mode", function() {
it("should display video as primary answer mode", function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "fake@invalid.com"
});
var primaryBtn = callView.getDOMNode()
.querySelector(".fx-embedded-btn-icon-video");
expect(primaryBtn).not.to.eql(null);
});
it("should display audio as primary answer mode", function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_ONLY,
callerId: "fake@invalid.com"
});
var primaryBtn = callView.getDOMNode()
.querySelector(".fx-embedded-btn-icon-audio");
expect(primaryBtn).not.to.eql(null);
});
it("should accept call with video", function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "fake@invalid.com"
});
var primaryBtn = callView.getDOMNode()
.querySelector(".fx-embedded-btn-icon-video");
React.addons.TestUtils.Simulate.click(primaryBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.AcceptCall({
callType: CALL_TYPES.AUDIO_VIDEO
}));
});
it("should accept call with audio", function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_ONLY,
callerId: "fake@invalid.com"
});
var primaryBtn = callView.getDOMNode()
.querySelector(".fx-embedded-btn-icon-audio");
React.addons.TestUtils.Simulate.click(primaryBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.AcceptCall({
callType: CALL_TYPES.AUDIO_ONLY
}));
});
it("should accept call with video when clicking on secondary btn",
function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_ONLY,
callerId: "fake@invalid.com"
});
var secondaryBtn = callView.getDOMNode()
.querySelector(".fx-embedded-btn-video-small");
React.addons.TestUtils.Simulate.click(secondaryBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.AcceptCall({
callType: CALL_TYPES.AUDIO_VIDEO
}));
});
it("should accept call with audio when clicking on secondary btn",
function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "fake@invalid.com"
});
var secondaryBtn = callView.getDOMNode()
.querySelector(".fx-embedded-btn-audio-small");
React.addons.TestUtils.Simulate.click(secondaryBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.AcceptCall({
callType: CALL_TYPES.AUDIO_ONLY
}));
});
});
describe("click event on .btn-decline", function() {
it("should dispatch a DeclineCall action", function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "fake@invalid.com"
});
var buttonDecline = callView.getDOMNode().querySelector(".btn-decline");
TestUtils.Simulate.click(buttonDecline);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.DeclineCall({blockCaller: false}));
});
});
describe("click event on .btn-block", function() {
it("should dispatch a DeclineCall action with blockCaller true", function() {
callView = mountTestComponent({
callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "fake@invalid.com"
});
var buttonBlock = callView.getDOMNode().querySelector(".btn-block");
TestUtils.Simulate.click(buttonBlock);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.DeclineCall({blockCaller: true}));
});
});
});
});

View File

@ -136,7 +136,7 @@ describe("loop.conversation", function() {
});
describe("AppControllerView", function() {
var conversationStore, activeRoomStore, client, ccView, dispatcher;
var activeRoomStore, ccView, dispatcher;
var conversationAppStore, roomStore, feedbackPeriodMs = 15770000000;
var ROOM_STATES = loop.store.ROOM_STATES;
@ -150,23 +150,7 @@ describe("loop.conversation", function() {
}
beforeEach(function() {
client = new loop.Client();
dispatcher = new loop.Dispatcher();
conversationStore = new loop.store.ConversationStore(
dispatcher, {
client: client,
mozLoop: navigator.mozLoop,
sdkDriver: {}
});
conversationStore.setStoreState({contact: {
name: [ "Mr Smith" ],
email: [{
type: "home",
value: "fakeEmail",
pref: true
}]
}});
activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: {},
@ -183,8 +167,7 @@ describe("loop.conversation", function() {
});
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
conversationStore: conversationStore
conversationAppStore: conversationAppStore
});
});
@ -192,24 +175,6 @@ describe("loop.conversation", function() {
ccView = undefined;
});
it("should display the CallControllerView for outgoing calls", function() {
conversationAppStore.setStoreState({windowType: "outgoing"});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversationViews.CallControllerView);
});
it("should display the CallControllerView for incoming calls", function() {
conversationAppStore.setStoreState({windowType: "incoming"});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversationViews.CallControllerView);
});
it("should display the RoomView for rooms", function() {
conversationAppStore.setStoreState({windowType: "room"});
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
@ -220,20 +185,17 @@ describe("loop.conversation", function() {
loop.roomViews.DesktopRoomConversationView);
});
it("should display the DirectCallFailureView for failures", function() {
it("should display the RoomFailureView for failures", function() {
conversationAppStore.setStoreState({
contact: {},
outgoing: false,
windowType: "failed"
});
conversationStore.setStoreState({
callStateReason: FAILURE_DETAILS.UNKNOWN
});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversationViews.DirectCallFailureView);
loop.roomViews.RoomFailureView);
});
it("should set the correct title when rendering feedback view", function() {

View File

@ -48,22 +48,18 @@
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/js/conversationAppStore.js"></script>
<script src="../../content/js/roomStore.js"></script>
<script src="../../content/js/roomViews.js"></script>
<script src="../../content/js/conversationViews.js"></script>
<script src="../../content/js/feedbackViews.js"></script>
<script src="../../content/js/conversation.js"></script>
<script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
@ -71,12 +67,10 @@
<!-- Test scripts -->
<script src="conversationAppStore_test.js"></script>
<script src="client_test.js"></script>
<script src="conversation_test.js"></script>
<script src="feedbackViews_test.js"></script>
<script src="panel_test.js"></script>
<script src="roomViews_test.js"></script>
<script src="conversationViews_test.js"></script>
<script src="contacts_test.js"></script>
<script src="l10n_test.js"></script>
<script src="roomStore_test.js"></script>

View File

@ -162,7 +162,7 @@ describe("loop.roomViews", function () {
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.FailureInfoView);
loop.roomViews.FailureInfoView);
});
it("should dispatch a JoinRoom action when the rejoin call button is pressed", function() {

View File

@ -19,23 +19,19 @@ module.exports = function(config) {
"content/shared/js/utils.js",
"content/shared/js/models.js",
"content/shared/js/mixins.js",
"content/shared/js/websocket.js",
"content/shared/js/actions.js",
"content/shared/js/otSdkDriver.js",
"content/shared/js/validate.js",
"content/shared/js/dispatcher.js",
"content/shared/js/store.js",
"content/shared/js/conversationStore.js",
"content/shared/js/activeRoomStore.js",
"content/shared/js/views.js",
"content/shared/js/textChatStore.js",
"content/shared/js/textChatView.js",
"content/js/feedbackViews.js",
"content/js/client.js",
"content/js/conversationAppStore.js",
"content/js/roomStore.js",
"content/js/roomViews.js",
"content/js/conversationViews.js",
"content/js/conversation.js",
"test/desktop-local/*.js"
]);

View File

@ -22,13 +22,11 @@ module.exports = function(config) {
"content/shared/js/models.js",
"content/shared/js/mixins.js",
"content/shared/js/crypto.js",
"content/shared/js/websocket.js",
"content/shared/js/validate.js",
"content/shared/js/actions.js",
"content/shared/js/dispatcher.js",
"content/shared/js/otSdkDriver.js",
"content/shared/js/activeRoomStore.js",
"content/shared/js/conversationStore.js",
"content/shared/js/views.js",
"content/shared/js/textChatStore.js",
"content/shared/js/textChatView.js",

File diff suppressed because it is too large Load Diff

View File

@ -50,7 +50,6 @@
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/crypto.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
@ -58,7 +57,6 @@
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
@ -71,11 +69,9 @@
<script src="utils_test.js"></script>
<script src="crypto_test.js"></script>
<script src="views_test.js"></script>
<script src="websocket_test.js"></script>
<script src="validate_test.js"></script>
<script src="dispatcher_test.js"></script>
<script src="activeRoomStore_test.js"></script>
<script src="conversationStore_test.js"></script>
<script src="otSdkDriver_test.js"></script>
<script src="store_test.js"></script>
<script src="textChatStore_test.js"></script>

View File

@ -1,340 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
describe("loop.CallConnectionWebSocket", function() {
"use strict";
var expect = chai.expect;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var sandbox,
dummySocket;
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
dummySocket = {
close: sinon.spy(),
send: sinon.spy()
};
sandbox.stub(window, "WebSocket").returns(dummySocket);
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("should require a url option", function() {
expect(function() {
return new loop.CallConnectionWebSocket();
}).to.Throw(/No url/);
});
it("should require a callId setting", function() {
expect(function() {
return new loop.CallConnectionWebSocket({url: "wss://fake/"});
}).to.Throw(/No callId/);
});
it("should require a websocketToken setting", function() {
expect(function() {
return new loop.CallConnectionWebSocket({
url: "http://fake/",
callId: "hello"
});
}).to.Throw(/No websocketToken/);
});
});
describe("constructed", function() {
var callWebSocket, fakeUrl, fakeCallId, fakeWebSocketToken;
beforeEach(function() {
fakeUrl = "wss://fake/";
fakeCallId = "callId";
fakeWebSocketToken = "7b";
callWebSocket = new loop.CallConnectionWebSocket({
url: fakeUrl,
callId: fakeCallId,
websocketToken: fakeWebSocketToken
});
});
describe("#promiseConnect", function() {
it("should create a new websocket connection", function() {
callWebSocket.promiseConnect();
sinon.assert.calledOnce(window.WebSocket);
sinon.assert.calledWithExactly(window.WebSocket, fakeUrl);
});
it("should reject the promise if connection is not completed in " +
"5 seconds", function(done) {
var promise = callWebSocket.promiseConnect();
sandbox.clock.tick(5101);
promise.then(function() {}, function(error) {
expect(error).to.be.equal(WEBSOCKET_REASONS.TIMEOUT);
done();
});
});
it("should reject the promise if the connection errors", function(done) {
var promise = callWebSocket.promiseConnect();
dummySocket.onerror("error");
promise.then(function() {}, function(error) {
expect(error).to.be.equal("error");
done();
});
});
it("should reject the promise if the connection closes", function(done) {
var promise = callWebSocket.promiseConnect();
dummySocket.onclose("close");
promise.then(function() {}, function(error) {
expect(error).to.be.equal("close");
done();
});
});
it("should send hello when the socket is opened", function() {
callWebSocket.promiseConnect();
dummySocket.onopen();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "hello",
callId: fakeCallId,
auth: fakeWebSocketToken
}));
});
it("should resolve the promise when the 'hello' is received",
function(done) {
var promise = callWebSocket.promiseConnect();
dummySocket.onmessage({
data: '{"messageType":"hello", "state":"init"}'
});
promise.then(function(state) {
expect(state).eql("init");
done();
}, function() {
done(new Error("shouldn't have rejected the promise"));
});
});
});
describe("#close", function() {
it("should close the socket", function() {
callWebSocket.promiseConnect();
callWebSocket.close();
sinon.assert.calledOnce(dummySocket.close);
});
});
describe("#decline", function() {
it("should send a terminate message to the server", function() {
callWebSocket.promiseConnect();
callWebSocket.decline();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "action",
event: "terminate",
reason: WEBSOCKET_REASONS.REJECT
}));
});
});
describe("#accept", function() {
it("should send an accept message to the server", function() {
callWebSocket.promiseConnect();
callWebSocket.accept();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "action",
event: "accept"
}));
});
});
describe("#mediaUp", function() {
it("should send a media-up message to the server", function() {
callWebSocket.promiseConnect();
callWebSocket.mediaUp();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "action",
event: "media-up"
}));
});
});
describe("#cancel", function() {
it("should send a terminate message to the server with a reason of WEBSOCKET_REASONS.CANCEL",
function() {
callWebSocket.promiseConnect();
callWebSocket.cancel();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "action",
event: "terminate",
reason: WEBSOCKET_REASONS.CANCEL
}));
});
});
describe("#mediaFail", function() {
it("should send a terminate message to the server with a reason of WEBSOCKET_REASONS.MEDIA_FAIL",
function() {
callWebSocket.promiseConnect();
callWebSocket.mediaFail();
sinon.assert.calledOnce(dummySocket.send);
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
messageType: "action",
event: "terminate",
reason: WEBSOCKET_REASONS.MEDIA_FAIL
}));
});
});
describe("Events", function() {
beforeEach(function() {
sandbox.stub(callWebSocket, "trigger");
callWebSocket.promiseConnect();
});
describe("Progress", function() {
it("should trigger a progress event on the callWebSocket", function() {
var eventData = {
messageType: "progress",
state: "terminate",
reason: WEBSOCKET_REASONS.REJECT
};
dummySocket.onmessage({
data: JSON.stringify(eventData)
});
sinon.assert.called(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
eventData, "init");
});
it("should trigger a progress event with the previous state", function() {
var previousEventData = {
messageType: "progress",
state: "alerting"
};
// This first call is to set the previous state of the object
// ready for the main test below.
dummySocket.onmessage({
data: JSON.stringify(previousEventData)
});
var currentEventData = {
messageType: "progress",
state: "terminate",
reason: WEBSOCKET_REASONS.REJECT
};
dummySocket.onmessage({
data: JSON.stringify(currentEventData)
});
sinon.assert.called(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
currentEventData, "alerting");
});
it("should trigger a progress:<state> event on the callWebSocket", function() {
var eventData = {
messageType: "progress",
state: "terminate",
reason: WEBSOCKET_REASONS.REJECT
};
dummySocket.onmessage({
data: JSON.stringify(eventData)
});
sinon.assert.called(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger, "progress:terminate");
});
});
describe("Error", function() {
// Handled in constructed -> #promiseConnect:
// should reject the promise if the connection errors
it("should trigger an error if state is not completed", function() {
callWebSocket._clearConnectionFlags();
dummySocket.onerror("Error");
sinon.assert.calledOnce(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger,
"error", "Error");
});
it("should not trigger an error if state is completed", function() {
callWebSocket._clearConnectionFlags();
callWebSocket._lastServerState = "connected";
dummySocket.onerror("Error");
sinon.assert.notCalled(callWebSocket.trigger);
});
});
describe("Close", function() {
// Handled in constructed -> #promiseConnect:
// should reject the promise if the connection closes
it("should trigger a close event if state is not completed", function() {
callWebSocket._clearConnectionFlags();
dummySocket.onclose("Error");
sinon.assert.calledOnce(callWebSocket.trigger);
sinon.assert.calledWithExactly(callWebSocket.trigger,
"closed", "Error");
});
it("should not trigger an error if state is completed", function() {
callWebSocket._clearConnectionFlags();
callWebSocket._lastServerState = "terminated";
dummySocket.onclose("Error");
sinon.assert.notCalled(callWebSocket.trigger);
});
});
});
});
});

View File

@ -46,7 +46,6 @@
<!-- App scripts -->
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>

View File

@ -36,11 +36,9 @@
<script src="../content/shared/js/utils.js"></script>
<script src="../content/shared/js/models.js"></script>
<script src="../content/shared/js/mixins.js"></script>
<script src="../content/shared/js/websocket.js"></script>
<script src="../content/shared/js/validate.js"></script>
<script src="../content/shared/js/dispatcher.js"></script>
<script src="../content/shared/js/store.js"></script>
<script src="../content/shared/js/conversationStore.js"></script>
<script src="../content/shared/js/activeRoomStore.js"></script>
<script src="../content/shared/js/views.js"></script>
<script src="../content/shared/js/textChatStore.js"></script>
@ -50,8 +48,6 @@
<script src="../content/shared/js/linkifiedTextView.js"></script>
<script src="../content/js/roomStore.js"></script>
<script src="../content/js/roomViews.js"></script>
<script src="../content/js/conversationViews.js"></script>
<script src="../content/js/client.js"></script>
<script src="../standalone/content/js/webapp.js"></script>
<script src="../standalone/content/js/standaloneRoomViews.js"></script>
<script type="text/javascript;version=1.8" src="../content/js/contacts.js"></script>

View File

@ -127,12 +127,6 @@ body {
font-weight: bold;
}
.call-action-group .btn-group-chevron,
.call-action-group .btn-group {
/* Prevent box overflow due to long string */
max-width: 120px;
}
.room-waiting-tile {
background-color: grey;
}

View File

@ -23,10 +23,6 @@
var ContactDetail = loop.contacts.ContactDetail;
var GettingStartedView = loop.panel.GettingStartedView;
// 1.2. Conversation Window
var AcceptCallView = loop.conversationViews.AcceptCallView;
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
var OngoingConversationView = loop.conversationViews.OngoingConversationView;
var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
var DesktopRoomEditContextView = loop.roomViews.DesktopRoomEditContextView;
var RoomFailureView = loop.roomViews.RoomFailureView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
@ -338,63 +334,6 @@
sdkDriver: mockSDK
});
/**
* Every view that uses an conversationStore needs its own; if they shared
* a conversation store, they'd interfere with each other.
*
* @param options
* @returns {loop.store.ConversationStore}
*/
function makeConversationStore() {
var roomDispatcher = new loop.Dispatcher();
var store = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
store.forcedUpdate = function forcedUpdate(contentWindow) {
// Since this is called by setTimeout, we don't want to lose any
// exceptions if there's a problem and we need to debug, so...
try {
var newStoreState = {
// Override the matchMedia, this is so that the correct version is
// used for the frame.
//
// Currently, we use an icky hack, and the showcase conspires with
// react-frame-component to set iframe.contentWindow.matchMedia onto
// the store. Once React context matures a bit (somewhere between
// 0.14 and 1.0, apparently):
//
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
//
// we should be able to use those to clean this up.
matchMedia: contentWindow.matchMedia.bind(contentWindow)
};
store.setStoreState(newStoreState);
} catch (ex) {
console.error("exception in forcedUpdate:", ex);
}
};
return store;
}
var conversationStores = [];
for (var index = 0; index < 5; index++) {
conversationStores[index] = makeConversationStore();
}
conversationStores[0].setStoreState({
callStateReason: FAILURE_DETAILS.NO_MEDIA
});
conversationStores[1].setStoreState({
callStateReason: FAILURE_DETAILS.USER_UNAVAILABLE,
contact: fakeManyContacts[0]
});
// Update the text chat store with the room info.
textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "A Very Long Conversation Name",
@ -449,7 +388,6 @@
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
conversationStore: conversationStores[0],
textChatStore: textChatStore
});
@ -545,12 +483,6 @@
requestCallUrlInfo: noop
};
var mockWebSocket = new loop.CallConnectionWebSocket({
url: "fake",
callId: "fakeId",
websocketToken: "fakeToken"
});
var notifications = new loop.shared.models.NotificationCollection();
var errNotifications = new loop.shared.models.NotificationCollection();
errNotifications.add({
@ -1060,47 +992,6 @@
)
),
React.createElement(Section, {name: "AcceptCallView"},
React.createElement(FramedExample, {dashed: true,
height: 272,
summary: "Default / incoming video call",
width: 332},
React.createElement("div", {className: "fx-embedded"},
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "Mr Smith",
dispatcher: dispatcher,
mozLoop: mockMozLoopLoggedIn})
)
),
React.createElement(FramedExample, {dashed: true,
height: 272,
summary: "Default / incoming audio only call",
width: 332},
React.createElement("div", {className: "fx-embedded"},
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_ONLY,
callerId: "Mr Smith",
dispatcher: dispatcher,
mozLoop: mockMozLoopLoggedIn})
)
)
),
React.createElement(Section, {name: "AcceptCallView-ActiveState"},
React.createElement(FramedExample, {dashed: true,
height: 272,
summary: "Default",
width: 332},
React.createElement("div", {className: "fx-embedded"},
React.createElement(AcceptCallView, {callType: CALL_TYPES.AUDIO_VIDEO,
callerId: "Mr Smith",
dispatcher: dispatcher,
mozLoop: mockMozLoopLoggedIn,
showMenu: true})
)
)
),
React.createElement(Section, {name: "ConversationToolbar"},
React.createElement("div", null,
React.createElement(FramedExample, {dashed: true,
@ -1151,159 +1042,6 @@
)
),
React.createElement(Section, {name: "PendingConversationView (Desktop)"},
React.createElement(FramedExample, {dashed: true,
height: 272,
summary: "Connecting",
width: 300},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopPendingConversationView, {callState: "gather",
contact: mockContact,
dispatcher: dispatcher})
)
)
),
React.createElement(Section, {name: "DirectCallFailureView"},
React.createElement(FramedExample, {
dashed: true,
height: 254,
summary: "Call Failed - Incoming",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DirectCallFailureView, {
conversationStore: conversationStores[0],
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
outgoing: false})
)
),
React.createElement(FramedExample, {
dashed: true,
height: 254,
summary: "Call Failed - Outgoing",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DirectCallFailureView, {
conversationStore: conversationStores[1],
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
outgoing: true})
)
),
React.createElement(FramedExample, {
dashed: true,
height: 254,
summary: "Call Failed — with call URL error",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DirectCallFailureView, {
conversationStore: conversationStores[0],
dispatcher: dispatcher,
emailLinkError: true,
mozLoop: navigator.mozLoop,
outgoing: true})
)
)
),
React.createElement(Section, {name: "OngoingConversationView"},
React.createElement(FramedExample, {dashed: true,
height: 398,
onContentsRendered: conversationStores[0].forcedUpdate,
summary: "Desktop ongoing conversation window",
width: 348},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: { enabled: true, visible: true},
chatWindowDetached: false,
conversationStore: conversationStores[0],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
video: { enabled: true, visible: true}})
)
),
React.createElement(FramedExample, {dashed: true,
height: 400,
onContentsRendered: conversationStores[1].forcedUpdate,
summary: "Desktop ongoing conversation window (medium)",
width: 600},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: { enabled: true, visible: true},
chatWindowDetached: false,
conversationStore: conversationStores[1],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
video: { enabled: true, visible: true}})
)
),
React.createElement(FramedExample, {height: 600,
onContentsRendered: conversationStores[2].forcedUpdate,
summary: "Desktop ongoing conversation window (large)",
width: 800},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: { enabled: true, visible: true},
chatWindowDetached: false,
conversationStore: conversationStores[2],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
video: { enabled: true, visible: true}})
)
),
React.createElement(FramedExample, {dashed: true,
height: 398,
onContentsRendered: conversationStores[3].forcedUpdate,
summary: "Desktop ongoing conversation window - local face mute",
width: 348},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: { enabled: true, visible: true},
chatWindowDetached: false,
conversationStore: conversationStores[3],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
video: { enabled: false, visible: true}})
)
),
React.createElement(FramedExample, {dashed: true,
height: 398,
onContentsRendered: conversationStores[4].forcedUpdate,
summary: "Desktop ongoing conversation window - remote face mute",
width: 348},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: { enabled: true, visible: true},
chatWindowDetached: false,
conversationStore: conversationStores[4],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: false,
video: { enabled: true, visible: true}})
)
)
),
React.createElement(Section, {name: "FeedbackView"},
React.createElement("p", {className: "note"}
),

View File

@ -23,10 +23,6 @@
var ContactDetail = loop.contacts.ContactDetail;
var GettingStartedView = loop.panel.GettingStartedView;
// 1.2. Conversation Window
var AcceptCallView = loop.conversationViews.AcceptCallView;
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
var OngoingConversationView = loop.conversationViews.OngoingConversationView;
var DirectCallFailureView = loop.conversationViews.DirectCallFailureView;
var DesktopRoomEditContextView = loop.roomViews.DesktopRoomEditContextView;
var RoomFailureView = loop.roomViews.RoomFailureView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
@ -338,63 +334,6 @@
sdkDriver: mockSDK
});
/**
* Every view that uses an conversationStore needs its own; if they shared
* a conversation store, they'd interfere with each other.
*
* @param options
* @returns {loop.store.ConversationStore}
*/
function makeConversationStore() {
var roomDispatcher = new loop.Dispatcher();
var store = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
store.forcedUpdate = function forcedUpdate(contentWindow) {
// Since this is called by setTimeout, we don't want to lose any
// exceptions if there's a problem and we need to debug, so...
try {
var newStoreState = {
// Override the matchMedia, this is so that the correct version is
// used for the frame.
//
// Currently, we use an icky hack, and the showcase conspires with
// react-frame-component to set iframe.contentWindow.matchMedia onto
// the store. Once React context matures a bit (somewhere between
// 0.14 and 1.0, apparently):
//
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
//
// we should be able to use those to clean this up.
matchMedia: contentWindow.matchMedia.bind(contentWindow)
};
store.setStoreState(newStoreState);
} catch (ex) {
console.error("exception in forcedUpdate:", ex);
}
};
return store;
}
var conversationStores = [];
for (var index = 0; index < 5; index++) {
conversationStores[index] = makeConversationStore();
}
conversationStores[0].setStoreState({
callStateReason: FAILURE_DETAILS.NO_MEDIA
});
conversationStores[1].setStoreState({
callStateReason: FAILURE_DETAILS.USER_UNAVAILABLE,
contact: fakeManyContacts[0]
});
// Update the text chat store with the room info.
textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "A Very Long Conversation Name",
@ -449,7 +388,6 @@
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
conversationStore: conversationStores[0],
textChatStore: textChatStore
});
@ -545,12 +483,6 @@
requestCallUrlInfo: noop
};
var mockWebSocket = new loop.CallConnectionWebSocket({
url: "fake",
callId: "fakeId",
websocketToken: "fakeToken"
});
var notifications = new loop.shared.models.NotificationCollection();
var errNotifications = new loop.shared.models.NotificationCollection();
errNotifications.add({
@ -1060,47 +992,6 @@
</FramedExample>
</Section>
<Section name="AcceptCallView">
<FramedExample dashed={true}
height={272}
summary="Default / incoming video call"
width={332}>
<div className="fx-embedded">
<AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
callerId="Mr Smith"
dispatcher={dispatcher}
mozLoop={mockMozLoopLoggedIn} />
</div>
</FramedExample>
<FramedExample dashed={true}
height={272}
summary="Default / incoming audio only call"
width={332}>
<div className="fx-embedded">
<AcceptCallView callType={CALL_TYPES.AUDIO_ONLY}
callerId="Mr Smith"
dispatcher={dispatcher}
mozLoop={mockMozLoopLoggedIn} />
</div>
</FramedExample>
</Section>
<Section name="AcceptCallView-ActiveState">
<FramedExample dashed={true}
height={272}
summary="Default"
width={332}>
<div className="fx-embedded">
<AcceptCallView callType={CALL_TYPES.AUDIO_VIDEO}
callerId="Mr Smith"
dispatcher={dispatcher}
mozLoop={mockMozLoopLoggedIn}
showMenu={true} />
</div>
</FramedExample>
</Section>
<Section name="ConversationToolbar">
<div>
<FramedExample dashed={true}
@ -1151,159 +1042,6 @@
</div>
</Section>
<Section name="PendingConversationView (Desktop)">
<FramedExample dashed={true}
height={272}
summary="Connecting"
width={300}>
<div className="fx-embedded">
<DesktopPendingConversationView callState={"gather"}
contact={mockContact}
dispatcher={dispatcher} />
</div>
</FramedExample>
</Section>
<Section name="DirectCallFailureView">
<FramedExample
dashed={true}
height={254}
summary="Call Failed - Incoming"
width={298}>
<div className="fx-embedded">
<DirectCallFailureView
conversationStore={conversationStores[0]}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
outgoing={false} />
</div>
</FramedExample>
<FramedExample
dashed={true}
height={254}
summary="Call Failed - Outgoing"
width={298}>
<div className="fx-embedded">
<DirectCallFailureView
conversationStore={conversationStores[1]}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
outgoing={true} />
</div>
</FramedExample>
<FramedExample
dashed={true}
height={254}
summary="Call Failed — with call URL error"
width={298}>
<div className="fx-embedded">
<DirectCallFailureView
conversationStore={conversationStores[0]}
dispatcher={dispatcher}
emailLinkError={true}
mozLoop={navigator.mozLoop}
outgoing={true} />
</div>
</FramedExample>
</Section>
<Section name="OngoingConversationView">
<FramedExample dashed={true}
height={398}
onContentsRendered={conversationStores[0].forcedUpdate}
summary="Desktop ongoing conversation window"
width={348}>
<div className="fx-embedded">
<OngoingConversationView
audio={{ enabled: true, visible: true }}
chatWindowDetached={false}
conversationStore={conversationStores[0]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
video={{ enabled: true, visible: true }} />
</div>
</FramedExample>
<FramedExample dashed={true}
height={400}
onContentsRendered={conversationStores[1].forcedUpdate}
summary="Desktop ongoing conversation window (medium)"
width={600}>
<div className="fx-embedded">
<OngoingConversationView
audio={{ enabled: true, visible: true }}
chatWindowDetached={false}
conversationStore={conversationStores[1]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
video={{ enabled: true, visible: true }} />
</div>
</FramedExample>
<FramedExample height={600}
onContentsRendered={conversationStores[2].forcedUpdate}
summary="Desktop ongoing conversation window (large)"
width={800}>
<div className="fx-embedded">
<OngoingConversationView
audio={{ enabled: true, visible: true }}
chatWindowDetached={false}
conversationStore={conversationStores[2]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
video={{ enabled: true, visible: true }} />
</div>
</FramedExample>
<FramedExample dashed={true}
height={398}
onContentsRendered={conversationStores[3].forcedUpdate}
summary="Desktop ongoing conversation window - local face mute"
width={348}>
<div className="fx-embedded">
<OngoingConversationView
audio={{ enabled: true, visible: true }}
chatWindowDetached={false}
conversationStore={conversationStores[3]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
video={{ enabled: false, visible: true }} />
</div>
</FramedExample>
<FramedExample dashed={true}
height={398}
onContentsRendered={conversationStores[4].forcedUpdate}
summary="Desktop ongoing conversation window - remote face mute"
width={348} >
<div className="fx-embedded">
<OngoingConversationView
audio={{ enabled: true, visible: true }}
chatWindowDetached={false}
conversationStore={conversationStores[4]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={false}
video={{ enabled: true, visible: true }} />
</div>
</FramedExample>
</Section>
<Section name="FeedbackView">
<p className="note">
</p>

View File

@ -87,4 +87,18 @@ timeline.csstransition.nameLabel=%S - CSS Transition
# when hovering over the name of an unknown animation type in the timeline UI.
# This can happen if devtools couldn't figure out the type of the animation.
# %S will be replaced by the name of the transition at run-time.
timeline.unknown.nameLabel=%S
timeline.unknown.nameLabel=%S
# LOCALIZATION NOTE (node.selectNodeLabel):
# This string is displayed in a tooltip of the animation panel that is shown
# when hovering over an animated node (e.g. something like div.animated).
# The tooltip invites the user to click on the node in order to select it in the
# inspector panel.
node.selectNodeLabel=Click to select this node in the Inspector
# LOCALIZATION NOTE (node.highlightNodeLabel):
# This string is displayed in a tooltip of the animation panel that is shown
# when hovering over the inspector icon displayed next to animated nodes.
# The tooltip invites the user to click on the icon in order to show the node
# highlighter.
node.highlightNodeLabel=Click to highlight this node in the page

View File

@ -42,9 +42,8 @@ display_name_dnd_status=Do Not Disturb
display_name_available_status=Available
# Error bars
## LOCALIZATION NOTE(unable_retrieve_url,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):
## 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.
unable_retrieve_url=Sorry, we were unable to retrieve a call URL.
session_expired_error_description=Session expired. All URLs you have previously created and shared will no longer work.
could_not_authenticate=Could Not Authenticate
password_changed_question=Did you change your password?
@ -73,7 +72,6 @@ share_email_footer=\n\n________\nJoin and create video conversations free with F
## in a tweet.
share_tweet=Join me for a video conversation on {{clientShortname2}}!
share_button3=Share Link
share_add_service_button=Add a Service
## LOCALIZATION NOTE (copy_link_menuitem, email_link_menuitem, delete_conversation_menuitem):
@ -222,11 +220,6 @@ gravatars_promo_button_use2=Use profile icons
initiate_call_button_label2=Ready to start your conversation?
incoming_call_title2=Conversation Request
incoming_call_accept_button=Accept
incoming_call_accept_audio_only_tooltip=Accept with voice
incoming_call_accept_audio_video_tooltip=Accept with video
incoming_call_cancel_button=Cancel
incoming_call_cancel_and_block_button=Cancel and Block
incoming_call_block_button=Block
hangup_button_title=Hang up
hangup_button_caption2=Exit
@ -252,27 +245,12 @@ 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
initiate_call_cancel_button=Cancel
## LOCALIZATION NOTE (call_progress_connecting_description): This is displayed
## whilst the client is contacting the client at the other end of the connection.
call_progress_connecting_description=Connecting…
## LOCALIZATION NOTE (call_progress_ringing_description): This is displayed
## when the other client is actually ringing.
call_progress_ringing_description=Ringing…
peer_ended_conversation2=The person you were calling has ended the conversation.
conversation_has_ended=Your conversation has ended.
restart_call=Rejoin
## LOCALIZATION NOTE (contact_unavailable_title): The title displayed
## when a contact is unavailable. Don't translate the part between {{..}}
## because this will be replaced by the contact's name.
contact_unavailable_title={{contactName}} is unavailable.
generic_contact_unavailable_title=This person is unavailable.
generic_failure_message=We're having technical difficulties…
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname2}}
## as this will be replaced by the shortname.

View File

@ -19,7 +19,7 @@
%define conditionalForwardWithUrlbarWidth 30
:root {
--backbutton-urlbar-overlap: 5px;
--backbutton-urlbar-overlap: 6px;
--toolbarbutton-hover-background: hsla(0,0%,100%,.3) linear-gradient(hsla(0,0%,100%,.7), hsla(0,0%,100%,.2));
--toolbarbutton-hover-boxshadow: 0 1px 0 hsla(0,0%,100%,.3) inset, 0 0 0 1px hsla(0,0%,100%,.2) inset, 0 1px 0 hsla(0,0%,0%,.03);
@ -697,10 +697,19 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
margin-bottom: 3px;
}
:-moz-any(#back-button, #forward-button) > .toolbarbutton-icon {
border-color: ThreeDShadow !important /* bug 561154 */;
}
:-moz-any(#back-button, #forward-button):not(:hover):not(:active):not([open=true]) > .toolbarbutton-icon,
:-moz-any(#back-button, #forward-button)[disabled=true] > .toolbarbutton-icon {
background-color: rgba(255,255,255,.15) !important /* bug 561154 */;
}
#back-button {
padding-top: 3px;
padding-bottom: 3px;
-moz-padding-start: 5px;
padding-top: 2px;
padding-bottom: 2px;
-moz-padding-start: 4px;
-moz-padding-end: 0;
position: relative;
z-index: 1;
@ -717,41 +726,7 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
#back-button > .toolbarbutton-icon {
border-radius: 10000px;
background-clip: padding-box;
padding: 6px;
border: none;
box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset,
0 0 0 1px hsla(0,0%,100%,.3) inset,
0 0 0 1px hsla(210,54%,20%,.25),
0 1px 0 hsla(210,54%,20%,.35);
background-image: linear-gradient(hsla(0,0%,100%,.6), hsla(0,0%,100%,.1));
transition-property: background-color, box-shadow;
transition-duration: 250ms;
}
#back-button:not([disabled="true"]):not([open="true"]):not(:active):hover > .toolbarbutton-icon {
background-color: hsla(210,48%,96%,.75);
box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset,
0 0 0 1px hsla(0,0%,100%,.3) inset,
0 0 0 1px hsla(210,54%,20%,.3),
0 1px 0 hsla(210,54%,20%,.4),
0 0 4px hsla(210,54%,20%,.2);
}
#back-button:not([disabled="true"]):hover:active > .toolbarbutton-icon,
#back-button[open="true"] > .toolbarbutton-icon {
background-color: hsla(210,54%,20%,.15);
box-shadow: 0 1px 1px hsla(210,54%,20%,.1) inset,
0 0 1px hsla(210,54%,20%,.2) inset,
0 0 0 1px hsla(210,54%,20%,.4),
0 1px 0 hsla(210,54%,20%,.2);
transition: none;
}
#main-window:not([customizing]) #back-button[disabled] > .toolbarbutton-icon {
box-shadow: 0 0 0 1px hsla(210,54%,20%,.55),
0 1px 0 hsla(210,54%,20%,.65) !important;
transition: none;
}
#back-button:-moz-locale-dir(rtl) > .toolbarbutton-icon {
@ -764,10 +739,8 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
}
#forward-button > .toolbarbutton-icon {
background-clip: padding-box;
padding-left: calc(var(--backbutton-urlbar-overlap) + 4px);
padding-left: calc(var(--backbutton-urlbar-overlap) + 3px);
padding-right: 3px;
border: 1px solid #9a9a9a;
border-left-style: none;
border-radius: 0;
}

View File

@ -191,6 +191,11 @@
background: url(chrome://browser/skin/controlcenter/warning-yellow.svg) no-repeat 0 50%;
}
.identity-popup-warning-gray:-moz-locale-dir(rtl),
.identity-popup-warning-yellow:-moz-locale-dir(rtl) {
background-position: 100% 50%;
}
/* SECURITY */
.identity-popup-connection-secure {

View File

@ -78,8 +78,6 @@ var AnimationsPanel = {
this.animationsTimelineComponent.destroy();
this.animationsTimelineComponent = null;
yield this.destroyPlayerWidgets();
this.playersEl = this.errorMessageEl = null;
this.toggleAllButtonEl = this.pickerButtonEl = null;
this.playTimelineButtonEl = null;
@ -187,7 +185,6 @@ var AnimationsPanel = {
// Empty the whole panel first.
this.togglePlayers(true);
yield this.destroyPlayerWidgets();
// Re-render the timeline component.
this.animationsTimelineComponent.render(
@ -205,17 +202,6 @@ var AnimationsPanel = {
this.emit(this.UI_UPDATED_EVENT);
done();
}),
destroyPlayerWidgets: Task.async(function*() {
if (!this.playerWidgets) {
return;
}
let destroyers = this.playerWidgets.map(widget => widget.destroy());
yield promise.all(destroyers);
this.playerWidgets = null;
this.playersEl.innerHTML = "";
})
};

View File

@ -25,7 +25,8 @@ const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const {
createNode,
drawGraphElementBackground,
findOptimalTimeInterval
findOptimalTimeInterval,
TargetNodeHighlighter
} = require("devtools/client/animationinspector/utils");
const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
@ -52,6 +53,8 @@ function AnimationTargetNode(inspector, options={}) {
this.onPreviewMouseOut = this.onPreviewMouseOut.bind(this);
this.onSelectNodeClick = this.onSelectNodeClick.bind(this);
this.onMarkupMutations = this.onMarkupMutations.bind(this);
this.onHighlightNodeClick = this.onHighlightNodeClick.bind(this);
this.onTargetHighlighterLocked = this.onTargetHighlighterLocked.bind(this);
EventEmitter.decorate(this);
}
@ -71,18 +74,22 @@ AnimationTargetNode.prototype = {
});
// Icon to select the node in the inspector.
this.selectNodeEl = createNode({
this.highlightNodeEl = createNode({
parent: this.el,
nodeType: "span",
attributes: {
"class": "node-selector"
"class": "node-highlighter",
"title": L10N.getStr("node.highlightNodeLabel")
}
});
// Wrapper used for mouseover/out event handling.
this.previewEl = createNode({
parent: this.el,
nodeType: "span"
nodeType: "span",
attributes: {
"title": L10N.getStr("node.selectNodeLabel")
}
});
if (!this.options.compact) {
@ -180,32 +187,48 @@ AnimationTargetNode.prototype = {
// Init events for highlighting and selecting the node.
this.previewEl.addEventListener("mouseover", this.onPreviewMouseOver);
this.previewEl.addEventListener("mouseout", this.onPreviewMouseOut);
this.selectNodeEl.addEventListener("click", this.onSelectNodeClick);
this.previewEl.addEventListener("click", this.onSelectNodeClick);
this.highlightNodeEl.addEventListener("click", this.onHighlightNodeClick);
// Start to listen for markupmutation events.
this.inspector.on("markupmutation", this.onMarkupMutations);
// Listen to the target node highlighter.
TargetNodeHighlighter.on("highlighted", this.onTargetHighlighterLocked);
},
destroy: function() {
TargetNodeHighlighter.unhighlight().catch(e => console.error(e));
TargetNodeHighlighter.off("highlighted", this.onTargetHighlighterLocked);
this.inspector.off("markupmutation", this.onMarkupMutations);
this.previewEl.removeEventListener("mouseover", this.onPreviewMouseOver);
this.previewEl.removeEventListener("mouseout", this.onPreviewMouseOut);
this.selectNodeEl.removeEventListener("click", this.onSelectNodeClick);
this.previewEl.removeEventListener("click", this.onSelectNodeClick);
this.highlightNodeEl.removeEventListener("click", this.onHighlightNodeClick);
this.el.remove();
this.el = this.tagNameEl = this.idEl = this.classEl = null;
this.selectNodeEl = this.previewEl = null;
this.highlightNodeEl = this.previewEl = null;
this.nodeFront = this.inspector = this.playerFront = null;
},
get highlighterUtils() {
return this.inspector.toolbox.highlighterUtils;
},
onPreviewMouseOver: function() {
if (!this.nodeFront) {
return;
}
this.inspector.toolbox.highlighterUtils.highlightNodeFront(this.nodeFront);
this.highlighterUtils.highlightNodeFront(this.nodeFront);
},
onPreviewMouseOut: function() {
this.inspector.toolbox.highlighterUtils.unhighlight();
if (!this.nodeFront) {
return;
}
this.highlighterUtils.unhighlight();
},
onSelectNodeClick: function() {
@ -215,6 +238,29 @@ AnimationTargetNode.prototype = {
this.inspector.selection.setNodeFront(this.nodeFront, "animationinspector");
},
onHighlightNodeClick: function() {
let classList = this.highlightNodeEl.classList;
let isHighlighted = classList.contains("selected");
if (isHighlighted) {
classList.remove("selected");
TargetNodeHighlighter.unhighlight().then(() => {
this.emit("target-highlighter-unlocked");
}, e => console.error(e));
} else {
classList.add("selected");
TargetNodeHighlighter.highlight(this).then(() => {
this.emit("target-highlighter-locked");
}, e => console.error(e));
}
},
onTargetHighlighterLocked: function(e, animationTargetNode) {
if (animationTargetNode !== this) {
this.highlightNodeEl.classList.remove("selected");
}
},
onMarkupMutations: function(e, mutations) {
if (!this.nodeFront || !this.playerFront) {
return;
@ -237,13 +283,14 @@ AnimationTargetNode.prototype = {
this.nodeFront = yield this.inspector.walker.getNodeFromActor(
playerFront.actorID, ["node"]);
} catch (e) {
// We might have been destroyed in the meantime, or the node might not be
// found.
if (!this.el) {
// The panel was destroyed in the meantime. Just log a warning.
console.warn("Cound't retrieve the animation target node, widget " +
"destroyed");
} else {
// This was an unexpected error, log it.
console.error(e);
}
console.error(e);
return;
}

View File

@ -23,6 +23,7 @@ support-files =
[browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
[browser_animation_shows_player_on_valid_node.js]
[browser_animation_target_highlight_select.js]
[browser_animation_target_highlighter_lock.js]
[browser_animation_timeline_header.js]
[browser_animation_timeline_pause_button.js]
[browser_animation_timeline_scrubber_exists.js]

View File

@ -10,12 +10,8 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel, controller} = yield openAnimationInspector();
let ui = yield openAnimationInspector();
yield testEventsOrder(ui);
});
function* testEventsOrder({inspector, panel, controller}) {
info("Listen for the players-updated, ui-updated and inspector-updated events");
let receivedEvents = [];
controller.once(controller.PLAYERS_UPDATED_EVENT, () => {
@ -32,12 +28,16 @@ function* testEventsOrder({inspector, panel, controller}) {
let node = yield getNodeFront(".animated", inspector);
yield selectNode(node, inspector);
info("Check that all events were received, and in the right order");
info("Check that all events were received");
// Only assert that the inspector-updated event is last, the order of the
// first 2 events is irrelevant.
is(receivedEvents.length, 3, "3 events were received");
is(receivedEvents[0], controller.PLAYERS_UPDATED_EVENT,
"The first event received was the players-updated event");
is(receivedEvents[1], panel.UI_UPDATED_EVENT,
"The second event received was the ui-updated event");
is(receivedEvents[2], "inspector-updated",
"The third event received was the inspector-updated event");
}
"The third event received was the inspector-updated event");
ok(receivedEvents.indexOf(controller.PLAYERS_UPDATED_EVENT) !== -1,
"The players-updated event was received");
ok(receivedEvents.indexOf(panel.UI_UPDATED_EVENT) !== -1,
"The ui-updated event was received");
});

View File

@ -23,7 +23,7 @@ add_task(function*() {
is(targetNodeComponent.el.textContent, "div#.ball.animated",
"The target element's content is correct");
let selectorEl = targetNodeComponent.el.querySelector(".node-selector");
ok(selectorEl,
"The icon to select the target element in the inspector exists");
let highlighterEl = targetNodeComponent.el.querySelector(".node-highlighter");
ok(highlighterEl,
"The icon to highlight the target element in the page exists");
});

View File

@ -8,38 +8,38 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {inspector, panel} = yield openAnimationInspector();
yield testRefreshOnNewAnimation(inspector, panel);
});
function* testRefreshOnNewAnimation(inspector, panel) {
info("Select a non animated node");
yield selectNode(".still", inspector);
assertAnimationsDisplayed(panel, 0);
info("Listen to the next UI update event");
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
info("Start an animation on the node");
yield executeInContent("devtools:test:setAttribute", {
yield changeElementAndWait({
selector: ".still",
attributeName: "class",
attributeValue: "ball animated"
});
yield onPanelUpdated;
ok(true, "The panel update event was fired");
}, panel, inspector);
assertAnimationsDisplayed(panel, 1);
info("Remove the animation class on the node");
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield executeInContent("devtools:test:setAttribute", {
yield changeElementAndWait({
selector: ".ball.animated",
attributeName: "class",
attributeValue: "ball still"
});
yield onPanelUpdated;
}
}, panel, inspector);
assertAnimationsDisplayed(panel, 0);
});
function* changeElementAndWait(options, panel, inspector) {
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
let onInspectorUpdated = inspector.once("inspector-updated");
yield executeInContent("devtools:test:setAttribute", options);
yield promise.all([
onInspectorUpdated, onPanelUpdated, waitForAllAnimationTargets(panel)]);
}

View File

@ -10,13 +10,9 @@
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let ui = yield openAnimationInspector();
yield testTargetNode(ui);
});
let {toolbox, inspector, panel} = yield openAnimationInspector();
function* testTargetNode({toolbox, inspector, panel}) {
info("Select the simple animated node");
let onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
yield selectNode(".animated", inspector);
yield onPanelUpdated;
@ -36,7 +32,7 @@ function* testTargetNode({toolbox, inspector, panel}) {
// Do not forget to mouseout, otherwise we get random mouseover event
// when selecting another node, which triggers some requests in animation
// inspector
// inspector.
EventUtils.synthesizeMouse(highlightingEl, 10, 5, {type: "mouseout"},
highlightingEl.ownerDocument.defaultView);
@ -58,19 +54,18 @@ function* testTargetNode({toolbox, inspector, panel}) {
targets = yield waitForAllAnimationTargets(panel);
targetNodeComponent = targets[0];
info("Click on the first animation widget's selector icon and wait for the " +
"selection to change");
info("Click on the first animated node component and wait for the " +
"selection to change");
let onSelection = inspector.selection.once("new-node-front");
onPanelUpdated = panel.once(panel.UI_UPDATED_EVENT);
let selectIconEl = targetNodeComponent.selectNodeEl;
EventUtils.sendMouseEvent({type: "click"}, selectIconEl,
selectIconEl.ownerDocument.defaultView);
let nodeEl = targetNodeComponent.previewEl;
EventUtils.sendMouseEvent({type: "click"}, nodeEl,
nodeEl.ownerDocument.defaultView);
yield onSelection;
is(inspector.selection.nodeFront, targetNodeComponent.nodeFront,
"The selected node is the one stored on the animation widget");
yield onPanelUpdated;
yield waitForAllAnimationTargets(panel);
}
});

View File

@ -0,0 +1,50 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the DOM element targets displayed in animation player widgets can
// be used to highlight elements in the DOM and select them in the inspector.
add_task(function*() {
yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
let {toolbox, inspector, panel} = yield openAnimationInspector();
let targets = panel.animationsTimelineComponent.targetNodes;
info("Click on the highlighter icon for the first animated node");
yield lockHighlighterOn(targets[0]);
ok(targets[0].highlightNodeEl.classList.contains("selected"),
"The highlighter icon is selected");
info("Click on the highlighter icon for the second animated node");
yield lockHighlighterOn(targets[1]);
ok(targets[1].highlightNodeEl.classList.contains("selected"),
"The highlighter icon is selected");
ok(!targets[0].highlightNodeEl.classList.contains("selected"),
"The highlighter icon for the first node is unselected");
info("Click again to unhighlight");
yield unlockHighlighterOn(targets[1]);
ok(!targets[1].highlightNodeEl.classList.contains("selected"),
"The highlighter icon for the second node is unselected");
});
function* lockHighlighterOn(targetComponent) {
let onLocked = targetComponent.once("target-highlighter-locked");
clickOnHighlighterIcon(targetComponent);
yield onLocked;
}
function* unlockHighlighterOn(targetComponent) {
let onUnlocked = targetComponent.once("target-highlighter-unlocked");
clickOnHighlighterIcon(targetComponent);
yield onUnlocked;
}
function clickOnHighlighterIcon(targetComponent) {
let lockEl = targetComponent.highlightNodeEl;
EventUtils.sendMouseEvent({type: "click"}, lockEl,
lockEl.ownerDocument.defaultView);
}

View File

@ -6,6 +6,12 @@
"use strict";
const {Cu} = require("chrome");
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
var {loader} = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm");
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/shared/event-emitter");
// How many times, maximum, can we loop before we find the optimal time
// interval in the timeline graph.
const OPTIMAL_TIME_INTERVAL_MAX_ITERS = 100;
@ -134,3 +140,40 @@ function findOptimalTimeInterval(timeScale,
}
exports.findOptimalTimeInterval = findOptimalTimeInterval;
/**
* The TargetNodeHighlighter util is a helper for AnimationTargetNode components
* that is used to lock the highlighter on animated nodes in the page.
* It instantiates a new highlighter that is then shared amongst all instances
* of AnimationTargetNode. This is useful because that means showing the
* highlighter on one animated node will unhighlight the previously highlighted
* one, but will not interfere with the default inspector highlighter.
*/
var TargetNodeHighlighter = {
highlighter: null,
isShown: false,
highlight: Task.async(function*(animationTargetNode) {
if (!this.highlighter) {
let hUtils = animationTargetNode.inspector.toolbox.highlighterUtils;
this.highlighter = yield hUtils.getHighlighterByType("BoxModelHighlighter");
}
yield this.highlighter.show(animationTargetNode.nodeFront);
this.isShown = true;
this.emit("highlighted", animationTargetNode);
}),
unhighlight: Task.async(function*() {
if (!this.highlighter || !this.isShown) {
return;
}
yield this.highlighter.hide();
this.isShown = false;
this.emit("unhighlighted");
})
};
EventEmitter.decorate(TargetNodeHighlighter);
exports.TargetNodeHighlighter = TargetNodeHighlighter;

View File

@ -130,160 +130,160 @@ function testSetAlpha() {
function getTestData() {
return [
{authored: "aliceblue", name: "aliceblue", hex: "#F0F8FF", hsl: "hsl(208, 100%, 97%)", rgb: "rgb(240, 248, 255)"},
{authored: "antiquewhite", name: "antiquewhite", hex: "#FAEBD7", hsl: "hsl(34, 78%, 91%)", rgb: "rgb(250, 235, 215)"},
{authored: "aqua", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
{authored: "aquamarine", name: "aquamarine", hex: "#7FFFD4", hsl: "hsl(160, 100%, 75%)", rgb: "rgb(127, 255, 212)"},
{authored: "azure", name: "azure", hex: "#F0FFFF", hsl: "hsl(180, 100%, 97%)", rgb: "rgb(240, 255, 255)"},
{authored: "beige", name: "beige", hex: "#F5F5DC", hsl: "hsl(60, 56%, 91%)", rgb: "rgb(245, 245, 220)"},
{authored: "bisque", name: "bisque", hex: "#FFE4C4", hsl: "hsl(33, 100%, 88%)", rgb: "rgb(255, 228, 196)"},
{authored: "aliceblue", name: "aliceblue", hex: "#f0f8ff", hsl: "hsl(208, 100%, 97%)", rgb: "rgb(240, 248, 255)"},
{authored: "antiquewhite", name: "antiquewhite", hex: "#faebd7", hsl: "hsl(34, 78%, 91%)", rgb: "rgb(250, 235, 215)"},
{authored: "aqua", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
{authored: "aquamarine", name: "aquamarine", hex: "#7fffd4", hsl: "hsl(160, 100%, 75%)", rgb: "rgb(127, 255, 212)"},
{authored: "azure", name: "azure", hex: "#f0ffff", hsl: "hsl(180, 100%, 97%)", rgb: "rgb(240, 255, 255)"},
{authored: "beige", name: "beige", hex: "#f5f5dc", hsl: "hsl(60, 56%, 91%)", rgb: "rgb(245, 245, 220)"},
{authored: "bisque", name: "bisque", hex: "#ffe4c4", hsl: "hsl(33, 100%, 88%)", rgb: "rgb(255, 228, 196)"},
{authored: "black", name: "black", hex: "#000", hsl: "hsl(0, 0%, 0%)", rgb: "rgb(0, 0, 0)"},
{authored: "blanchedalmond", name: "blanchedalmond", hex: "#FFEBCD", hsl: "hsl(36, 100%, 90%)", rgb: "rgb(255, 235, 205)"},
{authored: "blue", name: "blue", hex: "#00F", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)"},
{authored: "blueviolet", name: "blueviolet", hex: "#8A2BE2", hsl: "hsl(271, 76%, 53%)", rgb: "rgb(138, 43, 226)"},
{authored: "brown", name: "brown", hex: "#A52A2A", hsl: "hsl(0, 59%, 41%)", rgb: "rgb(165, 42, 42)"},
{authored: "burlywood", name: "burlywood", hex: "#DEB887", hsl: "hsl(34, 57%, 70%)", rgb: "rgb(222, 184, 135)"},
{authored: "cadetblue", name: "cadetblue", hex: "#5F9EA0", hsl: "hsl(182, 25%, 50%)", rgb: "rgb(95, 158, 160)"},
{authored: "chartreuse", name: "chartreuse", hex: "#7FFF00", hsl: "hsl(90, 100%, 50%)", rgb: "rgb(127, 255, 0)"},
{authored: "chocolate", name: "chocolate", hex: "#D2691E", hsl: "hsl(25, 75%, 47%)", rgb: "rgb(210, 105, 30)"},
{authored: "coral", name: "coral", hex: "#FF7F50", hsl: "hsl(16, 100%, 66%)", rgb: "rgb(255, 127, 80)"},
{authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ED", hsl: "hsl(219, 79%, 66%)", rgb: "rgb(100, 149, 237)"},
{authored: "cornsilk", name: "cornsilk", hex: "#FFF8DC", hsl: "hsl(48, 100%, 93%)", rgb: "rgb(255, 248, 220)"},
{authored: "crimson", name: "crimson", hex: "#DC143C", hsl: "hsl(348, 83%, 47%)", rgb: "rgb(220, 20, 60)"},
{authored: "cyan", name: "aqua", hex: "#0FF", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
{authored: "darkblue", name: "darkblue", hex: "#00008B", hsl: "hsl(240, 100%, 27%)", rgb: "rgb(0, 0, 139)"},
{authored: "darkcyan", name: "darkcyan", hex: "#008B8B", hsl: "hsl(180, 100%, 27%)", rgb: "rgb(0, 139, 139)"},
{authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#B8860B", hsl: "hsl(43, 89%, 38%)", rgb: "rgb(184, 134, 11)"},
{authored: "darkgray", name: "darkgray", hex: "#A9A9A9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
{authored: "blanchedalmond", name: "blanchedalmond", hex: "#ffebcd", hsl: "hsl(36, 100%, 90%)", rgb: "rgb(255, 235, 205)"},
{authored: "blue", name: "blue", hex: "#00f", hsl: "hsl(240, 100%, 50%)", rgb: "rgb(0, 0, 255)"},
{authored: "blueviolet", name: "blueviolet", hex: "#8a2be2", hsl: "hsl(271, 76%, 53%)", rgb: "rgb(138, 43, 226)"},
{authored: "brown", name: "brown", hex: "#a52a2a", hsl: "hsl(0, 59%, 41%)", rgb: "rgb(165, 42, 42)"},
{authored: "burlywood", name: "burlywood", hex: "#deb887", hsl: "hsl(34, 57%, 70%)", rgb: "rgb(222, 184, 135)"},
{authored: "cadetblue", name: "cadetblue", hex: "#5f9ea0", hsl: "hsl(182, 25%, 50%)", rgb: "rgb(95, 158, 160)"},
{authored: "chartreuse", name: "chartreuse", hex: "#7fff00", hsl: "hsl(90, 100%, 50%)", rgb: "rgb(127, 255, 0)"},
{authored: "chocolate", name: "chocolate", hex: "#d2691e", hsl: "hsl(25, 75%, 47%)", rgb: "rgb(210, 105, 30)"},
{authored: "coral", name: "coral", hex: "#ff7f50", hsl: "hsl(16, 100%, 66%)", rgb: "rgb(255, 127, 80)"},
{authored: "cornflowerblue", name: "cornflowerblue", hex: "#6495ed", hsl: "hsl(219, 79%, 66%)", rgb: "rgb(100, 149, 237)"},
{authored: "cornsilk", name: "cornsilk", hex: "#fff8dc", hsl: "hsl(48, 100%, 93%)", rgb: "rgb(255, 248, 220)"},
{authored: "crimson", name: "crimson", hex: "#dc143c", hsl: "hsl(348, 83%, 47%)", rgb: "rgb(220, 20, 60)"},
{authored: "cyan", name: "aqua", hex: "#0ff", hsl: "hsl(180, 100%, 50%)", rgb: "rgb(0, 255, 255)"},
{authored: "darkblue", name: "darkblue", hex: "#00008b", hsl: "hsl(240, 100%, 27%)", rgb: "rgb(0, 0, 139)"},
{authored: "darkcyan", name: "darkcyan", hex: "#008b8b", hsl: "hsl(180, 100%, 27%)", rgb: "rgb(0, 139, 139)"},
{authored: "darkgoldenrod", name: "darkgoldenrod", hex: "#b8860b", hsl: "hsl(43, 89%, 38%)", rgb: "rgb(184, 134, 11)"},
{authored: "darkgray", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
{authored: "darkgreen", name: "darkgreen", hex: "#006400", hsl: "hsl(120, 100%, 20%)", rgb: "rgb(0, 100, 0)"},
{authored: "darkgrey", name: "darkgray", hex: "#A9A9A9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
{authored: "darkkhaki", name: "darkkhaki", hex: "#BDB76B", hsl: "hsl(56, 38%, 58%)", rgb: "rgb(189, 183, 107)"},
{authored: "darkmagenta", name: "darkmagenta", hex: "#8B008B", hsl: "hsl(300, 100%, 27%)", rgb: "rgb(139, 0, 139)"},
{authored: "darkolivegreen", name: "darkolivegreen", hex: "#556B2F", hsl: "hsl(82, 39%, 30%)", rgb: "rgb(85, 107, 47)"},
{authored: "darkorange", name: "darkorange", hex: "#FF8C00", hsl: "hsl(33, 100%, 50%)", rgb: "rgb(255, 140, 0)"},
{authored: "darkorchid", name: "darkorchid", hex: "#9932CC", hsl: "hsl(280, 61%, 50%)", rgb: "rgb(153, 50, 204)"},
{authored: "darkred", name: "darkred", hex: "#8B0000", hsl: "hsl(0, 100%, 27%)", rgb: "rgb(139, 0, 0)"},
{authored: "darksalmon", name: "darksalmon", hex: "#E9967A", hsl: "hsl(15, 72%, 70%)", rgb: "rgb(233, 150, 122)"},
{authored: "darkseagreen", name: "darkseagreen", hex: "#8FBC8F", hsl: "hsl(120, 25%, 65%)", rgb: "rgb(143, 188, 143)"},
{authored: "darkslateblue", name: "darkslateblue", hex: "#483D8B", hsl: "hsl(248, 39%, 39%)", rgb: "rgb(72, 61, 139)"},
{authored: "darkslategray", name: "darkslategray", hex: "#2F4F4F", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
{authored: "darkslategrey", name: "darkslategray", hex: "#2F4F4F", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
{authored: "darkturquoise", name: "darkturquoise", hex: "#00CED1", hsl: "hsl(181, 100%, 41%)", rgb: "rgb(0, 206, 209)"},
{authored: "darkviolet", name: "darkviolet", hex: "#9400D3", hsl: "hsl(282, 100%, 41%)", rgb: "rgb(148, 0, 211)"},
{authored: "deeppink", name: "deeppink", hex: "#FF1493", hsl: "hsl(328, 100%, 54%)", rgb: "rgb(255, 20, 147)"},
{authored: "deepskyblue", name: "deepskyblue", hex: "#00BFFF", hsl: "hsl(195, 100%, 50%)", rgb: "rgb(0, 191, 255)"},
{authored: "darkgrey", name: "darkgray", hex: "#a9a9a9", hsl: "hsl(0, 0%, 66%)", rgb: "rgb(169, 169, 169)"},
{authored: "darkkhaki", name: "darkkhaki", hex: "#bdb76b", hsl: "hsl(56, 38%, 58%)", rgb: "rgb(189, 183, 107)"},
{authored: "darkmagenta", name: "darkmagenta", hex: "#8b008b", hsl: "hsl(300, 100%, 27%)", rgb: "rgb(139, 0, 139)"},
{authored: "darkolivegreen", name: "darkolivegreen", hex: "#556b2f", hsl: "hsl(82, 39%, 30%)", rgb: "rgb(85, 107, 47)"},
{authored: "darkorange", name: "darkorange", hex: "#ff8c00", hsl: "hsl(33, 100%, 50%)", rgb: "rgb(255, 140, 0)"},
{authored: "darkorchid", name: "darkorchid", hex: "#9932cc", hsl: "hsl(280, 61%, 50%)", rgb: "rgb(153, 50, 204)"},
{authored: "darkred", name: "darkred", hex: "#8b0000", hsl: "hsl(0, 100%, 27%)", rgb: "rgb(139, 0, 0)"},
{authored: "darksalmon", name: "darksalmon", hex: "#e9967a", hsl: "hsl(15, 72%, 70%)", rgb: "rgb(233, 150, 122)"},
{authored: "darkseagreen", name: "darkseagreen", hex: "#8fbc8f", hsl: "hsl(120, 25%, 65%)", rgb: "rgb(143, 188, 143)"},
{authored: "darkslateblue", name: "darkslateblue", hex: "#483d8b", hsl: "hsl(248, 39%, 39%)", rgb: "rgb(72, 61, 139)"},
{authored: "darkslategray", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
{authored: "darkslategrey", name: "darkslategray", hex: "#2f4f4f", hsl: "hsl(180, 25%, 25%)", rgb: "rgb(47, 79, 79)"},
{authored: "darkturquoise", name: "darkturquoise", hex: "#00ced1", hsl: "hsl(181, 100%, 41%)", rgb: "rgb(0, 206, 209)"},
{authored: "darkviolet", name: "darkviolet", hex: "#9400d3", hsl: "hsl(282, 100%, 41%)", rgb: "rgb(148, 0, 211)"},
{authored: "deeppink", name: "deeppink", hex: "#ff1493", hsl: "hsl(328, 100%, 54%)", rgb: "rgb(255, 20, 147)"},
{authored: "deepskyblue", name: "deepskyblue", hex: "#00bfff", hsl: "hsl(195, 100%, 50%)", rgb: "rgb(0, 191, 255)"},
{authored: "dimgray", name: "dimgray", hex: "#696969", hsl: "hsl(0, 0%, 41%)", rgb: "rgb(105, 105, 105)"},
{authored: "dodgerblue", name: "dodgerblue", hex: "#1E90FF", hsl: "hsl(210, 100%, 56%)", rgb: "rgb(30, 144, 255)"},
{authored: "firebrick", name: "firebrick", hex: "#B22222", hsl: "hsl(0, 68%, 42%)", rgb: "rgb(178, 34, 34)"},
{authored: "floralwhite", name: "floralwhite", hex: "#FFFAF0", hsl: "hsl(40, 100%, 97%)", rgb: "rgb(255, 250, 240)"},
{authored: "forestgreen", name: "forestgreen", hex: "#228B22", hsl: "hsl(120, 61%, 34%)", rgb: "rgb(34, 139, 34)"},
{authored: "fuchsia", name: "fuchsia", hex: "#F0F", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
{authored: "gainsboro", name: "gainsboro", hex: "#DCDCDC", hsl: "hsl(0, 0%, 86%)", rgb: "rgb(220, 220, 220)"},
{authored: "ghostwhite", name: "ghostwhite", hex: "#F8F8FF", hsl: "hsl(240, 100%, 99%)", rgb: "rgb(248, 248, 255)"},
{authored: "gold", name: "gold", hex: "#FFD700", hsl: "hsl(51, 100%, 50%)", rgb: "rgb(255, 215, 0)"},
{authored: "goldenrod", name: "goldenrod", hex: "#DAA520", hsl: "hsl(43, 74%, 49%)", rgb: "rgb(218, 165, 32)"},
{authored: "dodgerblue", name: "dodgerblue", hex: "#1e90ff", hsl: "hsl(210, 100%, 56%)", rgb: "rgb(30, 144, 255)"},
{authored: "firebrick", name: "firebrick", hex: "#b22222", hsl: "hsl(0, 68%, 42%)", rgb: "rgb(178, 34, 34)"},
{authored: "floralwhite", name: "floralwhite", hex: "#fffaf0", hsl: "hsl(40, 100%, 97%)", rgb: "rgb(255, 250, 240)"},
{authored: "forestgreen", name: "forestgreen", hex: "#228b22", hsl: "hsl(120, 61%, 34%)", rgb: "rgb(34, 139, 34)"},
{authored: "fuchsia", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
{authored: "gainsboro", name: "gainsboro", hex: "#dcdcdc", hsl: "hsl(0, 0%, 86%)", rgb: "rgb(220, 220, 220)"},
{authored: "ghostwhite", name: "ghostwhite", hex: "#f8f8ff", hsl: "hsl(240, 100%, 99%)", rgb: "rgb(248, 248, 255)"},
{authored: "gold", name: "gold", hex: "#ffd700", hsl: "hsl(51, 100%, 50%)", rgb: "rgb(255, 215, 0)"},
{authored: "goldenrod", name: "goldenrod", hex: "#daa520", hsl: "hsl(43, 74%, 49%)", rgb: "rgb(218, 165, 32)"},
{authored: "gray", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50%)", rgb: "rgb(128, 128, 128)"},
{authored: "green", name: "green", hex: "#008000", hsl: "hsl(120, 100%, 25%)", rgb: "rgb(0, 128, 0)"},
{authored: "greenyellow", name: "greenyellow", hex: "#ADFF2F", hsl: "hsl(84, 100%, 59%)", rgb: "rgb(173, 255, 47)"},
{authored: "greenyellow", name: "greenyellow", hex: "#adff2f", hsl: "hsl(84, 100%, 59%)", rgb: "rgb(173, 255, 47)"},
{authored: "grey", name: "gray", hex: "#808080", hsl: "hsl(0, 0%, 50%)", rgb: "rgb(128, 128, 128)"},
{authored: "honeydew", name: "honeydew", hex: "#F0FFF0", hsl: "hsl(120, 100%, 97%)", rgb: "rgb(240, 255, 240)"},
{authored: "hotpink", name: "hotpink", hex: "#FF69B4", hsl: "hsl(330, 100%, 71%)", rgb: "rgb(255, 105, 180)"},
{authored: "indianred", name: "indianred", hex: "#CD5C5C", hsl: "hsl(0, 53%, 58%)", rgb: "rgb(205, 92, 92)"},
{authored: "indigo", name: "indigo", hex: "#4B0082", hsl: "hsl(275, 100%, 25%)", rgb: "rgb(75, 0, 130)"},
{authored: "ivory", name: "ivory", hex: "#FFFFF0", hsl: "hsl(60, 100%, 97%)", rgb: "rgb(255, 255, 240)"},
{authored: "khaki", name: "khaki", hex: "#F0E68C", hsl: "hsl(54, 77%, 75%)", rgb: "rgb(240, 230, 140)"},
{authored: "lavender", name: "lavender", hex: "#E6E6FA", hsl: "hsl(240, 67%, 94%)", rgb: "rgb(230, 230, 250)"},
{authored: "lavenderblush", name: "lavenderblush", hex: "#FFF0F5", hsl: "hsl(340, 100%, 97%)", rgb: "rgb(255, 240, 245)"},
{authored: "lawngreen", name: "lawngreen", hex: "#7CFC00", hsl: "hsl(90, 100%, 49%)", rgb: "rgb(124, 252, 0)"},
{authored: "lemonchiffon", name: "lemonchiffon", hex: "#FFFACD", hsl: "hsl(54, 100%, 90%)", rgb: "rgb(255, 250, 205)"},
{authored: "lightblue", name: "lightblue", hex: "#ADD8E6", hsl: "hsl(195, 53%, 79%)", rgb: "rgb(173, 216, 230)"},
{authored: "lightcoral", name: "lightcoral", hex: "#F08080", hsl: "hsl(0, 79%, 72%)", rgb: "rgb(240, 128, 128)"},
{authored: "lightcyan", name: "lightcyan", hex: "#E0FFFF", hsl: "hsl(180, 100%, 94%)", rgb: "rgb(224, 255, 255)"},
{authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#FAFAD2", hsl: "hsl(60, 80%, 90%)", rgb: "rgb(250, 250, 210)"},
{authored: "lightgray", name: "lightgray", hex: "#D3D3D3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
{authored: "lightgreen", name: "lightgreen", hex: "#90EE90", hsl: "hsl(120, 73%, 75%)", rgb: "rgb(144, 238, 144)"},
{authored: "lightgrey", name: "lightgray", hex: "#D3D3D3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
{authored: "lightpink", name: "lightpink", hex: "#FFB6C1", hsl: "hsl(351, 100%, 86%)", rgb: "rgb(255, 182, 193)"},
{authored: "lightsalmon", name: "lightsalmon", hex: "#FFA07A", hsl: "hsl(17, 100%, 74%)", rgb: "rgb(255, 160, 122)"},
{authored: "lightseagreen", name: "lightseagreen", hex: "#20B2AA", hsl: "hsl(177, 70%, 41%)", rgb: "rgb(32, 178, 170)"},
{authored: "lightskyblue", name: "lightskyblue", hex: "#87CEFA", hsl: "hsl(203, 92%, 75%)", rgb: "rgb(135, 206, 250)"},
{authored: "honeydew", name: "honeydew", hex: "#f0fff0", hsl: "hsl(120, 100%, 97%)", rgb: "rgb(240, 255, 240)"},
{authored: "hotpink", name: "hotpink", hex: "#ff69b4", hsl: "hsl(330, 100%, 71%)", rgb: "rgb(255, 105, 180)"},
{authored: "indianred", name: "indianred", hex: "#cd5c5c", hsl: "hsl(0, 53%, 58%)", rgb: "rgb(205, 92, 92)"},
{authored: "indigo", name: "indigo", hex: "#4b0082", hsl: "hsl(275, 100%, 25%)", rgb: "rgb(75, 0, 130)"},
{authored: "ivory", name: "ivory", hex: "#fffff0", hsl: "hsl(60, 100%, 97%)", rgb: "rgb(255, 255, 240)"},
{authored: "khaki", name: "khaki", hex: "#f0e68c", hsl: "hsl(54, 77%, 75%)", rgb: "rgb(240, 230, 140)"},
{authored: "lavender", name: "lavender", hex: "#e6e6fa", hsl: "hsl(240, 67%, 94%)", rgb: "rgb(230, 230, 250)"},
{authored: "lavenderblush", name: "lavenderblush", hex: "#fff0f5", hsl: "hsl(340, 100%, 97%)", rgb: "rgb(255, 240, 245)"},
{authored: "lawngreen", name: "lawngreen", hex: "#7cfc00", hsl: "hsl(90, 100%, 49%)", rgb: "rgb(124, 252, 0)"},
{authored: "lemonchiffon", name: "lemonchiffon", hex: "#fffacd", hsl: "hsl(54, 100%, 90%)", rgb: "rgb(255, 250, 205)"},
{authored: "lightblue", name: "lightblue", hex: "#add8e6", hsl: "hsl(195, 53%, 79%)", rgb: "rgb(173, 216, 230)"},
{authored: "lightcoral", name: "lightcoral", hex: "#f08080", hsl: "hsl(0, 79%, 72%)", rgb: "rgb(240, 128, 128)"},
{authored: "lightcyan", name: "lightcyan", hex: "#e0ffff", hsl: "hsl(180, 100%, 94%)", rgb: "rgb(224, 255, 255)"},
{authored: "lightgoldenrodyellow", name: "lightgoldenrodyellow", hex: "#fafad2", hsl: "hsl(60, 80%, 90%)", rgb: "rgb(250, 250, 210)"},
{authored: "lightgray", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
{authored: "lightgreen", name: "lightgreen", hex: "#90ee90", hsl: "hsl(120, 73%, 75%)", rgb: "rgb(144, 238, 144)"},
{authored: "lightgrey", name: "lightgray", hex: "#d3d3d3", hsl: "hsl(0, 0%, 83%)", rgb: "rgb(211, 211, 211)"},
{authored: "lightpink", name: "lightpink", hex: "#ffb6c1", hsl: "hsl(351, 100%, 86%)", rgb: "rgb(255, 182, 193)"},
{authored: "lightsalmon", name: "lightsalmon", hex: "#ffa07a", hsl: "hsl(17, 100%, 74%)", rgb: "rgb(255, 160, 122)"},
{authored: "lightseagreen", name: "lightseagreen", hex: "#20b2aa", hsl: "hsl(177, 70%, 41%)", rgb: "rgb(32, 178, 170)"},
{authored: "lightskyblue", name: "lightskyblue", hex: "#87cefa", hsl: "hsl(203, 92%, 75%)", rgb: "rgb(135, 206, 250)"},
{authored: "lightslategray", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14%, 53%)", rgb: "rgb(119, 136, 153)"},
{authored: "lightslategrey", name: "lightslategray", hex: "#789", hsl: "hsl(210, 14%, 53%)", rgb: "rgb(119, 136, 153)"},
{authored: "lightsteelblue", name: "lightsteelblue", hex: "#B0C4DE", hsl: "hsl(214, 41%, 78%)", rgb: "rgb(176, 196, 222)"},
{authored: "lightyellow", name: "lightyellow", hex: "#FFFFE0", hsl: "hsl(60, 100%, 94%)", rgb: "rgb(255, 255, 224)"},
{authored: "lime", name: "lime", hex: "#0F0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)"},
{authored: "limegreen", name: "limegreen", hex: "#32CD32", hsl: "hsl(120, 61%, 50%)", rgb: "rgb(50, 205, 50)"},
{authored: "linen", name: "linen", hex: "#FAF0E6", hsl: "hsl(30, 67%, 94%)", rgb: "rgb(250, 240, 230)"},
{authored: "magenta", name: "fuchsia", hex: "#F0F", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
{authored: "lightsteelblue", name: "lightsteelblue", hex: "#b0c4de", hsl: "hsl(214, 41%, 78%)", rgb: "rgb(176, 196, 222)"},
{authored: "lightyellow", name: "lightyellow", hex: "#ffffe0", hsl: "hsl(60, 100%, 94%)", rgb: "rgb(255, 255, 224)"},
{authored: "lime", name: "lime", hex: "#0f0", hsl: "hsl(120, 100%, 50%)", rgb: "rgb(0, 255, 0)"},
{authored: "limegreen", name: "limegreen", hex: "#32cd32", hsl: "hsl(120, 61%, 50%)", rgb: "rgb(50, 205, 50)"},
{authored: "linen", name: "linen", hex: "#faf0e6", hsl: "hsl(30, 67%, 94%)", rgb: "rgb(250, 240, 230)"},
{authored: "magenta", name: "fuchsia", hex: "#f0f", hsl: "hsl(300, 100%, 50%)", rgb: "rgb(255, 0, 255)"},
{authored: "maroon", name: "maroon", hex: "#800000", hsl: "hsl(0, 100%, 25%)", rgb: "rgb(128, 0, 0)"},
{authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66CDAA", hsl: "hsl(160, 51%, 60%)", rgb: "rgb(102, 205, 170)"},
{authored: "mediumblue", name: "mediumblue", hex: "#0000CD", hsl: "hsl(240, 100%, 40%)", rgb: "rgb(0, 0, 205)"},
{authored: "mediumorchid", name: "mediumorchid", hex: "#BA55D3", hsl: "hsl(288, 59%, 58%)", rgb: "rgb(186, 85, 211)"},
{authored: "mediumpurple", name: "mediumpurple", hex: "#9370DB", hsl: "hsl(260, 60%, 65%)", rgb: "rgb(147, 112, 219)"},
{authored: "mediumseagreen", name: "mediumseagreen", hex: "#3CB371", hsl: "hsl(147, 50%, 47%)", rgb: "rgb(60, 179, 113)"},
{authored: "mediumslateblue", name: "mediumslateblue", hex: "#7B68EE", hsl: "hsl(249, 80%, 67%)", rgb: "rgb(123, 104, 238)"},
{authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00FA9A", hsl: "hsl(157, 100%, 49%)", rgb: "rgb(0, 250, 154)"},
{authored: "mediumturquoise", name: "mediumturquoise", hex: "#48D1CC", hsl: "hsl(178, 60%, 55%)", rgb: "rgb(72, 209, 204)"},
{authored: "mediumvioletred", name: "mediumvioletred", hex: "#C71585", hsl: "hsl(322, 81%, 43%)", rgb: "rgb(199, 21, 133)"},
{authored: "mediumaquamarine", name: "mediumaquamarine", hex: "#66cdaa", hsl: "hsl(160, 51%, 60%)", rgb: "rgb(102, 205, 170)"},
{authored: "mediumblue", name: "mediumblue", hex: "#0000cd", hsl: "hsl(240, 100%, 40%)", rgb: "rgb(0, 0, 205)"},
{authored: "mediumorchid", name: "mediumorchid", hex: "#ba55d3", hsl: "hsl(288, 59%, 58%)", rgb: "rgb(186, 85, 211)"},
{authored: "mediumpurple", name: "mediumpurple", hex: "#9370db", hsl: "hsl(260, 60%, 65%)", rgb: "rgb(147, 112, 219)"},
{authored: "mediumseagreen", name: "mediumseagreen", hex: "#3cb371", hsl: "hsl(147, 50%, 47%)", rgb: "rgb(60, 179, 113)"},
{authored: "mediumslateblue", name: "mediumslateblue", hex: "#7b68ee", hsl: "hsl(249, 80%, 67%)", rgb: "rgb(123, 104, 238)"},
{authored: "mediumspringgreen", name: "mediumspringgreen", hex: "#00fa9a", hsl: "hsl(157, 100%, 49%)", rgb: "rgb(0, 250, 154)"},
{authored: "mediumturquoise", name: "mediumturquoise", hex: "#48d1cc", hsl: "hsl(178, 60%, 55%)", rgb: "rgb(72, 209, 204)"},
{authored: "mediumvioletred", name: "mediumvioletred", hex: "#c71585", hsl: "hsl(322, 81%, 43%)", rgb: "rgb(199, 21, 133)"},
{authored: "midnightblue", name: "midnightblue", hex: "#191970", hsl: "hsl(240, 64%, 27%)", rgb: "rgb(25, 25, 112)"},
{authored: "mintcream", name: "mintcream", hex: "#F5FFFA", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)"},
{authored: "mistyrose", name: "mistyrose", hex: "#FFE4E1", hsl: "hsl(6, 100%, 94%)", rgb: "rgb(255, 228, 225)"},
{authored: "moccasin", name: "moccasin", hex: "#FFE4B5", hsl: "hsl(38, 100%, 85%)", rgb: "rgb(255, 228, 181)"},
{authored: "navajowhite", name: "navajowhite", hex: "#FFDEAD", hsl: "hsl(36, 100%, 84%)", rgb: "rgb(255, 222, 173)"},
{authored: "mintcream", name: "mintcream", hex: "#f5fffa", hsl: "hsl(150, 100%, 98%)", rgb: "rgb(245, 255, 250)"},
{authored: "mistyrose", name: "mistyrose", hex: "#ffe4e1", hsl: "hsl(6, 100%, 94%)", rgb: "rgb(255, 228, 225)"},
{authored: "moccasin", name: "moccasin", hex: "#ffe4b5", hsl: "hsl(38, 100%, 85%)", rgb: "rgb(255, 228, 181)"},
{authored: "navajowhite", name: "navajowhite", hex: "#ffdead", hsl: "hsl(36, 100%, 84%)", rgb: "rgb(255, 222, 173)"},
{authored: "navy", name: "navy", hex: "#000080", hsl: "hsl(240, 100%, 25%)", rgb: "rgb(0, 0, 128)"},
{authored: "oldlace", name: "oldlace", hex: "#FDF5E6", hsl: "hsl(39, 85%, 95%)", rgb: "rgb(253, 245, 230)"},
{authored: "oldlace", name: "oldlace", hex: "#fdf5e6", hsl: "hsl(39, 85%, 95%)", rgb: "rgb(253, 245, 230)"},
{authored: "olive", name: "olive", hex: "#808000", hsl: "hsl(60, 100%, 25%)", rgb: "rgb(128, 128, 0)"},
{authored: "olivedrab", name: "olivedrab", hex: "#6B8E23", hsl: "hsl(80, 60%, 35%)", rgb: "rgb(107, 142, 35)"},
{authored: "orange", name: "orange", hex: "#FFA500", hsl: "hsl(39, 100%, 50%)", rgb: "rgb(255, 165, 0)"},
{authored: "orangered", name: "orangered", hex: "#FF4500", hsl: "hsl(16, 100%, 50%)", rgb: "rgb(255, 69, 0)"},
{authored: "orchid", name: "orchid", hex: "#DA70D6", hsl: "hsl(302, 59%, 65%)", rgb: "rgb(218, 112, 214)"},
{authored: "palegoldenrod", name: "palegoldenrod", hex: "#EEE8AA", hsl: "hsl(55, 67%, 80%)", rgb: "rgb(238, 232, 170)"},
{authored: "palegreen", name: "palegreen", hex: "#98FB98", hsl: "hsl(120, 93%, 79%)", rgb: "rgb(152, 251, 152)"},
{authored: "paleturquoise", name: "paleturquoise", hex: "#AFEEEE", hsl: "hsl(180, 65%, 81%)", rgb: "rgb(175, 238, 238)"},
{authored: "palevioletred", name: "palevioletred", hex: "#DB7093", hsl: "hsl(340, 60%, 65%)", rgb: "rgb(219, 112, 147)"},
{authored: "papayawhip", name: "papayawhip", hex: "#FFEFD5", hsl: "hsl(37, 100%, 92%)", rgb: "rgb(255, 239, 213)"},
{authored: "peachpuff", name: "peachpuff", hex: "#FFDAB9", hsl: "hsl(28, 100%, 86%)", rgb: "rgb(255, 218, 185)"},
{authored: "peru", name: "peru", hex: "#CD853F", hsl: "hsl(30, 59%, 53%)", rgb: "rgb(205, 133, 63)"},
{authored: "pink", name: "pink", hex: "#FFC0CB", hsl: "hsl(350, 100%, 88%)", rgb: "rgb(255, 192, 203)"},
{authored: "plum", name: "plum", hex: "#DDA0DD", hsl: "hsl(300, 47%, 75%)", rgb: "rgb(221, 160, 221)"},
{authored: "powderblue", name: "powderblue", hex: "#B0E0E6", hsl: "hsl(187, 52%, 80%)", rgb: "rgb(176, 224, 230)"},
{authored: "olivedrab", name: "olivedrab", hex: "#6b8e23", hsl: "hsl(80, 60%, 35%)", rgb: "rgb(107, 142, 35)"},
{authored: "orange", name: "orange", hex: "#ffa500", hsl: "hsl(39, 100%, 50%)", rgb: "rgb(255, 165, 0)"},
{authored: "orangered", name: "orangered", hex: "#ff4500", hsl: "hsl(16, 100%, 50%)", rgb: "rgb(255, 69, 0)"},
{authored: "orchid", name: "orchid", hex: "#da70d6", hsl: "hsl(302, 59%, 65%)", rgb: "rgb(218, 112, 214)"},
{authored: "palegoldenrod", name: "palegoldenrod", hex: "#eee8aa", hsl: "hsl(55, 67%, 80%)", rgb: "rgb(238, 232, 170)"},
{authored: "palegreen", name: "palegreen", hex: "#98fb98", hsl: "hsl(120, 93%, 79%)", rgb: "rgb(152, 251, 152)"},
{authored: "paleturquoise", name: "paleturquoise", hex: "#afeeee", hsl: "hsl(180, 65%, 81%)", rgb: "rgb(175, 238, 238)"},
{authored: "palevioletred", name: "palevioletred", hex: "#db7093", hsl: "hsl(340, 60%, 65%)", rgb: "rgb(219, 112, 147)"},
{authored: "papayawhip", name: "papayawhip", hex: "#ffefd5", hsl: "hsl(37, 100%, 92%)", rgb: "rgb(255, 239, 213)"},
{authored: "peachpuff", name: "peachpuff", hex: "#ffdab9", hsl: "hsl(28, 100%, 86%)", rgb: "rgb(255, 218, 185)"},
{authored: "peru", name: "peru", hex: "#cd853f", hsl: "hsl(30, 59%, 53%)", rgb: "rgb(205, 133, 63)"},
{authored: "pink", name: "pink", hex: "#ffc0cb", hsl: "hsl(350, 100%, 88%)", rgb: "rgb(255, 192, 203)"},
{authored: "plum", name: "plum", hex: "#dda0dd", hsl: "hsl(300, 47%, 75%)", rgb: "rgb(221, 160, 221)"},
{authored: "powderblue", name: "powderblue", hex: "#b0e0e6", hsl: "hsl(187, 52%, 80%)", rgb: "rgb(176, 224, 230)"},
{authored: "purple", name: "purple", hex: "#800080", hsl: "hsl(300, 100%, 25%)", rgb: "rgb(128, 0, 128)"},
{authored: "rebeccapurple", name: "rebeccapurple", hex: "#639", hsl: "hsl(270, 50%, 40%)", rgb: "rgb(102, 51, 153)"},
{authored: "red", name: "red", hex: "#F00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)"},
{authored: "rosybrown", name: "rosybrown", hex: "#BC8F8F", hsl: "hsl(0, 25%, 65%)", rgb: "rgb(188, 143, 143)"},
{authored: "royalblue", name: "royalblue", hex: "#4169E1", hsl: "hsl(225, 73%, 57%)", rgb: "rgb(65, 105, 225)"},
{authored: "saddlebrown", name: "saddlebrown", hex: "#8B4513", hsl: "hsl(25, 76%, 31%)", rgb: "rgb(139, 69, 19)"},
{authored: "salmon", name: "salmon", hex: "#FA8072", hsl: "hsl(6, 93%, 71%)", rgb: "rgb(250, 128, 114)"},
{authored: "sandybrown", name: "sandybrown", hex: "#F4A460", hsl: "hsl(28, 87%, 67%)", rgb: "rgb(244, 164, 96)"},
{authored: "seagreen", name: "seagreen", hex: "#2E8B57", hsl: "hsl(146, 50%, 36%)", rgb: "rgb(46, 139, 87)"},
{authored: "seashell", name: "seashell", hex: "#FFF5EE", hsl: "hsl(25, 100%, 97%)", rgb: "rgb(255, 245, 238)"},
{authored: "sienna", name: "sienna", hex: "#A0522D", hsl: "hsl(19, 56%, 40%)", rgb: "rgb(160, 82, 45)"},
{authored: "silver", name: "silver", hex: "#C0C0C0", hsl: "hsl(0, 0%, 75%)", rgb: "rgb(192, 192, 192)"},
{authored: "skyblue", name: "skyblue", hex: "#87CEEB", hsl: "hsl(197, 71%, 73%)", rgb: "rgb(135, 206, 235)"},
{authored: "slateblue", name: "slateblue", hex: "#6A5ACD", hsl: "hsl(248, 53%, 58%)", rgb: "rgb(106, 90, 205)"},
{authored: "red", name: "red", hex: "#f00", hsl: "hsl(0, 100%, 50%)", rgb: "rgb(255, 0, 0)"},
{authored: "rosybrown", name: "rosybrown", hex: "#bc8f8f", hsl: "hsl(0, 25%, 65%)", rgb: "rgb(188, 143, 143)"},
{authored: "royalblue", name: "royalblue", hex: "#4169e1", hsl: "hsl(225, 73%, 57%)", rgb: "rgb(65, 105, 225)"},
{authored: "saddlebrown", name: "saddlebrown", hex: "#8b4513", hsl: "hsl(25, 76%, 31%)", rgb: "rgb(139, 69, 19)"},
{authored: "salmon", name: "salmon", hex: "#fa8072", hsl: "hsl(6, 93%, 71%)", rgb: "rgb(250, 128, 114)"},
{authored: "sandybrown", name: "sandybrown", hex: "#f4a460", hsl: "hsl(28, 87%, 67%)", rgb: "rgb(244, 164, 96)"},
{authored: "seagreen", name: "seagreen", hex: "#2e8b57", hsl: "hsl(146, 50%, 36%)", rgb: "rgb(46, 139, 87)"},
{authored: "seashell", name: "seashell", hex: "#fff5ee", hsl: "hsl(25, 100%, 97%)", rgb: "rgb(255, 245, 238)"},
{authored: "sienna", name: "sienna", hex: "#a0522d", hsl: "hsl(19, 56%, 40%)", rgb: "rgb(160, 82, 45)"},
{authored: "silver", name: "silver", hex: "#c0c0c0", hsl: "hsl(0, 0%, 75%)", rgb: "rgb(192, 192, 192)"},
{authored: "skyblue", name: "skyblue", hex: "#87ceeb", hsl: "hsl(197, 71%, 73%)", rgb: "rgb(135, 206, 235)"},
{authored: "slateblue", name: "slateblue", hex: "#6a5acd", hsl: "hsl(248, 53%, 58%)", rgb: "rgb(106, 90, 205)"},
{authored: "slategray", name: "slategray", hex: "#708090", hsl: "hsl(210, 13%, 50%)", rgb: "rgb(112, 128, 144)"},
{authored: "slategrey", name: "slategray", hex: "#708090", hsl: "hsl(210, 13%, 50%)", rgb: "rgb(112, 128, 144)"},
{authored: "snow", name: "snow", hex: "#FFFAFA", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)"},
{authored: "springgreen", name: "springgreen", hex: "#00FF7F", hsl: "hsl(150, 100%, 50%)", rgb: "rgb(0, 255, 127)"},
{authored: "steelblue", name: "steelblue", hex: "#4682B4", hsl: "hsl(207, 44%, 49%)", rgb: "rgb(70, 130, 180)"},
{authored: "tan", name: "tan", hex: "#D2B48C", hsl: "hsl(34, 44%, 69%)", rgb: "rgb(210, 180, 140)"},
{authored: "snow", name: "snow", hex: "#fffafa", hsl: "hsl(0, 100%, 99%)", rgb: "rgb(255, 250, 250)"},
{authored: "springgreen", name: "springgreen", hex: "#00ff7f", hsl: "hsl(150, 100%, 50%)", rgb: "rgb(0, 255, 127)"},
{authored: "steelblue", name: "steelblue", hex: "#4682b4", hsl: "hsl(207, 44%, 49%)", rgb: "rgb(70, 130, 180)"},
{authored: "tan", name: "tan", hex: "#d2b48c", hsl: "hsl(34, 44%, 69%)", rgb: "rgb(210, 180, 140)"},
{authored: "teal", name: "teal", hex: "#008080", hsl: "hsl(180, 100%, 25%)", rgb: "rgb(0, 128, 128)"},
{authored: "thistle", name: "thistle", hex: "#D8BFD8", hsl: "hsl(300, 24%, 80%)", rgb: "rgb(216, 191, 216)"},
{authored: "tomato", name: "tomato", hex: "#FF6347", hsl: "hsl(9, 100%, 64%)", rgb: "rgb(255, 99, 71)"},
{authored: "turquoise", name: "turquoise", hex: "#40E0D0", hsl: "hsl(174, 72%, 56%)", rgb: "rgb(64, 224, 208)"},
{authored: "violet", name: "violet", hex: "#EE82EE", hsl: "hsl(300, 76%, 72%)", rgb: "rgb(238, 130, 238)"},
{authored: "wheat", name: "wheat", hex: "#F5DEB3", hsl: "hsl(39, 77%, 83%)", rgb: "rgb(245, 222, 179)"},
{authored: "white", name: "white", hex: "#FFF", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)"},
{authored: "whitesmoke", name: "whitesmoke", hex: "#F5F5F5", hsl: "hsl(0, 0%, 96%)", rgb: "rgb(245, 245, 245)"},
{authored: "yellow", name: "yellow", hex: "#FF0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)"},
{authored: "yellowgreen", name: "yellowgreen", hex: "#9ACD32", hsl: "hsl(80, 61%, 50%)", rgb: "rgb(154, 205, 50)"},
{authored: "thistle", name: "thistle", hex: "#d8bfd8", hsl: "hsl(300, 24%, 80%)", rgb: "rgb(216, 191, 216)"},
{authored: "tomato", name: "tomato", hex: "#ff6347", hsl: "hsl(9, 100%, 64%)", rgb: "rgb(255, 99, 71)"},
{authored: "turquoise", name: "turquoise", hex: "#40e0d0", hsl: "hsl(174, 72%, 56%)", rgb: "rgb(64, 224, 208)"},
{authored: "violet", name: "violet", hex: "#ee82ee", hsl: "hsl(300, 76%, 72%)", rgb: "rgb(238, 130, 238)"},
{authored: "wheat", name: "wheat", hex: "#f5deb3", hsl: "hsl(39, 77%, 83%)", rgb: "rgb(245, 222, 179)"},
{authored: "white", name: "white", hex: "#fff", hsl: "hsl(0, 0%, 100%)", rgb: "rgb(255, 255, 255)"},
{authored: "whitesmoke", name: "whitesmoke", hex: "#f5f5f5", hsl: "hsl(0, 0%, 96%)", rgb: "rgb(245, 245, 245)"},
{authored: "yellow", name: "yellow", hex: "#ff0", hsl: "hsl(60, 100%, 50%)", rgb: "rgb(255, 255, 0)"},
{authored: "yellowgreen", name: "yellowgreen", hex: "#9acd32", hsl: "hsl(80, 61%, 50%)", rgb: "rgb(154, 205, 50)"},
{authored: "rgba(0, 0, 0, 0)", name: "rgba(0, 0, 0, 0)", hex: "rgba(0, 0, 0, 0)", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)"},
{authored: "hsla(0, 0%, 0%, 0)", name: "rgba(0, 0, 0, 0)", hex: "rgba(0, 0, 0, 0)", hsl: "hsla(0, 0%, 0%, 0)", rgb: "rgba(0, 0, 0, 0)"},
{authored: "rgba(50, 60, 70, 0.5)", name: "rgba(50, 60, 70, 0.5)", hex: "rgba(50, 60, 70, 0.5)", hsl: "hsla(210, 17%, 24%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)"},
{authored: "rgba(0, 0, 0, 0.3)", name: "rgba(0, 0, 0, 0.3)", hex: "rgba(0, 0, 0, 0.3)", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)"},
{authored: "rgba(255, 255, 255, 0.6)", name: "rgba(255, 255, 255, 0.6)", hex: "rgba(255, 255, 255, 0.6)", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)"},
{authored: "rgba(127, 89, 45, 1)", name: "#7F592D", hex: "#7F592D", hsl: "hsl(32, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
{authored: "hsla(19.304, 56%, 40%, 1)", name: "#9F512C", hex: "#9F512C", hsl: "hsl(19, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
{authored: "rgba(127, 89, 45, 1)", name: "#7f592d", hex: "#7f592d", hsl: "hsl(32, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
{authored: "hsla(19.304, 56%, 40%, 1)", name: "#9f512c", hex: "#9f512c", hsl: "hsl(19, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
{authored: "currentcolor", name: "currentcolor", hex: "currentcolor", hsl: "currentcolor", rgb: "currentcolor"},
{authored: "inherit", name: "inherit", hex: "inherit", hsl: "inherit", rgb: "inherit"},
{authored: "initial", name: "initial", hex: "initial", hsl: "initial", rgb: "initial"},

View File

@ -9,7 +9,7 @@
const TEST_URI = `
<style type="text/css">
.matches {
color: #F00;
color: #f00;
}
</style>
<span id="matches" class="matches">Some styled text</span>
@ -41,7 +41,7 @@ function checkColorCycling(container, inspector) {
// Hex
EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
is(valueNode.textContent, "#f00", "Color displayed as a hex value.");
// HSL
EventUtils.synthesizeMouseAtCenter(swatch,

View File

@ -91,8 +91,10 @@ function* overrideTest() {
function* colorEditingTest() {
let colors = [
{name: "hex", text: "#f0c", result: "#0F0"},
{name: "rgb", text: "rgb(0,128,250)", result: "rgb(0, 255, 0)"}
{name: "hex", text: "#f0c", result: "#0f0"},
{name: "rgb", text: "rgb(0,128,250)", result: "rgb(0, 255, 0)"},
// Test case preservation.
{name: "hex", text: "#F0C", result: "#0F0"},
];
Services.prefs.setCharPref("devtools.defaultColorUnit", "authored");

View File

@ -17,7 +17,7 @@ const TEST_URI = `
add_task(function*() {
let TESTS = [
{name: "hex", result: "#0F0"},
{name: "hex", result: "#0f0"},
{name: "rgb", result: "rgb(0, 255, 0)"}
];

View File

@ -9,7 +9,7 @@
const TEST_URI = `
<style type="text/css">
body {
color: #F00;
color: #f00;
}
</style>
Test cycling color types in the rule view!
@ -28,7 +28,7 @@ function checkColorCycling(container, inspector) {
let win = inspector.sidebar.getWindowForTab("ruleview");
// Hex
is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
is(valueNode.textContent, "#f00", "Color displayed as a hex value.");
// HSL
EventUtils.synthesizeMouseAtCenter(swatch,
@ -51,7 +51,7 @@ function checkColorCycling(container, inspector) {
// "Authored"
EventUtils.synthesizeMouseAtCenter(swatch,
{type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "#F00",
is(valueNode.textContent, "#f00",
"Color displayed as an authored value.");
// One more click skips hex, because it is the same as authored, and

View File

@ -349,24 +349,26 @@ body {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.animation-target .attribute-name {
padding-left: 4px;
}
.animation-target .node-selector {
.animation-target .node-highlighter {
background: url("chrome://devtools/skin/themes/images/vview-open-inspector.png") no-repeat 0 0;
padding-left: 16px;
margin-right: 5px;
cursor: pointer;
}
.animation-target .node-selector:hover {
.animation-target .node-highlighter:hover {
background-position: -32px 0;
}
.animation-target .node-selector:active {
.animation-target .node-highlighter:active,
.animation-target .node-highlighter.selected {
background-position: -16px 0;
}

View File

@ -30,10 +30,10 @@ const SPECIALVALUES = new Set([
* color.valid === true
* color.transparent === false // transparent has a special status.
* color.name === "red" // returns hex or rgba when no name available.
* color.hex === "#F00" // returns shortHex when available else returns
* color.hex === "#f00" // returns shortHex when available else returns
* longHex. If alpha channel is present then we
* return this.rgba.
* color.longHex === "#FF0000" // If alpha channel is present then we return
* color.longHex === "#ff0000" // If alpha channel is present then we return
* this.rgba.
* color.rgb === "rgb(255, 0, 0)" // If alpha channel is present then we return
* this.rgba.
@ -42,10 +42,10 @@ const SPECIALVALUES = new Set([
* color.hsla === "hsla(0, 100%, 50%, 1)" // If alpha channel is present
* then we return this.rgba.
*
* color.toString() === "#F00"; // Outputs the color type determined in the
* color.toString() === "#f00"; // Outputs the color type determined in the
* COLOR_UNIT_PREF constant (above).
* // Color objects can be reused
* color.newColor("green") === "#0F0"; // true
* color.newColor("green") === "#0f0"; // true
*
* Valid values for COLOR_UNIT_PREF are contained in CssColor.COLORUNIT.
*/
@ -74,6 +74,7 @@ CssColor.COLORUNIT = {
CssColor.prototype = {
_colorUnit: null,
_colorUnitUppercase: false,
// The value as-authored.
authored: null,
@ -84,6 +85,8 @@ CssColor.prototype = {
if (this._colorUnit === null) {
let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
this._colorUnit = CssColor.COLORUNIT[defaultUnit];
this._colorUnitUppercase =
(this.authored === this.authored.toUpperCase());
}
return this._colorUnit;
},
@ -103,6 +106,7 @@ CssColor.prototype = {
if (Services.prefs.getCharPref(COLOR_UNIT_PREF) ===
CssColor.COLORUNIT.authored) {
this._colorUnit = classifyColor(color);
this._colorUnitUppercase = (color === color.toUpperCase());
}
},
@ -180,7 +184,7 @@ CssColor.prototype = {
}
let tuple = this._getRGBATuple();
return "#" + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0)).toString(16).substr(-6).toUpperCase();
return "#" + ((1 << 24) + (tuple.r << 16) + (tuple.g << 8) + (tuple.b << 0)).toString(16).substr(-6);
},
get rgb() {
@ -325,6 +329,12 @@ CssColor.prototype = {
default:
color = this.rgb;
}
if (this._colorUnitUppercase &&
this.colorUnit != CssColor.COLORUNIT.authored) {
color = color.toUpperCase();
}
return color;
},