mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge fx-team to central, a=merge
This commit is contained in:
commit
6e6909afd7
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
})();
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
@ -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);
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 |
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
})();
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
|
@ -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}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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"
|
||||
]);
|
||||
|
@ -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
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"}
|
||||
),
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 = "";
|
||||
})
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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)]);
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
||||
|
@ -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"},
|
||||
|
@ -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,
|
||||
|
@ -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");
|
||||
|
@ -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)"}
|
||||
];
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user