/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ var loop = loop || {}; loop.panel = (function(_, mozL10n) { "use strict"; var sharedViews = loop.shared.views; var sharedModels = loop.shared.models; var sharedMixins = loop.shared.mixins; var sharedActions = loop.shared.actions; var sharedUtils = loop.shared.utils; var Button = sharedViews.Button; var ButtonGroup = sharedViews.ButtonGroup; var Checkbox = sharedViews.Checkbox; var ContactsControllerView = loop.contacts.ContactsControllerView; var TabView = React.createClass({ propTypes: { buttonsHidden: React.PropTypes.array, children: React.PropTypes.arrayOf(React.PropTypes.element), mozLoop: React.PropTypes.object, // The selectedTab prop is used by the UI showcase. selectedTab: React.PropTypes.string }, getDefaultProps: function() { return { buttonsHidden: [] }; }, shouldComponentUpdate: function(nextProps, nextState) { var tabChange = this.state.selectedTab !== nextState.selectedTab; if (tabChange) { this.props.mozLoop.notifyUITour("Loop:PanelTabChanged", nextState.selectedTab); } if (!tabChange && nextProps.buttonsHidden) { if (nextProps.buttonsHidden.length !== this.props.buttonsHidden.length) { tabChange = true; } else { for (var i = 0, l = nextProps.buttonsHidden.length; i < l && !tabChange; ++i) { if (this.props.buttonsHidden.indexOf(nextProps.buttonsHidden[i]) === -1) { tabChange = true; } } } } return tabChange; }, getInitialState: function() { // XXX Work around props.selectedTab being undefined initially. // When we don't need to rely on the pref, this can move back to // getDefaultProps (bug 1100258). return { selectedTab: this.props.selectedTab || "rooms" }; }, handleSelectTab: function(event) { var tabName = event.target.dataset.tabName; this.setState({selectedTab: tabName}); }, render: function() { var cx = React.addons.classSet; var tabButtons = []; var tabs = []; React.Children.forEach(this.props.children, function(tab, i) { // Filter out null tabs (eg. rooms when the feature is disabled) if (!tab) { return; } var tabName = tab.props.name; if (this.props.buttonsHidden.indexOf(tabName) > -1) { return; } var isSelected = (this.state.selectedTab === tabName); if (!tab.props.hidden) { var label = mozL10n.get(tabName + "_tab_button"); tabButtons.push(
  • {label}
  • ); } tabs.push(
    {tab.props.children}
    ); }, this); return (
    {tabs}
    ); } }); var Tab = React.createClass({ render: function() { return null; } }); /** * Availability drop down menu subview. */ var AvailabilityDropdown = React.createClass({ mixins: [sharedMixins.DropdownMenuMixin()], getInitialState: function() { return { doNotDisturb: navigator.mozLoop.doNotDisturb }; }, // XXX target event can either be the li, the span or the i tag // this makes it easier to figure out the target by making a // closure with the desired status already passed in. changeAvailability: function(newAvailabilty) { return function(event) { // Note: side effect! switch (newAvailabilty) { case "available": this.setState({doNotDisturb: false}); navigator.mozLoop.doNotDisturb = false; break; case "do-not-disturb": this.setState({doNotDisturb: true}); navigator.mozLoop.doNotDisturb = true; break; } this.hideDropdownMenu(); }.bind(this); }, render: function() { var cx = React.addons.classSet; var availabilityDropdown = cx({ "dropdown-menu": true, "hide": !this.state.showMenu }); var statusIcon = cx({ "status-unavailable": this.state.doNotDisturb, "status-available": !this.state.doNotDisturb }); var availabilityText = this.state.doNotDisturb ? mozL10n.get("display_name_dnd_status") : mozL10n.get("display_name_available_status"); return (

    {availabilityText}

    ); } }); var GettingStartedView = React.createClass({ mixins: [sharedMixins.WindowCloseMixin], propTypes: { mozLoop: React.PropTypes.object.isRequired }, handleButtonClick: function() { navigator.mozLoop.openGettingStartedTour("getting-started"); navigator.mozLoop.setLoopPref("gettingStarted.seen", true); var event = new CustomEvent("GettingStartedSeen"); window.dispatchEvent(event); this.closeWindow(); }, render: function() { if (this.props.mozLoop.getLoopPref("gettingStarted.seen")) { return null; } return (
    {mozL10n.get("first_time_experience_subheading")}
    ); } }); /** * Displays a view requesting the user to sign-in again. */ var SignInRequestView = React.createClass({ mixins: [sharedMixins.WindowCloseMixin], propTypes: { mozLoop: React.PropTypes.object.isRequired }, handleSignInClick: function(event) { event.preventDefault(); this.props.mozLoop.logInToFxA(true); this.closeWindow(); }, handleGuestClick: function(event) { this.props.mozLoop.logOutFromFxA(); }, render: function() { var shortname = mozL10n.get("clientShortname2"); var line1 = mozL10n.get("sign_in_again_title_line_one", { clientShortname2: shortname }); var line2 = mozL10n.get("sign_in_again_title_line_two2", { clientShortname2: shortname }); var useGuestString = mozL10n.get("sign_in_again_use_as_guest_button2", { clientSuperShortname: mozL10n.get("clientSuperShortname") }); return (

    {line1}

    {line2}

    {useGuestString}
    ); } }); var ToSView = React.createClass({ mixins: [sharedMixins.WindowCloseMixin], propTypes: { mozLoop: React.PropTypes.object.isRequired }, handleLinkClick: function(event) { if (!event.target || !event.target.href) { return; } event.preventDefault(); this.props.mozLoop.openURL(event.target.href); this.closeWindow(); }, render: function() { var locale = mozL10n.getLanguage(); var terms_of_use_url = this.props.mozLoop.getLoopPref("legal.ToS_url"); var privacy_notice_url = this.props.mozLoop.getLoopPref("legal.privacy_url"); var tosHTML = mozL10n.get("legal_text_and_links3", { "clientShortname": mozL10n.get("clientShortname2"), "terms_of_use": React.renderToStaticMarkup( {mozL10n.get("legal_text_tos")} ), "privacy_notice": React.renderToStaticMarkup( {mozL10n.get("legal_text_privacy")} ) }); return (

    {mozL10n.get("powered_by_beforeLogo")}

    ); } }); /** * Panel settings (gear) menu entry. */ var SettingsDropdownEntry = React.createClass({ propTypes: { displayed: React.PropTypes.bool, extraCSSClass: React.PropTypes.string, label: React.PropTypes.string.isRequired, onClick: React.PropTypes.func.isRequired }, getDefaultProps: function() { return {displayed: true}; }, render: function() { var cx = React.addons.classSet; if (!this.props.displayed) { return null; } var extraCSSClass = { "dropdown-menu-item": true }; if (this.props.extraCSSClass) { extraCSSClass[this.props.extraCSSClass] = true; } return (
  • {this.props.label}
  • ); } }); /** * Panel settings (gear) menu. */ var SettingsDropdown = React.createClass({ propTypes: { mozLoop: React.PropTypes.object.isRequired }, mixins: [sharedMixins.DropdownMenuMixin(), sharedMixins.WindowCloseMixin], handleClickSettingsEntry: function() { // XXX to be implemented at the same time as unhiding the entry }, handleClickAccountEntry: function() { this.props.mozLoop.openFxASettings(); this.closeWindow(); }, handleClickAuthEntry: function() { if (this._isSignedIn()) { this.props.mozLoop.logOutFromFxA(); } else { this.props.mozLoop.logInToFxA(); } }, handleHelpEntry: function(event) { event.preventDefault(); var helloSupportUrl = this.props.mozLoop.getLoopPref("support_url"); this.props.mozLoop.openURL(helloSupportUrl); this.closeWindow(); }, _isSignedIn: function() { return !!this.props.mozLoop.userProfile; }, openGettingStartedTour: function() { this.props.mozLoop.openGettingStartedTour("settings-menu"); this.closeWindow(); }, render: function() { var cx = React.addons.classSet; var accountEntryCSSClass = this._isSignedIn() ? "entry-settings-signout" : "entry-settings-signin"; return (
    ); } }); /** * FxA sign in/up link component. */ var AccountLink = React.createClass({ mixins: [sharedMixins.WindowCloseMixin], propTypes: { fxAEnabled: React.PropTypes.bool.isRequired, userProfile: userProfileValidator }, handleSignInLinkClick: function() { navigator.mozLoop.logInToFxA(); this.closeWindow(); }, render: function() { if (!this.props.fxAEnabled) { return null; } if (this.props.userProfile && this.props.userProfile.email) { return (
    {loop.shared.utils.truncate(this.props.userProfile.email, 24)}
    ); } return (

    {mozL10n.get("panel_footer_signin_or_signup_link")}

    ); } }); var RoomEntryContextItem = React.createClass({ mixins: [loop.shared.mixins.WindowCloseMixin], propTypes: { mozLoop: React.PropTypes.object.isRequired, roomUrls: React.PropTypes.array }, handleClick: function(event) { event.stopPropagation(); event.preventDefault(); this.props.mozLoop.openURL(event.currentTarget.href); this.closeWindow(); }, render: function() { var roomUrl = this.props.roomUrls && this.props.roomUrls[0]; if (!roomUrl) { return null; } return (
    ); } }); /** * Room list entry. */ var RoomEntry = React.createClass({ propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, mozLoop: React.PropTypes.object.isRequired, room: React.PropTypes.instanceOf(loop.store.Room).isRequired }, mixins: [ loop.shared.mixins.WindowCloseMixin, sharedMixins.DropdownMenuMixin() ], getInitialState: function() { return { eventPosY: 0 }; }, _isActive: function() { return this.props.room.participants.length > 0; }, handleClickEntry: function(event) { event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.OpenRoom({ roomToken: this.props.room.roomToken })); this.closeWindow(); }, handleContextChevronClick: function(e) { e.preventDefault(); e.stopPropagation(); this.setState({ eventPosY: e.pageY }); this.toggleDropdownMenu(); }, /** * Callback called when moving cursor away from the conversation entry. * Will close the dropdown menu. */ _handleMouseOut: function() { if (this.state.showMenu) { this.toggleDropdownMenu(); } }, render: function() { var roomClasses = React.addons.classSet({ "room-entry": true, "room-active": this._isActive() }); return (

    {this.props.room.decryptedContext.roomName}

    ); } }); /** * Buttons corresponding to each conversation entry. * This component renders the video icon call button and chevron button for * displaying contextual dropdown menu for conversation entries. * It also holds the dropdown menu. */ var RoomEntryContextButtons = React.createClass({ propTypes: { dispatcher: React.PropTypes.object.isRequired, eventPosY: React.PropTypes.number.isRequired, handleClickEntry: React.PropTypes.func.isRequired, handleContextChevronClick: React.PropTypes.func.isRequired, room: React.PropTypes.object.isRequired, showMenu: React.PropTypes.bool.isRequired, toggleDropdownMenu: React.PropTypes.func.isRequired }, handleEmailButtonClick: function(event) { event.preventDefault(); event.stopPropagation(); this.props.dispatcher.dispatch( new sharedActions.EmailRoomUrl({ roomUrl: this.props.room.roomUrl, from: "panel" }) ); this.props.toggleDropdownMenu(); }, handleCopyButtonClick: function(event) { event.stopPropagation(); event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.CopyRoomUrl({ roomUrl: this.props.room.roomUrl, from: "panel" })); this.props.toggleDropdownMenu(); }, handleDeleteButtonClick: function(event) { event.stopPropagation(); event.preventDefault(); this.props.dispatcher.dispatch(new sharedActions.DeleteRoom({ roomToken: this.props.room.roomToken })); this.props.toggleDropdownMenu(); }, render: function() { return (
    ); } }); /** * Panel view. */ var PanelView = React.createClass({ propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, initialSelectedTabComponent: React.PropTypes.string, mozLoop: React.PropTypes.object.isRequired, notifications: React.PropTypes.object.isRequired, roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired, selectedTab: React.PropTypes.string, // Used only for unit tests. showTabButtons: React.PropTypes.bool }, getInitialState: function() { return { hasEncryptionKey: this.props.mozLoop.hasEncryptionKey, userProfile: this.props.mozLoop.userProfile, gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen") }; }, _serviceErrorToShow: function() { if (!this.props.mozLoop.errors || !Object.keys(this.props.mozLoop.errors).length) { return null; } // Just get the first error for now since more than one should be rare. var firstErrorKey = Object.keys(this.props.mozLoop.errors)[0]; return { type: firstErrorKey, error: this.props.mozLoop.errors[firstErrorKey] }; }, updateServiceErrors: function() { var serviceError = this._serviceErrorToShow(); if (serviceError) { this.props.notifications.set({ id: "service-error", level: "error", message: serviceError.error.friendlyMessage, details: serviceError.error.friendlyDetails, detailsButtonLabel: serviceError.error.friendlyDetailsButtonLabel, detailsButtonCallback: serviceError.error.friendlyDetailsButtonCallback }); } else { this.props.notifications.remove(this.props.notifications.get("service-error")); } }, _onStatusChanged: function() { var profile = this.props.mozLoop.userProfile; var currUid = this.state.userProfile ? this.state.userProfile.uid : null; var newUid = profile ? profile.uid : null; if (currUid === newUid) { // Update the state of hasEncryptionKey as this might have changed now. this.setState({hasEncryptionKey: this.props.mozLoop.hasEncryptionKey}); } else { // On profile change (login, logout), switch back to the default tab. this.selectTab("rooms"); this.setState({userProfile: profile}); } this.updateServiceErrors(); }, _gettingStartedSeen: function() { this.setState({ gettingStartedSeen: this.props.mozLoop.getLoopPref("gettingStarted.seen") }); }, _UIActionHandler: function(e) { switch (e.detail.action) { case "selectTab": this.selectTab(e.detail.tab); break; default: console.error("Invalid action", e.detail.action); break; } }, selectTab: function(name) { // The tab view might not be created yet (e.g. getting started or fxa // re-sign in. if (this.refs.tabView) { this.refs.tabView.setState({ selectedTab: name }); } }, componentWillMount: function() { this.updateServiceErrors(); }, componentDidMount: function() { window.addEventListener("LoopStatusChanged", this._onStatusChanged); window.addEventListener("GettingStartedSeen", this._gettingStartedSeen); window.addEventListener("UIAction", this._UIActionHandler); }, componentWillUnmount: function() { window.removeEventListener("LoopStatusChanged", this._onStatusChanged); window.removeEventListener("GettingStartedSeen", this._gettingStartedSeen); window.removeEventListener("UIAction", this._UIActionHandler); }, render: function() { var NotificationListView = sharedViews.NotificationListView; if (!this.state.gettingStartedSeen) { return (
    ); } if (!this.state.hasEncryptionKey) { return ; } // Determine which buttons to NOT show. var hideButtons = []; if (!this.state.userProfile && !this.props.showTabButtons) { hideButtons.push("contacts"); } return (
    ); } }); /** * Panel initialisation. */ function init() { // Do the initial L10n setup, we do this before anything // else to ensure the L10n environment is setup correctly. mozL10n.initialize(navigator.mozLoop); var notifications = new sharedModels.NotificationCollection(); var dispatcher = new loop.Dispatcher(); var roomStore = new loop.store.RoomStore(dispatcher, { mozLoop: navigator.mozLoop, notifications: notifications }); React.render(, document.querySelector("#main")); document.documentElement.setAttribute("lang", mozL10n.getLanguage()); document.documentElement.setAttribute("dir", mozL10n.getDirection()); document.body.setAttribute("platform", loop.shared.utils.getPlatform()); // Notify the window that we've finished initalization and initial layout var evtObject = document.createEvent("Event"); evtObject.initEvent("loopPanelInitialized", true, false); window.dispatchEvent(evtObject); } return { AccountLink: AccountLink, AvailabilityDropdown: AvailabilityDropdown, ConversationDropdown: ConversationDropdown, GettingStartedView: GettingStartedView, init: init, NewRoomView: NewRoomView, PanelView: PanelView, RoomEntry: RoomEntry, RoomEntryContextButtons: RoomEntryContextButtons, RoomList: RoomList, SettingsDropdown: SettingsDropdown, SignInRequestView: SignInRequestView, ToSView: ToSView }; })(_, document.mozL10n); document.addEventListener("DOMContentLoaded", loop.panel.init);