forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			929 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			929 lines
		
	
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /** @jsx React.DOM */
 | |
| 
 | |
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | |
|  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| /* global loop:true, React, MozActivity */
 | |
| /* jshint newcap:false, maxlen:false */
 | |
| 
 | |
| var loop = loop || {};
 | |
| loop.webapp = (function($, _, OT, mozL10n) {
 | |
|   "use strict";
 | |
| 
 | |
|   loop.config = loop.config || {};
 | |
|   loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
 | |
| 
 | |
|   var sharedMixins = loop.shared.mixins;
 | |
|   var sharedModels = loop.shared.models;
 | |
|   var sharedViews = loop.shared.views;
 | |
|   var sharedUtils = loop.shared.utils;
 | |
| 
 | |
|   /**
 | |
|    * Homepage view.
 | |
|    */
 | |
|   var HomeView = React.createClass({displayName: 'HomeView',
 | |
|     render: function() {
 | |
|       return (
 | |
|         React.DOM.p(null, mozL10n.get("welcome"))
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Unsupported Browsers view.
 | |
|    */
 | |
|   var UnsupportedBrowserView = React.createClass({displayName: 'UnsupportedBrowserView',
 | |
|     render: function() {
 | |
|       var useLatestFF = mozL10n.get("use_latest_firefox", {
 | |
|         "firefoxBrandNameLink": React.renderComponentToStaticMarkup(
 | |
|           React.DOM.a({target: "_blank", href: "https://www.mozilla.org/firefox/"}, "Firefox")
 | |
|         )
 | |
|       });
 | |
|       return (
 | |
|         React.DOM.div(null, 
 | |
|           React.DOM.h2(null, mozL10n.get("incompatible_browser")), 
 | |
|           React.DOM.p(null, mozL10n.get("powered_by_webrtc")), 
 | |
|           React.DOM.p({dangerouslySetInnerHTML: {__html: useLatestFF}})
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Unsupported Device view.
 | |
|    */
 | |
|   var UnsupportedDeviceView = React.createClass({displayName: 'UnsupportedDeviceView',
 | |
|     render: function() {
 | |
|       return (
 | |
|         React.DOM.div(null, 
 | |
|           React.DOM.h2(null, mozL10n.get("incompatible_device")), 
 | |
|           React.DOM.p(null, mozL10n.get("sorry_device_unsupported")), 
 | |
|           React.DOM.p(null, mozL10n.get("use_firefox_windows_mac_linux"))
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Firefox promotion interstitial. Will display only to non-Firefox users.
 | |
|    */
 | |
|   var PromoteFirefoxView = React.createClass({displayName: 'PromoteFirefoxView',
 | |
|     propTypes: {
 | |
|       helper: React.PropTypes.object.isRequired
 | |
|     },
 | |
| 
 | |
|     render: function() {
 | |
|       if (this.props.helper.isFirefox(navigator.userAgent)) {
 | |
|         return React.DOM.div(null);
 | |
|       }
 | |
|       return (
 | |
|         React.DOM.div({className: "promote-firefox"}, 
 | |
|           React.DOM.h3(null, mozL10n.get("promote_firefox_hello_heading")), 
 | |
|           React.DOM.p(null, 
 | |
|             React.DOM.a({className: "btn btn-large btn-accept", 
 | |
|                href: "https://www.mozilla.org/firefox/"}, 
 | |
|               mozL10n.get("get_firefox_button")
 | |
|             )
 | |
|           )
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Expired call URL view.
 | |
|    */
 | |
|   var CallUrlExpiredView = React.createClass({displayName: 'CallUrlExpiredView',
 | |
|     propTypes: {
 | |
|       helper: React.PropTypes.object.isRequired
 | |
|     },
 | |
| 
 | |
|     render: function() {
 | |
|       return (
 | |
|         React.DOM.div({className: "expired-url-info"}, 
 | |
|           React.DOM.div({className: "info-panel"}, 
 | |
|             React.DOM.div({className: "firefox-logo"}), 
 | |
|             React.DOM.h1(null, mozL10n.get("call_url_unavailable_notification_heading")), 
 | |
|             React.DOM.h4(null, mozL10n.get("call_url_unavailable_notification_message2"))
 | |
|           ), 
 | |
|           PromoteFirefoxView({helper: this.props.helper})
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var ConversationBranding = React.createClass({displayName: 'ConversationBranding',
 | |
|     render: function() {
 | |
|       return (
 | |
|         React.DOM.h1({className: "standalone-header-title"}, 
 | |
|           React.DOM.strong(null, mozL10n.get("brandShortname")), 
 | |
|           mozL10n.get("clientShortname2")
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * The Firefox Marketplace exposes a web page that contains a postMesssage
 | |
|    * based API that wraps a small set of functionality from the WebApps API
 | |
|    * that allow us to request the installation of apps given their manifest
 | |
|    * URL. We will be embedding the content of this web page within an hidden
 | |
|    * iframe in case that we need to request the installation of the FxOS Loop
 | |
|    * client.
 | |
|    */
 | |
|   var FxOSHiddenMarketplace = React.createClass({displayName: 'FxOSHiddenMarketplace',
 | |
|     render: function() {
 | |
|       return React.DOM.iframe({id: "marketplace", src: this.props.marketplaceSrc, hidden: true});
 | |
|     },
 | |
| 
 | |
|     componentDidUpdate: function() {
 | |
|       // This happens only once when we change the 'src' property of the iframe.
 | |
|       if (this.props.onMarketplaceMessage) {
 | |
|         // The reason for listening on the global window instead of on the
 | |
|         // iframe content window is because the Marketplace is doing a
 | |
|         // window.top.postMessage.
 | |
|         window.addEventListener("message", this.props.onMarketplaceMessage);
 | |
|       }
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var FxOSConversationModel = Backbone.Model.extend({
 | |
|     setupOutgoingCall: function() {
 | |
|       // The FxOS Loop client exposes a "loop-call" activity. If we get the
 | |
|       // activity onerror callback it means that there is no "loop-call"
 | |
|       // activity handler available and so no FxOS Loop client installed.
 | |
|       var request = new MozActivity({
 | |
|         name: "loop-call",
 | |
|         data: {
 | |
|           type: "loop/token",
 | |
|           token: this.get("loopToken"),
 | |
|           callerId: this.get("callerId"),
 | |
|           callType: this.get("callType")
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       request.onsuccess = function() {};
 | |
| 
 | |
|       request.onerror = (function(event) {
 | |
|         if (event.target.error.name !== "NO_PROVIDER") {
 | |
|           console.error ("Unexpected " + event.target.error.name);
 | |
|           this.trigger("session:error", "fxos_app_needed", {
 | |
|             fxosAppName: loop.config.fxosApp.name
 | |
|           });
 | |
|           return;
 | |
|         }
 | |
|         this.trigger("fxos:app-needed");
 | |
|       }).bind(this);
 | |
|     },
 | |
| 
 | |
|     onMarketplaceMessage: function(event) {
 | |
|       var message = event.data;
 | |
|       switch (message.name) {
 | |
|         case "loaded":
 | |
|           var marketplace = window.document.getElementById("marketplace");
 | |
|           // Once we have it loaded, we request the installation of the FxOS
 | |
|           // Loop client app. We will be receiving the result of this action
 | |
|           // via postMessage from the child iframe.
 | |
|           marketplace.contentWindow.postMessage({
 | |
|             "name": "install-package",
 | |
|             "data": {
 | |
|               "product": {
 | |
|                 "name": loop.config.fxosApp.name,
 | |
|                 "manifest_url": loop.config.fxosApp.manifestUrl,
 | |
|                 "is_packaged": true
 | |
|               }
 | |
|             }
 | |
|           }, "*");
 | |
|           break;
 | |
|         case "install-package":
 | |
|           window.removeEventListener("message", this.onMarketplaceMessage);
 | |
|           if (message.error) {
 | |
|             console.error(message.error.error);
 | |
|             this.trigger("session:error", "fxos_app_needed", {
 | |
|               fxosAppName: loop.config.fxosApp.name
 | |
|             });
 | |
|             return;
 | |
|           }
 | |
|           // We installed the FxOS app \o/, so we can continue with the call
 | |
|           // process.
 | |
|           this.setupOutgoingCall();
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var ConversationHeader = React.createClass({displayName: 'ConversationHeader',
 | |
|     render: function() {
 | |
|       var cx = React.addons.classSet;
 | |
|       var conversationUrl = location.href;
 | |
| 
 | |
|       var urlCreationDateClasses = cx({
 | |
|         "light-color-font": true,
 | |
|         "call-url-date": true, /* Used as a handler in the tests */
 | |
|         /*hidden until date is available*/
 | |
|         "hide": !this.props.urlCreationDateString.length
 | |
|       });
 | |
| 
 | |
|       var callUrlCreationDateString = mozL10n.get("call_url_creation_date_label", {
 | |
|         "call_url_creation_date": this.props.urlCreationDateString
 | |
|       });
 | |
| 
 | |
|       return (
 | |
|         React.DOM.header({className: "standalone-header header-box container-box"}, 
 | |
|           ConversationBranding(null), 
 | |
|           React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}), 
 | |
|           React.DOM.h3({className: "call-url"}, 
 | |
|             conversationUrl
 | |
|           ), 
 | |
|           React.DOM.h4({className: urlCreationDateClasses}, 
 | |
|             callUrlCreationDateString
 | |
|           )
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var ConversationFooter = React.createClass({displayName: 'ConversationFooter',
 | |
|     render: function() {
 | |
|       return (
 | |
|         React.DOM.div({className: "standalone-footer container-box"}, 
 | |
|           React.DOM.div({title: "Mozilla Logo", className: "footer-logo"})
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
 | |
|     getInitialState: function() {
 | |
|       return {
 | |
|         callState: this.props.callState || "connecting"
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     propTypes: {
 | |
|       websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
 | |
|                       .isRequired
 | |
|     },
 | |
| 
 | |
|     componentDidMount: function() {
 | |
|       this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
 | |
|                                     this._handleRingingProgress);
 | |
|     },
 | |
| 
 | |
|     _handleRingingProgress: function() {
 | |
|       this.setState({callState: "ringing"});
 | |
|     },
 | |
| 
 | |
|     _cancelOutgoingCall: function() {
 | |
|       this.props.websocket.cancel();
 | |
|     },
 | |
| 
 | |
|     render: function() {
 | |
|       var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
 | |
|       return (
 | |
|         React.DOM.div({className: "container"}, 
 | |
|           React.DOM.div({className: "container-box"}, 
 | |
|             React.DOM.header({className: "pending-header header-box"}, 
 | |
|               ConversationBranding(null)
 | |
|             ), 
 | |
| 
 | |
|             React.DOM.div({id: "cameraPreview"}), 
 | |
| 
 | |
|             React.DOM.div({id: "messages"}), 
 | |
| 
 | |
|             React.DOM.p({className: "standalone-btn-label"}, 
 | |
|               callState
 | |
|             ), 
 | |
| 
 | |
|             React.DOM.div({className: "btn-pending-cancel-group btn-group"}, 
 | |
|               React.DOM.div({className: "flex-padding-1"}), 
 | |
|               React.DOM.button({className: "btn btn-large btn-cancel", 
 | |
|                       onClick: this._cancelOutgoingCall}, 
 | |
|                 React.DOM.span({className: "standalone-call-btn-text"}, 
 | |
|                   mozL10n.get("initiate_call_cancel_button")
 | |
|                 )
 | |
|               ), 
 | |
|               React.DOM.div({className: "flex-padding-1"})
 | |
|             )
 | |
|           ), 
 | |
|           ConversationFooter(null)
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var InitiateCallButton = React.createClass({displayName: 'InitiateCallButton',
 | |
|     mixins: [sharedMixins.DropdownMenuMixin],
 | |
| 
 | |
|     propTypes: {
 | |
|       caption: React.PropTypes.string.isRequired,
 | |
|       startCall: React.PropTypes.func.isRequired,
 | |
|       disabled: React.PropTypes.bool
 | |
|     },
 | |
| 
 | |
|     getDefaultProps: function() {
 | |
|       return {disabled: false};
 | |
|     },
 | |
| 
 | |
|     render: function() {
 | |
|       var dropdownMenuClasses = React.addons.classSet({
 | |
|         "native-dropdown-large-parent": true,
 | |
|         "standalone-dropdown-menu": true,
 | |
|         "visually-hidden": !this.state.showMenu
 | |
|       });
 | |
|       var chevronClasses = React.addons.classSet({
 | |
|         "btn-chevron": true,
 | |
|         "disabled": this.props.disabled
 | |
|       });
 | |
|       return (
 | |
|         React.DOM.div({className: "standalone-btn-chevron-menu-group"}, 
 | |
|           React.DOM.div({className: "btn-group-chevron"}, 
 | |
|             React.DOM.div({className: "btn-group"}, 
 | |
|               React.DOM.button({className: "btn btn-large btn-accept", 
 | |
|                       onClick: this.props.startCall("audio-video"), 
 | |
|                       disabled: this.props.disabled, 
 | |
|                       title: mozL10n.get("initiate_audio_video_call_tooltip2")}, 
 | |
|                 React.DOM.span({className: "standalone-call-btn-text"}, 
 | |
|                   this.props.caption
 | |
|                 ), 
 | |
|                 React.DOM.span({className: "standalone-call-btn-video-icon"})
 | |
|               ), 
 | |
|               React.DOM.div({className: chevronClasses, 
 | |
|                    onClick: this.toggleDropdownMenu}
 | |
|               )
 | |
|             ), 
 | |
|             React.DOM.ul({className: dropdownMenuClasses}, 
 | |
|               React.DOM.li(null, 
 | |
|                 React.DOM.button({className: "start-audio-only-call", 
 | |
|                         onClick: this.props.startCall("audio"), 
 | |
|                         disabled: this.props.disabled}, 
 | |
|                   mozL10n.get("initiate_audio_call_button2")
 | |
|                 )
 | |
|               )
 | |
|             )
 | |
|           )
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Initiate conversation view.
 | |
|    */
 | |
|   var InitiateConversationView = React.createClass({displayName: 'InitiateConversationView',
 | |
|     mixins: [Backbone.Events],
 | |
| 
 | |
|     propTypes: {
 | |
|       conversation: React.PropTypes.oneOfType([
 | |
|                       React.PropTypes.instanceOf(sharedModels.ConversationModel),
 | |
|                       React.PropTypes.instanceOf(FxOSConversationModel)
 | |
|                     ]).isRequired,
 | |
|       // XXX Check more tightly here when we start injecting window.loop.*
 | |
|       notifications: React.PropTypes.object.isRequired,
 | |
|       client: React.PropTypes.object.isRequired,
 | |
|       title: React.PropTypes.string.isRequired,
 | |
|       callButtonLabel: React.PropTypes.string.isRequired
 | |
|     },
 | |
| 
 | |
|     getInitialState: function() {
 | |
|       return {
 | |
|         urlCreationDateString: '',
 | |
|         disableCallButton: false
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     componentDidMount: function() {
 | |
|       this.listenTo(this.props.conversation,
 | |
|                     "session:error", this._onSessionError);
 | |
|       this.listenTo(this.props.conversation,
 | |
|                     "fxos:app-needed", this._onFxOSAppNeeded);
 | |
|       this.props.client.requestCallUrlInfo(
 | |
|         this.props.conversation.get("loopToken"),
 | |
|         this._setConversationTimestamp);
 | |
|     },
 | |
| 
 | |
|     componentWillUnmount: function() {
 | |
|       this.stopListening(this.props.conversation);
 | |
|       localStorage.setItem("has-seen-tos", "true");
 | |
|     },
 | |
| 
 | |
|     _onSessionError: function(error, l10nProps) {
 | |
|       var errorL10n = error || "unable_retrieve_call_info";
 | |
|       this.props.notifications.errorL10n(errorL10n, l10nProps);
 | |
|       console.error(errorL10n);
 | |
|     },
 | |
| 
 | |
|     _onFxOSAppNeeded: function() {
 | |
|       this.setState({
 | |
|         marketplaceSrc: loop.config.marketplaceUrl,
 | |
|         onMarketplaceMessage: this.props.conversation.onMarketplaceMessage.bind(
 | |
|           this.props.conversation
 | |
|         )
 | |
|       });
 | |
|      },
 | |
| 
 | |
|     /**
 | |
|      * Initiates the call.
 | |
|      * Takes in a call type parameter "audio" or "audio-video" and returns
 | |
|      * a function that initiates the call. React click handler requires a function
 | |
|      * to be called when that event happenes.
 | |
|      *
 | |
|      * @param {string} User call type choice "audio" or "audio-video"
 | |
|      */
 | |
|     startCall: function(callType) {
 | |
|       return function() {
 | |
|         this.props.conversation.setupOutgoingCall(callType);
 | |
|         this.setState({disableCallButton: true});
 | |
|       }.bind(this);
 | |
|     },
 | |
| 
 | |
|     _setConversationTimestamp: function(err, callUrlInfo) {
 | |
|       if (err) {
 | |
|         this.props.notifications.errorL10n("unable_retrieve_call_info");
 | |
|       } else {
 | |
|         var date = (new Date(callUrlInfo.urlCreationDate * 1000));
 | |
|         var options = {year: "numeric", month: "long", day: "numeric"};
 | |
|         var timestamp = date.toLocaleDateString(navigator.language, options);
 | |
|         this.setState({urlCreationDateString: timestamp});
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     render: function() {
 | |
|       var tosLinkName = mozL10n.get("terms_of_use_link_text");
 | |
|       var privacyNoticeName = mozL10n.get("privacy_notice_link_text");
 | |
| 
 | |
|       var tosHTML = mozL10n.get("legal_text_and_links", {
 | |
|         "terms_of_use_url": "<a target=_blank href='/legal/terms/'>" +
 | |
|           tosLinkName + "</a>",
 | |
|         "privacy_notice_url": "<a target=_blank href='" +
 | |
|           "https://www.mozilla.org/privacy/'>" + privacyNoticeName + "</a>"
 | |
|       });
 | |
| 
 | |
|       var tosClasses = React.addons.classSet({
 | |
|         "terms-service": true,
 | |
|         hide: (localStorage.getItem("has-seen-tos") === "true")
 | |
|       });
 | |
| 
 | |
|       return (
 | |
|         React.DOM.div({className: "container"}, 
 | |
|           React.DOM.div({className: "container-box"}, 
 | |
| 
 | |
|             ConversationHeader({
 | |
|               urlCreationDateString: this.state.urlCreationDateString}), 
 | |
| 
 | |
|             React.DOM.p({className: "standalone-btn-label"}, 
 | |
|               this.props.title
 | |
|             ), 
 | |
| 
 | |
|             React.DOM.div({id: "messages"}), 
 | |
| 
 | |
|             React.DOM.div({className: "btn-group"}, 
 | |
|               React.DOM.div({className: "flex-padding-1"}), 
 | |
|               InitiateCallButton({
 | |
|                 caption: this.props.callButtonLabel, 
 | |
|                 disabled: this.state.disableCallButton, 
 | |
|                 startCall: this.startCall}
 | |
|               ), 
 | |
|               React.DOM.div({className: "flex-padding-1"})
 | |
|             ), 
 | |
| 
 | |
|             React.DOM.p({className: tosClasses, 
 | |
|                dangerouslySetInnerHTML: {__html: tosHTML}})
 | |
|           ), 
 | |
| 
 | |
|           FxOSHiddenMarketplace({
 | |
|             marketplaceSrc: this.state.marketplaceSrc, 
 | |
|             onMarketplaceMessage: this.state.onMarketplaceMessage}), 
 | |
| 
 | |
|           ConversationFooter(null)
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Ended conversation view.
 | |
|    */
 | |
|   var EndedConversationView = React.createClass({displayName: 'EndedConversationView',
 | |
|     propTypes: {
 | |
|       conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
 | |
|                          .isRequired,
 | |
|       sdk: React.PropTypes.object.isRequired,
 | |
|       feedbackApiClient: React.PropTypes.object.isRequired,
 | |
|       onAfterFeedbackReceived: React.PropTypes.func.isRequired
 | |
|     },
 | |
| 
 | |
|     render: function() {
 | |
|       return (
 | |
|         React.DOM.div({className: "ended-conversation"}, 
 | |
|           sharedViews.FeedbackView({
 | |
|             feedbackApiClient: this.props.feedbackApiClient, 
 | |
|             onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}
 | |
|           ), 
 | |
|           sharedViews.ConversationView({
 | |
|             initiate: false, 
 | |
|             sdk: this.props.sdk, 
 | |
|             model: this.props.conversation, 
 | |
|             audio: {enabled: false, visible: false}, 
 | |
|             video: {enabled: false, visible: false}}
 | |
|           )
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var StartConversationView = React.createClass({displayName: 'StartConversationView',
 | |
|     render: function() {
 | |
|       return this.transferPropsTo(
 | |
|         InitiateConversationView({
 | |
|           title: mozL10n.get("initiate_call_button_label2"), 
 | |
|           callButtonLabel: mozL10n.get("initiate_audio_video_call_button2")})
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   var FailedConversationView = React.createClass({displayName: 'FailedConversationView',
 | |
|     render: function() {
 | |
|       return this.transferPropsTo(
 | |
|         InitiateConversationView({
 | |
|           title: mozL10n.get("call_failed_title"), 
 | |
|           callButtonLabel: mozL10n.get("retry_call_button")})
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * This view manages the outgoing conversation views - from
 | |
|    * call initiation through to the actual conversation and call end.
 | |
|    *
 | |
|    * At the moment, it does more than that, these parts need refactoring out.
 | |
|    */
 | |
|   var OutgoingConversationView = React.createClass({displayName: 'OutgoingConversationView',
 | |
|     propTypes: {
 | |
|       client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
 | |
|       conversation: React.PropTypes.oneOfType([
 | |
|         React.PropTypes.instanceOf(sharedModels.ConversationModel),
 | |
|         React.PropTypes.instanceOf(FxOSConversationModel)
 | |
|       ]).isRequired,
 | |
|       helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
 | |
|       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
 | |
|                           .isRequired,
 | |
|       sdk: React.PropTypes.object.isRequired,
 | |
|       feedbackApiClient: React.PropTypes.object.isRequired
 | |
|     },
 | |
| 
 | |
|     getInitialState: function() {
 | |
|       return {
 | |
|         callStatus: "start"
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     componentDidMount: function() {
 | |
|       this.props.conversation.on("call:outgoing", this.startCall, this);
 | |
|       this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
 | |
|       this.props.conversation.on("change:publishedStream", this._checkConnected, this);
 | |
|       this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
 | |
|       this.props.conversation.on("session:ended", this._endCall, this);
 | |
|       this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
 | |
|       this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
 | |
|       this.props.conversation.on("session:connection-error", this._notifyError, this);
 | |
|     },
 | |
| 
 | |
|     componentDidUnmount: function() {
 | |
|       this.props.conversation.off(null, null, this);
 | |
|     },
 | |
| 
 | |
|     shouldComponentUpdate: function(nextProps, nextState) {
 | |
|       // Only rerender if current state has actually changed
 | |
|       return nextState.callStatus !== this.state.callStatus;
 | |
|     },
 | |
| 
 | |
|     callStatusSwitcher: function(status) {
 | |
|       return function() {
 | |
|         this.setState({callStatus: status});
 | |
|       }.bind(this);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Renders the conversation views.
 | |
|      */
 | |
|     render: function() {
 | |
|       switch (this.state.callStatus) {
 | |
|         case "start": {
 | |
|           return (
 | |
|             StartConversationView({
 | |
|               conversation: this.props.conversation, 
 | |
|               notifications: this.props.notifications, 
 | |
|               client: this.props.client}
 | |
|             )
 | |
|           );
 | |
|         }
 | |
|         case "failure": {
 | |
|           return (
 | |
|             FailedConversationView({
 | |
|               conversation: this.props.conversation, 
 | |
|               notifications: this.props.notifications, 
 | |
|               client: this.props.client}
 | |
|             )
 | |
|           );
 | |
|         }
 | |
|         case "pending": {
 | |
|           return PendingConversationView({websocket: this._websocket});
 | |
|         }
 | |
|         case "connected": {
 | |
|           return (
 | |
|             sharedViews.ConversationView({
 | |
|               initiate: true, 
 | |
|               sdk: this.props.sdk, 
 | |
|               model: this.props.conversation, 
 | |
|               video: {enabled: this.props.conversation.hasVideoStream("outgoing")}}
 | |
|             )
 | |
|           );
 | |
|         }
 | |
|         case "end": {
 | |
|           return (
 | |
|             EndedConversationView({
 | |
|               sdk: this.props.sdk, 
 | |
|               conversation: this.props.conversation, 
 | |
|               feedbackApiClient: this.props.feedbackApiClient, 
 | |
|               onAfterFeedbackReceived: this.callStatusSwitcher("start")}
 | |
|             )
 | |
|           );
 | |
|         }
 | |
|         case "expired": {
 | |
|           return (
 | |
|             CallUrlExpiredView({helper: this.props.helper})
 | |
|           );
 | |
|         }
 | |
|         default: {
 | |
|           return HomeView(null);
 | |
|         }
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Notify the user that the connection was not possible
 | |
|      * @param {{code: number, message: string}} error
 | |
|      */
 | |
|     _notifyError: function(error) {
 | |
|       console.error(error);
 | |
|       this.props.notifications.errorL10n("connection_error_see_console_notification");
 | |
|       this.setState({callStatus: "end"});
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Peer hung up. Notifies the user and ends the call.
 | |
|      *
 | |
|      * Event properties:
 | |
|      * - {String} connectionId: OT session id
 | |
|      */
 | |
|     _onPeerHungup: function() {
 | |
|       this.props.notifications.warnL10n("peer_ended_conversation2");
 | |
|       this.setState({callStatus: "end"});
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Network disconnected. Notifies the user and ends the call.
 | |
|      */
 | |
|     _onNetworkDisconnected: function() {
 | |
|       this.props.notifications.warnL10n("network_disconnected");
 | |
|       this.setState({callStatus: "end"});
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Starts the set up of a call, obtaining the required information from the
 | |
|      * server.
 | |
|      */
 | |
|     setupOutgoingCall: function() {
 | |
|       var loopToken = this.props.conversation.get("loopToken");
 | |
|       if (!loopToken) {
 | |
|         this.props.notifications.errorL10n("missing_conversation_info");
 | |
|         this.setState({callStatus: "failure"});
 | |
|       } else {
 | |
|         var callType = this.props.conversation.get("selectedCallType");
 | |
| 
 | |
|         this.props.client.requestCallInfo(this.props.conversation.get("loopToken"),
 | |
|                                           callType, function(err, sessionData) {
 | |
|           if (err) {
 | |
|             switch (err.errno) {
 | |
|               // loop-server sends 404 + INVALID_TOKEN (errno 105) whenever a token is
 | |
|               // missing OR expired; we treat this information as if the url is always
 | |
|               // expired.
 | |
|               case 105:
 | |
|                 this.setState({callStatus: "expired"});
 | |
|                 break;
 | |
|               default:
 | |
|                 this.props.notifications.errorL10n("missing_conversation_info");
 | |
|                 this.setState({callStatus: "failure"});
 | |
|                 break;
 | |
|             }
 | |
|             return;
 | |
|           }
 | |
|           this.props.conversation.outgoing(sessionData);
 | |
|         }.bind(this));
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Actually starts the call.
 | |
|      */
 | |
|     startCall: function() {
 | |
|       var loopToken = this.props.conversation.get("loopToken");
 | |
|       if (!loopToken) {
 | |
|         this.props.notifications.errorL10n("missing_conversation_info");
 | |
|         this.setState({callStatus: "failure"});
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this._setupWebSocket();
 | |
|       this.setState({callStatus: "pending"});
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Used to set up the web socket connection and navigate to the
 | |
|      * call view if appropriate.
 | |
|      *
 | |
|      * @param {string} loopToken The session token to use.
 | |
|      */
 | |
|     _setupWebSocket: function() {
 | |
|       this._websocket = new loop.CallConnectionWebSocket({
 | |
|         url: this.props.conversation.get("progressURL"),
 | |
|         websocketToken: this.props.conversation.get("websocketToken"),
 | |
|         callId: this.props.conversation.get("callId"),
 | |
|       });
 | |
|       this._websocket.promiseConnect().then(function() {
 | |
|       }.bind(this), function() {
 | |
|         // XXX Not the ideal response, but bug 1047410 will be replacing
 | |
|         // this by better "call failed" UI.
 | |
|         this.props.notifications.errorL10n("cannot_start_call_session_not_ready");
 | |
|         return;
 | |
|       }.bind(this));
 | |
| 
 | |
|       this._websocket.on("progress", this._handleWebSocketProgress, this);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Checks if the streams have been connected, and notifies the
 | |
|      * websocket that the media is now connected.
 | |
|      */
 | |
|     _checkConnected: function() {
 | |
|       // Check we've had both local and remote streams connected before
 | |
|       // sending the media up message.
 | |
|       if (this.props.conversation.streamsConnected()) {
 | |
|         this._websocket.mediaUp();
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Used to receive websocket progress and to determine how to handle
 | |
|      * it if appropraite.
 | |
|      */
 | |
|     _handleWebSocketProgress: function(progressData) {
 | |
|       switch(progressData.state) {
 | |
|         case "connecting": {
 | |
|           // We just go straight to the connected view as the media gets set up.
 | |
|           this.setState({callStatus: "connected"});
 | |
|           break;
 | |
|         }
 | |
|         case "terminated": {
 | |
|           // At the moment, we show the same text regardless
 | |
|           // of the terminated reason.
 | |
|           this._handleCallTerminated(progressData.reason);
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Handles call rejection.
 | |
|      *
 | |
|      * @param {String} reason The reason the call was terminated (reject, busy,
 | |
|      *                        timeout, cancel, media-fail, user-unknown, closed)
 | |
|      */
 | |
|     _handleCallTerminated: function(reason) {
 | |
|       if (reason === "cancel") {
 | |
|         this.setState({callStatus: "start"});
 | |
|         return;
 | |
|       }
 | |
|       // XXX later, we'll want to display more meaningfull messages (needs UX)
 | |
|       this.props.notifications.errorL10n("call_timeout_notification_text");
 | |
|       this.setState({callStatus: "failure"});
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Handles ending a call by resetting the view to the start state.
 | |
|      */
 | |
|     _endCall: function() {
 | |
|       this.setState({callStatus: "end"});
 | |
|     },
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Webapp Root View. This is the main, single, view that controls the display
 | |
|    * of the webapp page.
 | |
|    */
 | |
|   var WebappRootView = React.createClass({displayName: 'WebappRootView',
 | |
|     propTypes: {
 | |
|       client: React.PropTypes.instanceOf(loop.StandaloneClient).isRequired,
 | |
|       conversation: React.PropTypes.oneOfType([
 | |
|         React.PropTypes.instanceOf(sharedModels.ConversationModel),
 | |
|         React.PropTypes.instanceOf(FxOSConversationModel)
 | |
|       ]).isRequired,
 | |
|       helper: React.PropTypes.instanceOf(sharedUtils.Helper).isRequired,
 | |
|       notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
 | |
|                           .isRequired,
 | |
|       sdk: React.PropTypes.object.isRequired,
 | |
|       feedbackApiClient: React.PropTypes.object.isRequired
 | |
|     },
 | |
| 
 | |
|     getInitialState: function() {
 | |
|       return {
 | |
|         unsupportedDevice: this.props.helper.isIOS(navigator.platform),
 | |
|         unsupportedBrowser: !this.props.sdk.checkSystemRequirements(),
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     render: function() {
 | |
|       if (this.state.unsupportedDevice) {
 | |
|         return UnsupportedDeviceView(null);
 | |
|       } else if (this.state.unsupportedBrowser) {
 | |
|         return UnsupportedBrowserView(null);
 | |
|       } else if (this.props.conversation.get("loopToken")) {
 | |
|         return (
 | |
|           OutgoingConversationView({
 | |
|              client: this.props.client, 
 | |
|              conversation: this.props.conversation, 
 | |
|              helper: this.props.helper, 
 | |
|              notifications: this.props.notifications, 
 | |
|              sdk: this.props.sdk, 
 | |
|              feedbackApiClient: this.props.feedbackApiClient}
 | |
|           )
 | |
|         );
 | |
|       } else {
 | |
|         return HomeView(null);
 | |
|       }
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * App initialization.
 | |
|    */
 | |
|   function init() {
 | |
|     var helper = new sharedUtils.Helper();
 | |
|     var client = new loop.StandaloneClient({
 | |
|       baseServerUrl: loop.config.serverUrl
 | |
|     });
 | |
|     var notifications = new sharedModels.NotificationCollection();
 | |
|     var conversation
 | |
|     if (helper.isFirefoxOS(navigator.userAgent)) {
 | |
|       conversation = new FxOSConversationModel();
 | |
|     } else {
 | |
|       conversation = new sharedModels.ConversationModel({}, {
 | |
|         sdk: OT
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     var feedbackApiClient = new loop.FeedbackAPIClient(
 | |
|       loop.config.feedbackApiUrl, {
 | |
|         product: loop.config.feedbackProductName,
 | |
|         user_agent: navigator.userAgent,
 | |
|         url: document.location.origin
 | |
|       });
 | |
| 
 | |
|     // Obtain the loopToken and pass it to the conversation
 | |
|     var locationHash = helper.locationHash();
 | |
|     if (locationHash) {
 | |
|       conversation.set("loopToken", locationHash.match(/\#call\/(.*)/)[1]);
 | |
|     }
 | |
| 
 | |
|     React.renderComponent(WebappRootView({
 | |
|       client: client, 
 | |
|       conversation: conversation, 
 | |
|       helper: helper, 
 | |
|       notifications: notifications, 
 | |
|       sdk: OT, 
 | |
|       feedbackApiClient: feedbackApiClient}
 | |
|     ), document.querySelector("#main"));
 | |
| 
 | |
|     // Set the 'lang' and 'dir' attributes to <html> when the page is translated
 | |
|     document.documentElement.lang = mozL10n.language.code;
 | |
|     document.documentElement.dir = mozL10n.language.direction;
 | |
|   }
 | |
| 
 | |
|   return {
 | |
|     CallUrlExpiredView: CallUrlExpiredView,
 | |
|     PendingConversationView: PendingConversationView,
 | |
|     StartConversationView: StartConversationView,
 | |
|     FailedConversationView: FailedConversationView,
 | |
|     OutgoingConversationView: OutgoingConversationView,
 | |
|     EndedConversationView: EndedConversationView,
 | |
|     HomeView: HomeView,
 | |
|     UnsupportedBrowserView: UnsupportedBrowserView,
 | |
|     UnsupportedDeviceView: UnsupportedDeviceView,
 | |
|     init: init,
 | |
|     PromoteFirefoxView: PromoteFirefoxView,
 | |
|     WebappRootView: WebappRootView,
 | |
|     FxOSConversationModel: FxOSConversationModel
 | |
|   };
 | |
| })(jQuery, _, window.OT, navigator.mozL10n);
 | 
