forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			721 lines
		
	
	
	
		
			23 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			721 lines
		
	
	
	
		
			23 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 */
 | |
| /* jshint newcap: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 sharedModels = loop.shared.models,
 | |
|       sharedViews = loop.shared.views,
 | |
|       baseServerUrl = loop.config.serverUrl;
 | |
| 
 | |
|   /**
 | |
|    * App router.
 | |
|    * @type {loop.webapp.WebappRouter}
 | |
|    */
 | |
|   var router;
 | |
| 
 | |
|   /**
 | |
|    * 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() {
 | |
|       /* jshint ignore:start */
 | |
|       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})
 | |
|         )
 | |
|       );
 | |
|       /* jshint ignore:end */
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   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("clientShortname")
 | |
|         )
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   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 (
 | |
|         /* jshint ignore:start */
 | |
|         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
 | |
|           )
 | |
|         )
 | |
|         /* jshint ignore:end */
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   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 (
 | |
|         /* jshint ignore:start */
 | |
|         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)
 | |
|         )
 | |
|         /* jshint ignore:end */
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Conversation launcher view. A ConversationModel is associated and attached
 | |
|    * as a `model` property.
 | |
|    */
 | |
|   var StartConversationView = React.createClass({displayName: 'StartConversationView',
 | |
|     /**
 | |
|      * Constructor.
 | |
|      *
 | |
|      * Required options:
 | |
|      * - {loop.shared.models.ConversationModel}    model    Conversation model.
 | |
|      * - {loop.shared.models.NotificationCollection} notifications
 | |
|      *
 | |
|      */
 | |
| 
 | |
|     getInitialProps: function() {
 | |
|       return {showCallOptionsMenu: false};
 | |
|     },
 | |
| 
 | |
|     getInitialState: function() {
 | |
|       return {
 | |
|         urlCreationDateString: '',
 | |
|         disableCallButton: false,
 | |
|         showCallOptionsMenu: this.props.showCallOptionsMenu
 | |
|       };
 | |
|     },
 | |
| 
 | |
|     propTypes: {
 | |
|       model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
 | |
|                                        .isRequired,
 | |
|       // XXX Check more tightly here when we start injecting window.loop.*
 | |
|       notifications: React.PropTypes.object.isRequired,
 | |
|       client: React.PropTypes.object.isRequired
 | |
|     },
 | |
| 
 | |
|     componentDidMount: function() {
 | |
|       // Listen for events & hide dropdown menu if user clicks away
 | |
|       window.addEventListener("click", this.clickHandler);
 | |
|       this.props.model.listenTo(this.props.model, "session:error",
 | |
|                                 this._onSessionError);
 | |
|       this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"),
 | |
|                                            this._setConversationTimestamp);
 | |
|     },
 | |
| 
 | |
|     _onSessionError: function(error) {
 | |
|       console.error(error);
 | |
|       this.props.notifications.errorL10n("unable_retrieve_call_info");
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * 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"
 | |
|      */
 | |
|     _initiateOutgoingCall: function(callType) {
 | |
|       return function() {
 | |
|         this.props.model.set("selectedCallType", callType);
 | |
|         this.setState({disableCallButton: true});
 | |
|         this.props.model.setupOutgoingCall();
 | |
|       }.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});
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     componentWillUnmount: function() {
 | |
|       window.removeEventListener("click", this.clickHandler);
 | |
|       localStorage.setItem("has-seen-tos", "true");
 | |
|     },
 | |
| 
 | |
|     clickHandler: function(e) {
 | |
|       if (!e.target.classList.contains('btn-chevron') &&
 | |
|           this.state.showCallOptionsMenu) {
 | |
|             this._toggleCallOptionsMenu();
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     _toggleCallOptionsMenu: function() {
 | |
|       var state = this.state.showCallOptionsMenu;
 | |
|       this.setState({showCallOptionsMenu: !state});
 | |
|     },
 | |
| 
 | |
|     render: function() {
 | |
|       var tos_link_name = mozL10n.get("terms_of_use_link_text");
 | |
|       var privacy_notice_name = mozL10n.get("privacy_notice_link_text");
 | |
| 
 | |
|       var tosHTML = mozL10n.get("legal_text_and_links", {
 | |
|         "terms_of_use_url": "<a target=_blank href='" +
 | |
|           "https://accounts.firefox.com/legal/terms'>" + tos_link_name + "</a>",
 | |
|         "privacy_notice_url": "<a target=_blank href='" +
 | |
|           "https://www.mozilla.org/privacy/'>" + privacy_notice_name + "</a>"
 | |
|       });
 | |
| 
 | |
|       var dropdownMenuClasses = React.addons.classSet({
 | |
|         "native-dropdown-large-parent": true,
 | |
|         "standalone-dropdown-menu": true,
 | |
|         "visually-hidden": !this.state.showCallOptionsMenu
 | |
|       });
 | |
|       var tosClasses = React.addons.classSet({
 | |
|         "terms-service": true,
 | |
|         hide: (localStorage.getItem("has-seen-tos") === "true")
 | |
|       });
 | |
| 
 | |
|       return (
 | |
|         /* jshint ignore:start */
 | |
|         React.DOM.div({className: "container"}, 
 | |
|           React.DOM.div({className: "container-box"}, 
 | |
| 
 | |
|             ConversationHeader({
 | |
|               urlCreationDateString: this.state.urlCreationDateString}), 
 | |
| 
 | |
|             React.DOM.p({className: "standalone-btn-label"}, 
 | |
|               mozL10n.get("initiate_call_button_label2")
 | |
|             ), 
 | |
| 
 | |
|             React.DOM.div({id: "messages"}), 
 | |
| 
 | |
|             React.DOM.div({className: "btn-group"}, 
 | |
|               React.DOM.div({className: "flex-padding-1"}), 
 | |
|               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._initiateOutgoingCall("audio-video"), 
 | |
|                             disabled: this.state.disableCallButton, 
 | |
|                             title: mozL10n.get("initiate_audio_video_call_tooltip2")}, 
 | |
|                       React.DOM.span({className: "standalone-call-btn-text"}, 
 | |
|                         mozL10n.get("initiate_audio_video_call_button2")
 | |
|                       ), 
 | |
|                       React.DOM.span({className: "standalone-call-btn-video-icon"})
 | |
|                     ), 
 | |
| 
 | |
|                     React.DOM.div({className: "btn-chevron", 
 | |
|                          onClick: this._toggleCallOptionsMenu}
 | |
|                     )
 | |
| 
 | |
|                   ), 
 | |
| 
 | |
|                   React.DOM.ul({className: dropdownMenuClasses}, 
 | |
|                     React.DOM.li(null, 
 | |
|                       /*
 | |
|                        Button required for disabled state.
 | |
|                        */
 | |
|                       React.DOM.button({className: "start-audio-only-call", 
 | |
|                               onClick: this._initiateOutgoingCall("audio"), 
 | |
|                               disabled: this.state.disableCallButton}, 
 | |
|                         mozL10n.get("initiate_audio_call_button2")
 | |
|                       )
 | |
|                     )
 | |
|                   )
 | |
| 
 | |
|                 )
 | |
|               ), 
 | |
|               React.DOM.div({className: "flex-padding-1"})
 | |
|             ), 
 | |
| 
 | |
|             React.DOM.p({className: tosClasses, 
 | |
|                dangerouslySetInnerHTML: {__html: tosHTML}})
 | |
|           ), 
 | |
| 
 | |
|           ConversationFooter(null)
 | |
|         )
 | |
|         /* jshint ignore:end */
 | |
|       );
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Webapp Router.
 | |
|    */
 | |
|   var WebappRouter = loop.shared.router.BaseConversationRouter.extend({
 | |
|     routes: {
 | |
|       "":                    "home",
 | |
|       "unsupportedDevice":   "unsupportedDevice",
 | |
|       "unsupportedBrowser":  "unsupportedBrowser",
 | |
|       "call/expired":        "expired",
 | |
|       "call/pending/:token": "pendingConversation",
 | |
|       "call/ongoing/:token": "loadConversation",
 | |
|       "call/:token":         "initiate"
 | |
|     },
 | |
| 
 | |
|     initialize: function(options) {
 | |
|       this.helper = options.helper;
 | |
|       if (!this.helper) {
 | |
|         throw new Error("WebappRouter requires a helper object");
 | |
|       }
 | |
| 
 | |
|       // Load default view
 | |
|       this.loadReactComponent(HomeView(null));
 | |
|     },
 | |
| 
 | |
|     _onSessionExpired: function() {
 | |
|       this.navigate("/call/expired", {trigger: true});
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Starts the set up of a call, obtaining the required information from the
 | |
|      * server.
 | |
|      */
 | |
|     setupOutgoingCall: function() {
 | |
|       var loopToken = this._conversation.get("loopToken");
 | |
|       if (!loopToken) {
 | |
|         this._notifications.errorL10n("missing_conversation_info");
 | |
|         this.navigate("home", {trigger: true});
 | |
|       } else {
 | |
|         var callType = this._conversation.get("selectedCallType");
 | |
| 
 | |
|         this._conversation.once("call:outgoing", this.startCall, this);
 | |
| 
 | |
|         this._client.requestCallInfo(this._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._onSessionExpired();
 | |
|                 break;
 | |
|               default:
 | |
|                 this._notifications.errorL10n("missing_conversation_info");
 | |
|                 this.navigate("home", {trigger: true});
 | |
|                 break;
 | |
|             }
 | |
|             return;
 | |
|           }
 | |
|           this._conversation.outgoing(sessionData);
 | |
|         }.bind(this));
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Actually starts the call.
 | |
|      */
 | |
|     startCall: function() {
 | |
|       var loopToken = this._conversation.get("loopToken");
 | |
|       if (!loopToken) {
 | |
|         this._notifications.errorL10n("missing_conversation_info");
 | |
|         this.navigate("home", {trigger: true});
 | |
|       } else {
 | |
|         this.navigate("call/pending/" + loopToken, {
 | |
|           trigger: true
 | |
|         });
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Used to set up the web socket connection and navigate to the
 | |
|      * call view if appropriate.
 | |
|      *
 | |
|      * @param {string} loopToken The session token to use.
 | |
|      */
 | |
|     _setupWebSocketAndCallView: function() {
 | |
|       this._websocket = new loop.CallConnectionWebSocket({
 | |
|         url: this._conversation.get("progressURL"),
 | |
|         websocketToken: this._conversation.get("websocketToken"),
 | |
|         callId: this._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._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._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": {
 | |
|           this._handleCallConnecting();
 | |
|           break;
 | |
|         }
 | |
|         case "terminated": {
 | |
|           // At the moment, we show the same text regardless
 | |
|           // of the terminated reason.
 | |
|           this._handleCallTerminated(progressData.reason);
 | |
|           break;
 | |
|         }
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Handles a call moving to the connecting stage.
 | |
|      */
 | |
|     _handleCallConnecting: function() {
 | |
|       var loopToken = this._conversation.get("loopToken");
 | |
|       if (!loopToken) {
 | |
|         this._notifications.errorL10n("missing_conversation_info");
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this.navigate("call/ongoing/" + loopToken, {
 | |
|         trigger: true
 | |
|       });
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Handles call rejection.
 | |
|      *
 | |
|      * @param {String} reason The reason the call was terminated.
 | |
|      */
 | |
|     _handleCallTerminated: function(reason) {
 | |
|       this.endCall();
 | |
|       // For reasons other than cancel, display some notification text.
 | |
|       if (reason !== "cancel") {
 | |
|         // XXX This should really display the call failed view - bug 1046959
 | |
|         // will implement this.
 | |
|         this._notifications.errorL10n("call_timeout_notification_text");
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * @override {loop.shared.router.BaseConversationRouter.endCall}
 | |
|      */
 | |
|     endCall: function() {
 | |
|       var route = "home";
 | |
|       if (this._conversation.get("loopToken")) {
 | |
|         route = "call/" + this._conversation.get("loopToken");
 | |
|       }
 | |
|       this.navigate(route, {trigger: true});
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Default entry point.
 | |
|      */
 | |
|     home: function() {
 | |
|       this.loadReactComponent(HomeView(null));
 | |
|     },
 | |
| 
 | |
|     unsupportedDevice: function() {
 | |
|       this.loadReactComponent(UnsupportedDeviceView(null));
 | |
|     },
 | |
| 
 | |
|     unsupportedBrowser: function() {
 | |
|       this.loadReactComponent(UnsupportedBrowserView(null));
 | |
|     },
 | |
| 
 | |
|     expired: function() {
 | |
|       this.loadReactComponent(CallUrlExpiredView({helper: this.helper}));
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Loads conversation launcher view, setting the received conversation token
 | |
|      * to the current conversation model. If a session is currently established,
 | |
|      * terminates it first.
 | |
|      *
 | |
|      * @param  {String} loopToken Loop conversation token.
 | |
|      */
 | |
|     initiate: function(loopToken) {
 | |
|       // Check if a session is ongoing; if so, terminate it
 | |
|       if (this._conversation.get("ongoing")) {
 | |
|         this._conversation.endSession();
 | |
|       }
 | |
|       this._conversation.set("loopToken", loopToken);
 | |
| 
 | |
|       var startView = StartConversationView({
 | |
|         model: this._conversation,
 | |
|         notifications: this._notifications,
 | |
|         client: this._client
 | |
|       });
 | |
|       this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
 | |
|       this._conversation.once("change:publishedStream", this._checkConnected, this);
 | |
|       this._conversation.once("change:subscribedStream", this._checkConnected, this);
 | |
|       this.loadReactComponent(startView);
 | |
|     },
 | |
| 
 | |
|     pendingConversation: function(loopToken) {
 | |
|       if (!this._conversation.isSessionReady()) {
 | |
|         // User has loaded this url directly, actually setup the call.
 | |
|         return this.navigate("call/" + loopToken, {trigger: true});
 | |
|       }
 | |
|       this._setupWebSocketAndCallView();
 | |
|       this.loadReactComponent(PendingConversationView({
 | |
|         websocket: this._websocket
 | |
|       }));
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Loads conversation establishment view.
 | |
|      *
 | |
|      */
 | |
|     loadConversation: function(loopToken) {
 | |
|       if (!this._conversation.isSessionReady()) {
 | |
|         // User has loaded this url directly, actually setup the call.
 | |
|         return this.navigate("call/" + loopToken, {trigger: true});
 | |
|       }
 | |
|       this.loadReactComponent(sharedViews.ConversationView({
 | |
|         sdk: OT,
 | |
|         model: this._conversation,
 | |
|         video: {enabled: this._conversation.hasVideoStream("outgoing")}
 | |
|       }));
 | |
|     }
 | |
|   });
 | |
| 
 | |
|   /**
 | |
|    * Local helpers.
 | |
|    */
 | |
|   function WebappHelper() {
 | |
|     this._iOSRegex = /^(iPad|iPhone|iPod)/;
 | |
|   }
 | |
| 
 | |
|   WebappHelper.prototype = {
 | |
|     isFirefox: function(platform) {
 | |
|       return platform.indexOf("Firefox") !== -1;
 | |
|     },
 | |
| 
 | |
|     isIOS: function(platform) {
 | |
|       return this._iOSRegex.test(platform);
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   /**
 | |
|    * App initialization.
 | |
|    */
 | |
|   function init() {
 | |
|     var helper = new WebappHelper();
 | |
|     var client = new loop.StandaloneClient({
 | |
|       baseServerUrl: baseServerUrl
 | |
|     });
 | |
|     var router = new WebappRouter({
 | |
|       helper: helper,
 | |
|       notifications: new sharedModels.NotificationCollection(),
 | |
|       client: client,
 | |
|       conversation: new sharedModels.ConversationModel({}, {
 | |
|         sdk: OT
 | |
|       })
 | |
|     });
 | |
| 
 | |
|     Backbone.history.start();
 | |
|     if (helper.isIOS(navigator.platform)) {
 | |
|       router.navigate("unsupportedDevice", {trigger: true});
 | |
|     } else if (!OT.checkSystemRequirements()) {
 | |
|       router.navigate("unsupportedBrowser", {trigger: true});
 | |
|     }
 | |
| 
 | |
|     // 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 {
 | |
|     baseServerUrl: baseServerUrl,
 | |
|     CallUrlExpiredView: CallUrlExpiredView,
 | |
|     PendingConversationView: PendingConversationView,
 | |
|     StartConversationView: StartConversationView,
 | |
|     HomeView: HomeView,
 | |
|     UnsupportedBrowserView: UnsupportedBrowserView,
 | |
|     UnsupportedDeviceView: UnsupportedDeviceView,
 | |
|     init: init,
 | |
|     PromoteFirefoxView: PromoteFirefoxView,
 | |
|     WebappHelper: WebappHelper,
 | |
|     WebappRouter: WebappRouter
 | |
|   };
 | |
| })(jQuery, _, window.OT, navigator.mozL10n);
 | 
