forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			405 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			405 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // 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/.
 | |
| 
 | |
| // the "exported" symbols
 | |
| let LoopUI;
 | |
| 
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
 | |
| XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
 | |
| 
 | |
| 
 | |
| (function() {
 | |
|   LoopUI = {
 | |
|     /**
 | |
|      * @var {XULWidgetSingleWrapper} toolbarButton Getter for the Loop toolbarbutton
 | |
|      *                                             instance for this window.
 | |
|      */
 | |
|     get toolbarButton() {
 | |
|       delete this.toolbarButton;
 | |
|       return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * @var {XULElement} panel Getter for the Loop panel element.
 | |
|      */
 | |
|     get panel() {
 | |
|       delete this.panel;
 | |
|       return this.panel = document.getElementById("loop-notification-panel");
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * @var {XULElement|null} browser Getter for the Loop panel browser element.
 | |
|      *                                Will be NULL if the panel hasn't loaded yet.
 | |
|      */
 | |
|     get browser() {
 | |
|       let browser = document.querySelector("#loop-notification-panel > #loop-panel-iframe");
 | |
|       if (browser) {
 | |
|         delete this.browser;
 | |
|         this.browser = browser;
 | |
|       }
 | |
|       return browser;
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * @var {String|null} selectedTab Getter for the name of the currently selected
 | |
|      *                                tab inside the Loop panel. Will be NULL if
 | |
|      *                                the panel hasn't loaded yet.
 | |
|      */
 | |
|     get selectedTab() {
 | |
|       if (!this.browser) {
 | |
|         return null;
 | |
|       }
 | |
| 
 | |
|       let selectedTab = this.browser.contentDocument.querySelector(".tab-view > .selected");
 | |
|       return selectedTab && selectedTab.getAttribute("data-tab-name");
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * @return {Promise}
 | |
|      */
 | |
|     promiseDocumentVisible(aDocument) {
 | |
|       if (!aDocument.hidden) {
 | |
|         return Promise.resolve();
 | |
|       }
 | |
| 
 | |
|       return new Promise((resolve) => {
 | |
|         aDocument.addEventListener("visibilitychange", function onVisibilityChanged() {
 | |
|           aDocument.removeEventListener("visibilitychange", onVisibilityChanged);
 | |
|           resolve();
 | |
|         });
 | |
|       });
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Toggle between opening or hiding the Loop panel.
 | |
|      *
 | |
|      * @param {DOMEvent} [event] Optional event that triggered the call to this
 | |
|      *                           function.
 | |
|      * @param {String}   [tabId] Optional name of the tab to select after the panel
 | |
|      *                           has opened. Does nothing when the panel is hidden.
 | |
|      * @return {Promise}
 | |
|      */
 | |
|     togglePanel: function(event, tabId = null) {
 | |
|       if (this.panel.state == "open") {
 | |
|         return new Promise(resolve => {
 | |
|           this.panel.hidePopup();
 | |
|           resolve();
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       return this.openCallPanel(event, tabId);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Opens the panel for Loop and sizes it appropriately.
 | |
|      *
 | |
|      * @param {event}  event   The event opening the panel, used to anchor
 | |
|      *                         the panel to the button which triggers it.
 | |
|      * @param {String} [tabId] Identifier of the tab to select when the panel is
 | |
|      *                         opened. Example: 'rooms', 'contacts', etc.
 | |
|      * @return {Promise}
 | |
|      */
 | |
|     openCallPanel: function(event, tabId = null) {
 | |
|       return new Promise((resolve) => {
 | |
|         let callback = iframe => {
 | |
|           // Helper function to show a specific tab view in the panel.
 | |
|           function showTab() {
 | |
|             if (!tabId) {
 | |
|               resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument));
 | |
|               return;
 | |
|             }
 | |
| 
 | |
|             let win = iframe.contentWindow;
 | |
|             let ev = new win.CustomEvent("UIAction", Cu.cloneInto({
 | |
|               detail: {
 | |
|                 action: "selectTab",
 | |
|                 tab: tabId
 | |
|               }
 | |
|             }, win));
 | |
|             win.dispatchEvent(ev);
 | |
|             resolve(LoopUI.promiseDocumentVisible(iframe.contentDocument));
 | |
|           }
 | |
| 
 | |
|           // If the panel has been opened and initialized before, we can skip waiting
 | |
|           // for the content to load - because it's already there.
 | |
|           if (("contentWindow" in iframe) && iframe.contentWindow.document.readyState == "complete") {
 | |
|             showTab();
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           iframe.addEventListener("DOMContentLoaded", function documentDOMLoaded() {
 | |
|             iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true);
 | |
|             injectLoopAPI(iframe.contentWindow);
 | |
|             iframe.contentWindow.addEventListener("loopPanelInitialized", function loopPanelInitialized() {
 | |
|               iframe.contentWindow.removeEventListener("loopPanelInitialized",
 | |
|                                                        loopPanelInitialized);
 | |
|               showTab();
 | |
|             });
 | |
|           }, true);
 | |
|         };
 | |
| 
 | |
|         // Used to clear the temporary "login" state from the button.
 | |
|         Services.obs.notifyObservers(null, "loop-status-changed", null);
 | |
| 
 | |
|         this.shouldResumeTour().then((resume) => {
 | |
|           if (resume) {
 | |
|             // Assume the conversation with the visitor wasn't open since we would
 | |
|             // have resumed the tour as soon as the visitor joined if it was (and
 | |
|             // the pref would have been set to false already.
 | |
|             MozLoopService.resumeTour("waiting");
 | |
|             resolve();
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node,
 | |
|                                "loop", null, "about:looppanel", null, callback);
 | |
|         });
 | |
|       });
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Method to know whether actions to open the panel should instead resume the tour.
 | |
|      *
 | |
|      * We need the panel to be opened via UITour so that it gets @noautohide.
 | |
|      *
 | |
|      * @return {Promise} resolving with a {Boolean} of whether the tour should be resumed instead of
 | |
|      *                   opening the panel.
 | |
|      */
 | |
|     shouldResumeTour: Task.async(function* () {
 | |
|       // Resume the FTU tour if this is the first time a room was joined by
 | |
|       // someone else since the tour.
 | |
|       if (!Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin")) {
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       if (!LoopRooms.participantsCount) {
 | |
|         // Nobody is in the rooms
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       let roomsWithNonOwners = yield this.roomsWithNonOwners();
 | |
|       if (!roomsWithNonOwners.length) {
 | |
|         // We were the only one in a room but we want to know about someone else joining.
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       return true;
 | |
|     }),
 | |
| 
 | |
|     /**
 | |
|      * @return {Promise} resolved with an array of Rooms with participants (excluding owners)
 | |
|      */
 | |
|     roomsWithNonOwners: function() {
 | |
|       return new Promise(resolve => {
 | |
|         LoopRooms.getAll((error, rooms) => {
 | |
|           let roomsWithNonOwners = [];
 | |
|           for (let room of rooms) {
 | |
|             if (!("participants" in room)) {
 | |
|               continue;
 | |
|             }
 | |
|             let numNonOwners = room.participants.filter(participant => !participant.owner).length;
 | |
|             if (!numNonOwners) {
 | |
|               continue;
 | |
|             }
 | |
|             roomsWithNonOwners.push(room);
 | |
|           }
 | |
|           resolve(roomsWithNonOwners);
 | |
|         });
 | |
|       });
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Triggers the initialization of the loop service.  Called by
 | |
|      * delayedStartup.
 | |
|      */
 | |
|     init: function() {
 | |
|       // Add observer notifications before the service is initialized
 | |
|       Services.obs.addObserver(this, "loop-status-changed", false);
 | |
| 
 | |
|       MozLoopService.initialize();
 | |
|       this.updateToolbarState();
 | |
|     },
 | |
| 
 | |
|     uninit: function() {
 | |
|       Services.obs.removeObserver(this, "loop-status-changed");
 | |
|     },
 | |
| 
 | |
|     // Implements nsIObserver
 | |
|     observe: function(subject, topic, data) {
 | |
|       if (topic != "loop-status-changed") {
 | |
|         return;
 | |
|       }
 | |
|       this.updateToolbarState(data);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Updates the toolbar/menu-button state to reflect Loop status.
 | |
|      *
 | |
|      * @param {string} [aReason] Some states are only shown if
 | |
|      *                           a related reason is provided.
 | |
|      *
 | |
|      *                 aReason="login": Used after a login is completed
 | |
|      *                   successfully. This is used so the state can be
 | |
|      *                   temporarily shown until the next state change.
 | |
|      */
 | |
|     updateToolbarState: function(aReason = null) {
 | |
|       if (!this.toolbarButton.node) {
 | |
|         return;
 | |
|       }
 | |
|       let state = "";
 | |
|       if (MozLoopService.errors.size) {
 | |
|         state = "error";
 | |
|       } else if (MozLoopService.screenShareActive) {
 | |
|         state = "action";
 | |
|       } else if (aReason == "login" && MozLoopService.userProfile) {
 | |
|         state = "active";
 | |
|       } else if (MozLoopService.doNotDisturb) {
 | |
|         state = "disabled";
 | |
|       } else if (MozLoopService.roomsParticipantsCount > 0) {
 | |
|         state = "active";
 | |
|       }
 | |
|       this.toolbarButton.node.setAttribute("state", state);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Show a desktop notification when 'do not disturb' isn't enabled.
 | |
|      *
 | |
|      * @param {Object} options Set of options that may tweak the appearance and
 | |
|      *                         behavior of the notification.
 | |
|      *                         Option params:
 | |
|      *                         - {String}   title       Notification title message
 | |
|      *                         - {String}   [message]   Notification body text
 | |
|      *                         - {String}   [icon]      Notification icon
 | |
|      *                         - {String}   [sound]     Sound to play
 | |
|      *                         - {String}   [selectTab] Tab to select when the panel
 | |
|      *                                                  opens
 | |
|      *                         - {Function} [onclick]   Callback to invoke when
 | |
|      *                                                  the notification is clicked.
 | |
|      *                                                  Opens the panel by default.
 | |
|      */
 | |
|     showNotification: function(options) {
 | |
|       if (MozLoopService.doNotDisturb) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (!options.title) {
 | |
|         throw new Error("Missing title, can not display notification");
 | |
|       }
 | |
| 
 | |
|       let notificationOptions = {
 | |
|         body: options.message || ""
 | |
|       };
 | |
|       if (options.icon) {
 | |
|         notificationOptions.icon = options.icon;
 | |
|       }
 | |
|       if (options.sound) {
 | |
|         // This will not do anything, until bug bug 1105222 is resolved.
 | |
|         notificationOptions.mozbehavior = {
 | |
|           soundFile: ""
 | |
|         };
 | |
|         this.playSound(options.sound);
 | |
|       }
 | |
| 
 | |
|       let notification = new window.Notification(options.title, notificationOptions);
 | |
|       notification.addEventListener("click", e => {
 | |
|         if (window.closed) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           window.focus();
 | |
|         } catch (ex) {}
 | |
| 
 | |
|         // We need a setTimeout here, otherwise the panel won't show after the
 | |
|         // window received focus.
 | |
|         window.setTimeout(() => {
 | |
|           if (typeof options.onclick == "function") {
 | |
|             options.onclick();
 | |
|           } else {
 | |
|             // Open the Loop panel as a default action.
 | |
|             this.openCallPanel(null, options.selectTab || null);
 | |
|           }
 | |
|         }, 0);
 | |
|       });
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Play a sound in this window IF there's no sound playing yet.
 | |
|      *
 | |
|      * @param {String} name Name of the sound, like 'ringtone' or 'room-joined'
 | |
|      */
 | |
|     playSound: function(name) {
 | |
|       if (this.ActiveSound || MozLoopService.doNotDisturb) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       this.activeSound = new window.Audio();
 | |
|       this.activeSound.src = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
 | |
|       this.activeSound.load();
 | |
|       this.activeSound.play();
 | |
| 
 | |
|       this.activeSound.addEventListener("ended", () => this.activeSound = undefined, false);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Adds a listener for browser sharing. It will inform the listener straight
 | |
|      * away for the current windowId, and then on every tab change.
 | |
|      *
 | |
|      * Listener parameters:
 | |
|      * - {Object}  err       If there is a error this will be defined, null otherwise.
 | |
|      * - {Integer} windowId  The new windowId for the browser.
 | |
|      *
 | |
|      * @param {Function} listener The listener to receive information on when the
 | |
|      *                            windowId changes.
 | |
|      */
 | |
|     addBrowserSharingListener: function(listener) {
 | |
|       if (!this._tabChangeListeners) {
 | |
|         this._tabChangeListeners = new Set();
 | |
|         gBrowser.addEventListener("select", this);
 | |
|       }
 | |
| 
 | |
|       this._tabChangeListeners.add(listener);
 | |
| 
 | |
|       // Get the first window Id for the listener.
 | |
|       listener(null, gBrowser.selectedTab.linkedBrowser.outerWindowID);
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Removes a listener from browser sharing.
 | |
|      *
 | |
|      * @param {Function} listener The listener to remove from the list.
 | |
|      */
 | |
|     removeBrowserSharingListener: function(listener) {
 | |
|       if (!this._tabChangeListeners) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       if (this._tabChangeListeners.has(listener)) {
 | |
|         this._tabChangeListeners.delete(listener);
 | |
|       }
 | |
| 
 | |
|       if (!this._tabChangeListeners.size) {
 | |
|         gBrowser.removeEventListener("select", this);
 | |
|         delete this._tabChangeListeners;
 | |
|       }
 | |
|     },
 | |
| 
 | |
|     /**
 | |
|      * Handles events from gBrowser.
 | |
|      */
 | |
|     handleEvent: function(event) {
 | |
|       // We only should get "select" events.
 | |
|       if (event.type != "select") {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // We've changed the tab, so get the new window id.
 | |
|       for (let listener of this._tabChangeListeners) {
 | |
|         listener(null, gBrowser.selectedTab.linkedBrowser.outerWindowID);
 | |
|       };
 | |
|     },
 | |
|   };
 | |
| })();
 | 
