/* 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 var SocialUI, SocialFlyout, SocialMarks, SocialShare, SocialSidebar, SocialStatus, SocialActivationListener; (function() { XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm"); XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() { let tmp = {}; Cu.import("resource:///modules/Social.jsm", tmp); return tmp.OpenGraphBuilder; }); XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() { let tmp = {}; Cu.import("resource:///modules/Social.jsm", tmp); return tmp.DynamicResizeWatcher; }); XPCOMUtils.defineLazyGetter(this, "sizeSocialPanelToContent", function() { let tmp = {}; Cu.import("resource:///modules/Social.jsm", tmp); return tmp.sizeSocialPanelToContent; }); XPCOMUtils.defineLazyGetter(this, "CreateSocialStatusWidget", function() { let tmp = {}; Cu.import("resource:///modules/Social.jsm", tmp); return tmp.CreateSocialStatusWidget; }); XPCOMUtils.defineLazyGetter(this, "CreateSocialMarkWidget", function() { let tmp = {}; Cu.import("resource:///modules/Social.jsm", tmp); return tmp.CreateSocialMarkWidget; }); XPCOMUtils.defineLazyGetter(this, "hookWindowCloseForPanelClose", function() { let tmp = {}; Cu.import("resource://gre/modules/MozSocialAPI.jsm", tmp); return tmp.hookWindowCloseForPanelClose; }); SocialUI = { _initialized: false, // Called on delayed startup to initialize the UI init: function SocialUI_init() { if (this._initialized) { return; } let mm = window.getGroupMessageManager("social"); mm.loadFrameScript("chrome://browser/content/content.js", true); mm.loadFrameScript("chrome://browser/content/social-content.js", true); Services.obs.addObserver(this, "social:ambient-notification-changed", false); Services.obs.addObserver(this, "social:profile-changed", false); Services.obs.addObserver(this, "social:frameworker-error", false); Services.obs.addObserver(this, "social:providers-changed", false); Services.obs.addObserver(this, "social:provider-reload", false); Services.obs.addObserver(this, "social:provider-enabled", false); Services.obs.addObserver(this, "social:provider-disabled", false); Services.prefs.addObserver("social.toast-notifications.enabled", this, false); CustomizableUI.addListener(this); SocialActivationListener.init(); // menupopups that list social providers. we only populate them when shown, // and if it has not been done already. document.getElementById("viewSidebarMenu").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); document.getElementById("social-statusarea-popup").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); Social.init().then((update) => { if (update) this._providersChanged(); // handle SessionStore for the sidebar state SocialSidebar.restoreWindowState(); }); this._initialized = true; }, // Called on window unload uninit: function SocialUI_uninit() { if (!this._initialized) { return; } SocialSidebar.saveWindowState(); Services.obs.removeObserver(this, "social:ambient-notification-changed"); Services.obs.removeObserver(this, "social:profile-changed"); Services.obs.removeObserver(this, "social:frameworker-error"); Services.obs.removeObserver(this, "social:providers-changed"); Services.obs.removeObserver(this, "social:provider-reload"); Services.obs.removeObserver(this, "social:provider-enabled"); Services.obs.removeObserver(this, "social:provider-disabled"); Services.prefs.removeObserver("social.toast-notifications.enabled", this); CustomizableUI.removeListener(this); SocialActivationListener.uninit(); document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); this._initialized = false; }, observe: function SocialUI_observe(subject, topic, data) { switch (topic) { case "social:provider-enabled": SocialMarks.populateToolbarPalette(); SocialStatus.populateToolbarPalette(); break; case "social:provider-disabled": SocialMarks.removeProvider(data); SocialStatus.removeProvider(data); SocialSidebar.disableProvider(data); break; case "social:provider-reload": SocialStatus.reloadProvider(data); // if the reloaded provider is our current provider, fall through // to social:providers-changed so the ui will be reset if (!SocialSidebar.provider || SocialSidebar.provider.origin != data) return; // currently only the sidebar and flyout have a selected provider. // sidebar provider has changed (possibly to null), ensure the content // is unloaded and the frames are reset, they will be loaded in // providers-changed below if necessary. SocialSidebar.unloadSidebar(); SocialFlyout.unload(); // fall through to providers-changed to ensure the reloaded provider // is correctly reflected in any UI and the multi-provider menu case "social:providers-changed": this._providersChanged(); break; // Provider-specific notifications case "social:ambient-notification-changed": SocialStatus.updateButton(data); break; case "social:profile-changed": // make sure anything that happens here only affects the provider for // which the profile is changing, and that anything we call actually // needs to change based on profile data. SocialStatus.updateButton(data); break; case "social:frameworker-error": if (this.enabled && SocialSidebar.provider && SocialSidebar.provider.origin == data) { SocialSidebar.loadFrameworkerFailure(); } break; case "nsPref:changed": if (data == "social.toast-notifications.enabled") { SocialSidebar.updateToggleNotifications(); } break; } }, _providersChanged: function() { SocialSidebar.clearProviderMenus(); SocialSidebar.update(); SocialShare.populateProviderMenu(); SocialStatus.populateToolbarPalette(); SocialMarks.populateToolbarPalette(); }, showLearnMore: function() { let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api"; openUILinkIn(url, "tab"); }, closeSocialPanelForLinkTraversal: function (target, linkNode) { // No need to close the panel if this traversal was not retargeted if (target == "" || target == "_self") return; // Check to see whether this link traversal was in a social panel let win = linkNode.ownerDocument.defaultView; let container = win.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell) .chromeEventHandler; let containerParent = container.parentNode; if (containerParent.classList.contains("social-panel") && containerParent instanceof Ci.nsIDOMXULPopupElement) { // allow the link traversal to finish before closing the panel setTimeout(() => { containerParent.hidePopup(); }, 0); } }, get _chromeless() { // Is this a popup window that doesn't want chrome shown? let docElem = document.documentElement; // extrachrome is not restored during session restore, so we need // to check for the toolbar as well. let chromeless = docElem.getAttribute("chromehidden").includes("extrachrome") || docElem.getAttribute('chromehidden').includes("toolbar"); // This property is "fixed" for a window, so avoid doing the check above // multiple times... delete this._chromeless; this._chromeless = chromeless; return chromeless; }, get enabled() { // Returns whether social is enabled *for this window*. if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window)) return false; return Social.providers.length > 0; }, canShareOrMarkPage: function(aURI) { // Bug 898706 we do not enable social in private sessions since frameworker // would be shared between private and non-private windows if (PrivateBrowsingUtils.isWindowPrivate(window)) return false; return (aURI && (aURI.schemeIs('http') || aURI.schemeIs('https'))); }, onCustomizeEnd: function(aWindow) { if (aWindow != window) return; // customization mode gets buttons out of sync with command updating, fix // the disabled state let canShare = this.canShareOrMarkPage(gBrowser.currentURI); let shareButton = SocialShare.shareButton; if (shareButton) { if (canShare) { shareButton.removeAttribute("disabled") } else { shareButton.setAttribute("disabled", "true") } } // update the disabled state of the button based on the command for (let node of SocialMarks.nodes) { if (canShare) { node.removeAttribute("disabled") } else { node.setAttribute("disabled", "true") } } }, // called on tab/urlbar/location changes and after customization. Update // anything that is tab specific. updateState: function() { if (location == "about:customizing") return; goSetCommandEnabled("Social:PageShareOrMark", this.canShareOrMarkPage(gBrowser.currentURI)); if (!SocialUI.enabled) return; // larger update that may change button icons SocialMarks.update(); } } // message manager handlers SocialActivationListener = { init: function() { messageManager.addMessageListener("Social:Activation", this); }, uninit: function() { messageManager.removeMessageListener("Social:Activation", this); }, receiveMessage: function(aMessage) { let data = aMessage.json; let browser = aMessage.target; data.window = window; // if the source if the message is the share panel, we do a one-click // installation. The source of activations is controlled by the // social.directories preference let options; if (browser == SocialShare.iframe && Services.prefs.getBoolPref("social.share.activationPanelEnabled")) { options = { bypassContentCheck: true, bypassInstallPanel: true }; } // If we are in PB mode, we silently do nothing (bug 829404 exists to // do something sensible here...) if (PrivateBrowsingUtils.isWindowPrivate(window)) return; Social.installProvider(data, function(manifest) { Social.activateFromOrigin(manifest.origin, function(provider) { if (provider.sidebarURL) { SocialSidebar.show(provider.origin); } if (provider.shareURL) { // Ensure that the share button is somewhere usable. // SocialShare.shareButton may return null if it is in the menu-panel // and has never been visible, so we check the widget directly. If // there is no area for the widget we move it into the toolbar. let widget = CustomizableUI.getWidget("social-share-button"); // If the panel is already open, we can be sure that the provider can // already be accessed, possibly anchored to another toolbar button. // In that case we don't move the widget. if (!widget.areaType && SocialShare.panel.state != "open") { CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR); // Ensure correct state. SocialUI.onCustomizeEnd(window); } // make this new provider the selected provider. If the panel hasn't // been opened, we need to make the frame first. SocialShare._createFrame(); SocialShare.iframe.setAttribute('src', 'data:text/plain;charset=utf8,'); SocialShare.iframe.setAttribute('origin', provider.origin); // get the right button selected SocialShare.populateProviderMenu(); if (SocialShare.panel.state == "open") { SocialShare.sharePage(provider.origin); } } if (provider.postActivationURL) { // if activated from an open share panel, we load the landing page in // a background tab gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"}); } }); }, options); } } SocialFlyout = { get panel() { return document.getElementById("social-flyout-panel"); }, get iframe() { if (!this.panel.firstChild) this._createFrame(); return this.panel.firstChild; }, dispatchPanelEvent: function(name) { let doc = this.iframe.contentDocument; let evt = doc.createEvent("CustomEvent"); evt.initCustomEvent(name, true, true, {}); doc.documentElement.dispatchEvent(evt); }, _createFrame: function() { let panel = this.panel; if (!SocialUI.enabled || panel.firstChild) return; // create and initialize the panel for this window let iframe = document.createElement("browser"); iframe.setAttribute("type", "content"); iframe.setAttribute("class", "social-panel-frame"); iframe.setAttribute("flex", "1"); iframe.setAttribute("message", "true"); iframe.setAttribute("messagemanagergroup", "social"); iframe.setAttribute("tooltip", "aHTMLTooltip"); iframe.setAttribute("context", "contentAreaContextMenu"); iframe.setAttribute("origin", SocialSidebar.provider.origin); panel.appendChild(iframe); // the xbl bindings for the iframe probably don't exist yet, so we can't // access iframe.messageManager directly - but can get at it with this dance. let mm = iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager; mm.sendAsyncMessage("Social:SetErrorURL", null, { template: "about:socialerror?mode=compactInfo&origin=%{origin}" }); }, unload: function() { let panel = this.panel; panel.hidePopup(); if (!panel.firstChild) return let iframe = panel.firstChild; panel.removeChild(iframe); }, onShown: function(aEvent) { let panel = this.panel; let iframe = this.iframe; this._dynamicResizer = new DynamicResizeWatcher(); iframe.docShellIsActive = true; if (iframe.contentDocument.readyState == "complete") { this._dynamicResizer.start(panel, iframe); this.dispatchPanelEvent("socialFrameShow"); } else { // first time load, wait for load and dispatch after load iframe.addEventListener("load", function panelBrowserOnload(e) { iframe.removeEventListener("load", panelBrowserOnload, true); setTimeout(function() { if (SocialFlyout._dynamicResizer) { // may go null if hidden quickly SocialFlyout._dynamicResizer.start(panel, iframe); SocialFlyout.dispatchPanelEvent("socialFrameShow"); } }, 0); }, true); } }, onHidden: function(aEvent) { this._dynamicResizer.stop(); this._dynamicResizer = null; this.iframe.docShellIsActive = false; this.dispatchPanelEvent("socialFrameHide"); }, load: function(aURL, cb) { if (!SocialSidebar.provider) return; this.panel.hidden = false; let iframe = this.iframe; // same url with only ref difference does not cause a new load, so we // want to go right to the callback let src = iframe.contentDocument && iframe.contentDocument.documentURIObject; if (!src || !src.equalsExceptRef(Services.io.newURI(aURL, null, null))) { iframe.addEventListener("load", function documentLoaded() { iframe.removeEventListener("load", documentLoaded, true); cb(); }, true); iframe.setAttribute("src", aURL); } else { // we still need to set the src to trigger the contents hashchange event // for ref changes iframe.setAttribute("src", aURL); cb(); } }, open: function(aURL, yOffset, aCallback) { // Hide any other social panels that may be open. document.getElementById("social-notification-panel").hidePopup(); if (!SocialUI.enabled) return; let panel = this.panel; let iframe = this.iframe; this.load(aURL, function() { sizeSocialPanelToContent(panel, iframe); let anchor = document.getElementById("social-sidebar-browser"); if (panel.state == "open") { panel.moveToAnchor(anchor, "start_before", 0, yOffset, false); } else { panel.openPopup(anchor, "start_before", 0, yOffset, false, false); } if (aCallback) { try { aCallback(iframe.contentWindow); } catch(e) { Cu.reportError(e); } } }); } } SocialShare = { get _dynamicResizer() { delete this._dynamicResizer; this._dynamicResizer = new DynamicResizeWatcher(); return this._dynamicResizer; }, // Share panel may be attached to the overflow or menu button depending on // customization, we need to manage open state of the anchor. get anchor() { let widget = CustomizableUI.getWidget("social-share-button"); return widget.forWindow(window).anchor; }, // Holds the anchor node in use whilst the panel is open, because it may vary. _currentAnchor: null, get panel() { return document.getElementById("social-share-panel"); }, get iframe() { // panel.firstChild is our toolbar hbox, panel.lastChild is the iframe // container hbox used for an interstitial "loading" graphic return this.panel.lastChild.firstChild; }, uninit: function () { if (this.iframe) { this.iframe.remove(); } }, _createFrame: function() { let panel = this.panel; if (this.iframe) return; this.panel.hidden = false; // create and initialize the panel for this window let iframe = document.createElement("browser"); iframe.setAttribute("type", "content"); iframe.setAttribute("class", "social-share-frame"); iframe.setAttribute("context", "contentAreaContextMenu"); iframe.setAttribute("tooltip", "aHTMLTooltip"); iframe.setAttribute("disableglobalhistory", "true"); iframe.setAttribute("flex", "1"); iframe.setAttribute("message", "true"); iframe.setAttribute("messagemanagergroup", "social"); panel.lastChild.appendChild(iframe); let mm = iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager; mm.sendAsyncMessage("Social:SetErrorURL", null, { template: "about:socialerror?mode=compactInfo&origin=%{origin}&url=%{url}" }); this.populateProviderMenu(); }, getSelectedProvider: function() { let provider; let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin"); if (lastProviderOrigin) { provider = Social._getProviderFromOrigin(lastProviderOrigin); } return provider; }, createTooltip: function(event) { let tt = event.target; let provider = Social._getProviderFromOrigin(tt.triggerNode.getAttribute("origin")); tt.firstChild.setAttribute("value", provider.name); tt.lastChild.setAttribute("value", provider.origin); }, populateProviderMenu: function() { if (!this.iframe) return; let providers = [p for (p of Social.providers) if (p.shareURL)]; let hbox = document.getElementById("social-share-provider-buttons"); // remove everything before the add-share-provider button (which should also // be lastChild if any share providers were added) let addButton = document.getElementById("add-share-provider"); while (hbox.lastChild != addButton) { hbox.removeChild(hbox.lastChild); } let selectedProvider = this.getSelectedProvider(); for (let provider of providers) { let button = document.createElement("toolbarbutton"); button.setAttribute("class", "toolbarbutton-1 share-provider-button"); button.setAttribute("type", "radio"); button.setAttribute("group", "share-providers"); button.setAttribute("image", provider.iconURL); button.setAttribute("tooltip", "share-button-tooltip"); button.setAttribute("origin", provider.origin); button.setAttribute("label", provider.name); button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));"); if (provider == selectedProvider) { this.defaultButton = button; } hbox.appendChild(button); } if (!this.defaultButton) { this.defaultButton = addButton; } this.defaultButton.setAttribute("checked", "true"); }, get shareButton() { // web-panels (bookmark/sidebar) don't include customizableui, so // nsContextMenu fails when accessing shareButton, breaking // browser_bug409481.js. if (!window.CustomizableUI) return null; let widget = CustomizableUI.getWidget("social-share-button"); if (!widget || !widget.areaType) return null; return widget.forWindow(window).node; }, _onclick: function() { Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(0); }, onShowing: function() { (this._currentAnchor || this.anchor).setAttribute("open", "true"); this.iframe.addEventListener("click", this._onclick, true); }, onHidden: function() { (this._currentAnchor || this.anchor).removeAttribute("open"); this._currentAnchor = null; this.iframe.removeEventListener("click", this._onclick, true); this.iframe.setAttribute("src", "data:text/plain;charset=utf8,"); // make sure that the frame is unloaded after it is hidden this.iframe.docShell.createAboutBlankContentViewer(null); this.currentShare = null; // share panel use is over, purge any history if (this.iframe.sessionHistory) { let purge = this.iframe.sessionHistory.count; if (purge > 0) this.iframe.sessionHistory.PurgeHistory(purge); } }, sharePage: function(providerOrigin, graphData, target, anchor) { // if providerOrigin is undefined, we use the last-used provider, or the // current/default provider. The provider selection in the share panel // will call sharePage with an origin for us to switch to. this._createFrame(); let iframe = this.iframe; // graphData is an optional param that either defines the full set of data // to be shared, or partial data about the current page. It is set by a call // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST // define at least url. If it is undefined, we're sharing the current url in // the browser tab. let pageData = graphData ? graphData : this.currentShare; let sharedURI = pageData ? Services.io.newURI(pageData.url, null, null) : gBrowser.currentURI; if (!SocialUI.canShareOrMarkPage(sharedURI)) return; // the point of this action type is that we can use existing share // endpoints (e.g. oexchange) that do not support additional // socialapi functionality. One tweak is that we shoot an event // containing the open graph data. let _dataFn; if (!pageData || sharedURI == gBrowser.currentURI) { messageManager.addMessageListener("PageMetadata:PageDataResult", _dataFn = (msg) => { messageManager.removeMessageListener("PageMetadata:PageDataResult", _dataFn); let pageData = msg.json; if (graphData) { // overwrite data retreived from page with data given to us as a param for (let p in graphData) { pageData[p] = graphData[p]; } } this.sharePage(providerOrigin, pageData, target, anchor); }); gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData"); return; } // if this is a share of a selected item, get any microdata if (!pageData.microdata && target) { messageManager.addMessageListener("PageMetadata:MicrodataResult", _dataFn = (msg) => { messageManager.removeMessageListener("PageMetadata:MicrodataResult", _dataFn); pageData.microdata = msg.data; this.sharePage(providerOrigin, pageData, target, anchor); }); gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetMicrodata", null, { target }); return; } this.currentShare = pageData; let provider; if (providerOrigin) provider = Social._getProviderFromOrigin(providerOrigin); else provider = this.getSelectedProvider(); if (!provider || !provider.shareURL) { this.showDirectory(anchor); return; } // check the menu button let hbox = document.getElementById("social-share-provider-buttons"); let btn = hbox.querySelector("[origin='" + provider.origin + "']"); if (btn) btn.checked = true; let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData); this._dynamicResizer.stop(); let size = provider.getPageSize("share"); if (size) { // let the css on the share panel define width, but height // calculations dont work on all sites, so we allow that to be // defined. delete size.width; } // if we've already loaded this provider/page share endpoint, we don't want // to add another load event listener. let endpointMatch = shareEndpoint == iframe.getAttribute("src"); if (endpointMatch) { this._dynamicResizer.start(iframe.parentNode, iframe, size); iframe.docShellIsActive = true; let evt = iframe.contentDocument.createEvent("CustomEvent"); evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData)); iframe.contentDocument.documentElement.dispatchEvent(evt); } else { iframe.parentNode.setAttribute("loading", "true"); // first time load, wait for load and dispatch after load iframe.addEventListener("load", function panelBrowserOnload(e) { iframe.removeEventListener("load", panelBrowserOnload, true); iframe.docShellIsActive = true; iframe.parentNode.removeAttribute("loading"); // to support standard share endpoints mimick window.open by setting // window.opener, some share endpoints rely on w.opener to know they // should close the window when done. iframe.contentWindow.opener = iframe.contentWindow; SocialShare._dynamicResizer.start(iframe.parentNode, iframe, size); let evt = iframe.contentDocument.createEvent("CustomEvent"); evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData)); iframe.contentDocument.documentElement.dispatchEvent(evt); }, true); } // if the user switched between share providers we do not want that history // available. if (iframe.sessionHistory) { let purge = iframe.sessionHistory.count; if (purge > 0) iframe.sessionHistory.PurgeHistory(purge); } // always ensure that origin belongs to the endpoint let uri = Services.io.newURI(shareEndpoint, null, null); iframe.setAttribute("origin", provider.origin); iframe.setAttribute("src", shareEndpoint); this._openPanel(anchor); }, showDirectory: function(anchor) { this._createFrame(); let iframe = this.iframe; if (iframe.getAttribute("src") == "about:providerdirectory") return; iframe.removeAttribute("origin"); iframe.parentNode.setAttribute("loading", "true"); iframe.addEventListener("DOMContentLoaded", function _dcl(e) { iframe.removeEventListener("DOMContentLoaded", _dcl, true); iframe.parentNode.removeAttribute("loading"); }, true); iframe.addEventListener("load", function panelBrowserOnload(e) { iframe.removeEventListener("load", panelBrowserOnload, true); hookWindowCloseForPanelClose(iframe.contentWindow); SocialShare._dynamicResizer.start(iframe.parentNode, iframe); iframe.addEventListener("unload", function panelBrowserOnload(e) { iframe.removeEventListener("unload", panelBrowserOnload, true); SocialShare._dynamicResizer.stop(); }, true); }, true); iframe.setAttribute("src", "about:providerdirectory"); this._openPanel(anchor); }, _openPanel: function(anchor) { this._currentAnchor = anchor || this.anchor; anchor = document.getAnonymousElementByAttribute(this._currentAnchor, "class", "toolbarbutton-icon"); this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0); } }; SocialSidebar = { _openStartTime: 0, // Whether the sidebar can be shown for this window. get canShow() { if (!SocialUI.enabled || document.mozFullScreen) return false; return Social.providers.some(p => p.sidebarURL); }, // Whether the user has toggled the sidebar on (for windows where it can appear) get opened() { let broadcaster = document.getElementById("socialSidebarBroadcaster"); return !broadcaster.hidden; }, restoreWindowState: function() { // Window state is used to allow different sidebar providers in each window. // We also store the provider used in a pref as the default sidebar to // maintain that state for users who do not restore window state. The // existence of social.sidebar.provider means the sidebar is open with that // provider. this._initialized = true; if (!this.canShow) return; if (Services.prefs.prefHasUserValue("social.provider.current")) { // "upgrade" when the first window opens if we have old prefs. We get the // values from prefs this one time, window state will be saved when this // window is closed. let origin = Services.prefs.getCharPref("social.provider.current"); Services.prefs.clearUserPref("social.provider.current"); // social.sidebar.open default was true, but we only opened if there was // a current provider let opened = origin && true; if (Services.prefs.prefHasUserValue("social.sidebar.open")) { opened = origin && Services.prefs.getBoolPref("social.sidebar.open"); Services.prefs.clearUserPref("social.sidebar.open"); } let data = { "hidden": !opened, "origin": origin }; SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data)); } let data = SessionStore.getWindowValue(window, "socialSidebar"); // if this window doesn't have it's own state, use the state from the opener if (!data && window.opener && !window.opener.closed) { try { data = SessionStore.getWindowValue(window.opener, "socialSidebar"); } catch(e) { // Window is not tracked, which happens on osx if the window is opened // from the hidden window. That happens when you close the last window // without quiting firefox, then open a new window. } } if (data) { data = JSON.parse(data); document.getElementById("social-sidebar-browser").setAttribute("origin", data.origin); if (!data.hidden) this.show(data.origin); } else if (Services.prefs.prefHasUserValue("social.sidebar.provider")) { // no window state, use the global state if it is available this.show(Services.prefs.getCharPref("social.sidebar.provider")); } }, saveWindowState: function() { let broadcaster = document.getElementById("socialSidebarBroadcaster"); let sidebarOrigin = document.getElementById("social-sidebar-browser").getAttribute("origin"); let data = { "hidden": broadcaster.hidden, "origin": sidebarOrigin }; if (broadcaster.hidden) { Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_OPEN_DURATION").add(Date.now() / 1000 - this._openStartTime); } else { this._openStartTime = Date.now() / 1000; } // Save a global state for users who do not restore state. if (broadcaster.hidden) Services.prefs.clearUserPref("social.sidebar.provider"); else Services.prefs.setCharPref("social.sidebar.provider", sidebarOrigin); try { SessionStore.setWindowValue(window, "socialSidebar", JSON.stringify(data)); } catch(e) { // window not tracked during uninit } }, setSidebarVisibilityState: function(aEnabled) { let sbrowser = document.getElementById("social-sidebar-browser"); // it's possible we'll be called twice with aEnabled=false so let's // just assume we may often be called with the same state. if (aEnabled == sbrowser.docShellIsActive) return; sbrowser.docShellIsActive = aEnabled; let evt = sbrowser.contentDocument.createEvent("CustomEvent"); evt.initCustomEvent(aEnabled ? "socialFrameShow" : "socialFrameHide", true, true, {}); sbrowser.contentDocument.documentElement.dispatchEvent(evt); }, updateToggleNotifications: function() { let command = document.getElementById("Social:ToggleNotifications"); command.setAttribute("checked", Services.prefs.getBoolPref("social.toast-notifications.enabled")); command.setAttribute("hidden", !SocialUI.enabled); }, update: function SocialSidebar_update() { // ensure we never update before restoreWindowState if (!this._initialized) return; this.ensureProvider(); this.updateToggleNotifications(); this._updateHeader(); clearTimeout(this._unloadTimeoutId); // Hide the toggle menu item if the sidebar cannot appear let command = document.getElementById("Social:ToggleSidebar"); command.setAttribute("hidden", this.canShow ? "false" : "true"); // Hide the sidebar if it cannot appear, or has been toggled off. // Also set the command "checked" state accordingly. let hideSidebar = !this.canShow || !this.opened; let broadcaster = document.getElementById("socialSidebarBroadcaster"); broadcaster.hidden = hideSidebar; command.setAttribute("checked", !hideSidebar); let sbrowser = document.getElementById("social-sidebar-browser"); if (hideSidebar) { sbrowser.removeEventListener("load", SocialSidebar._loadListener, true); this.setSidebarVisibilityState(false); // If we've been disabled, unload the sidebar content immediately; // if the sidebar was just toggled to invisible, wait a timeout // before unloading. if (!this.canShow) { this.unloadSidebar(); } else { this._unloadTimeoutId = setTimeout( this.unloadSidebar, Services.prefs.getIntPref("social.sidebar.unload_timeout_ms") ); } } else { sbrowser.setAttribute("origin", this.provider.origin); // Make sure the right sidebar URL is loaded if (sbrowser.getAttribute("src") != this.provider.sidebarURL) { // we check readyState right after setting src, we need a new content // viewer to ensure we are checking against the correct document. sbrowser.docShell.createAboutBlankContentViewer(null); sbrowser.setAttribute("src", this.provider.sidebarURL); PopupNotifications.locationChange(sbrowser); } // if the document has not loaded, delay until it is if (sbrowser.contentDocument.readyState != "complete") { document.getElementById("social-sidebar-button").setAttribute("loading", "true"); sbrowser.addEventListener("load", SocialSidebar._loadListener, true); } else { this.setSidebarVisibilityState(true); } } this._updateCheckedMenuItems(this.opened && this.provider ? this.provider.origin : null); }, _onclick: function() { Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(3); }, _loadListener: function SocialSidebar_loadListener() { let sbrowser = document.getElementById("social-sidebar-browser"); sbrowser.removeEventListener("load", SocialSidebar._loadListener, true); document.getElementById("social-sidebar-button").removeAttribute("loading"); SocialSidebar.setSidebarVisibilityState(true); sbrowser.addEventListener("click", SocialSidebar._onclick, true); }, unloadSidebar: function SocialSidebar_unloadSidebar() { let sbrowser = document.getElementById("social-sidebar-browser"); if (!sbrowser.hasAttribute("origin")) return; sbrowser.removeEventListener("click", SocialSidebar._onclick, true); sbrowser.stop(); sbrowser.removeAttribute("origin"); sbrowser.setAttribute("src", "about:blank"); // We need to explicitly create a new content viewer because the old one // doesn't get destroyed until about:blank has loaded (which does not happen // as long as the element is hidden). sbrowser.docShell.createAboutBlankContentViewer(null); SocialFlyout.unload(); }, _unloadTimeoutId: 0, loadFrameworkerFailure: function() { if (this.provider && this.provider.errorState == "frameworker-error") { // we have to explicitly load this error page since it is not being // handled via the normal error page paths. let sbrowser = document.getElementById("social-sidebar-browser"); sbrowser.setAttribute("src", "about:socialerror?mode=workerFailure&origin=" + encodeURIComponent(this.provider.origin)); } }, _provider: null, ensureProvider: function() { if (this._provider) return; // origin for sidebar is persisted, so get the previously selected sidebar // first, otherwise fallback to the first provider in the list let sbrowser = document.getElementById("social-sidebar-browser"); let origin = sbrowser.getAttribute("origin"); let providers = [p for (p of Social.providers) if (p.sidebarURL)]; let provider; if (origin) provider = Social._getProviderFromOrigin(origin); if (!provider && providers.length > 0) provider = providers[0]; if (provider) this.provider = provider; }, get provider() { return this._provider; }, set provider(provider) { if (!provider || provider.sidebarURL) { this._provider = provider; this._updateHeader(); this._updateCheckedMenuItems(provider && provider.origin); this.update(); } }, disableProvider: function(origin) { if (this._provider && this._provider.origin == origin) { this._provider = null; // force a selection of the next provider if there is one this.ensureProvider(); } }, _updateHeader: function() { let provider = this.provider; let image, title; if (provider) { image = "url(" + (provider.icon32URL || provider.iconURL) + ")"; title = provider.name; } document.getElementById("social-sidebar-favico").style.listStyleImage = image; document.getElementById("social-sidebar-title").value = title; }, _updateCheckedMenuItems: function(origin) { // update selected menuitems let menuitems = document.getElementsByClassName("social-provider-menuitem"); for (let mi of menuitems) { if (origin && mi.getAttribute("origin") == origin) { mi.setAttribute("checked", "true"); mi.setAttribute("oncommand", "SocialSidebar.hide();"); } else if (mi.getAttribute("checked")) { mi.removeAttribute("checked"); mi.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));"); } } }, show: function(origin) { // always show the sidebar, and set the provider let broadcaster = document.getElementById("socialSidebarBroadcaster"); broadcaster.hidden = false; if (origin) this.provider = Social._getProviderFromOrigin(origin); else SocialSidebar.update(); this.saveWindowState(); Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_STATE").add(true); }, hide: function() { let broadcaster = document.getElementById("socialSidebarBroadcaster"); broadcaster.hidden = true; this._updateCheckedMenuItems(); this.clearProviderMenus(); SocialSidebar.update(); this.saveWindowState(); Services.telemetry.getHistogramById("SOCIAL_SIDEBAR_STATE").add(false); }, toggleSidebar: function SocialSidebar_toggle() { let broadcaster = document.getElementById("socialSidebarBroadcaster"); if (broadcaster.hidden) this.show(); else this.hide(); }, populateSidebarMenu: function(event) { // Providers are removed from the view->sidebar menu when there is a change // in providers, so we only have to populate onshowing if there are no // provider menus. We populate this menu so long as there are enabled // providers with sidebars. let popup = event.target; let providerMenuSeps = popup.getElementsByClassName("social-provider-menu"); if (providerMenuSeps[0].previousSibling.nodeName == "menuseparator") SocialSidebar.populateProviderMenu(providerMenuSeps[0]); }, clearProviderMenus: function() { // called when there is a change in the provider list we clear all menus, // they will be repopulated when the menu is shown let providerMenuSeps = document.getElementsByClassName("social-provider-menu"); for (let providerMenuSep of providerMenuSeps) { while (providerMenuSep.previousSibling.nodeName == "menuitem") { let menu = providerMenuSep.parentNode; menu.removeChild(providerMenuSep.previousSibling); } } }, populateProviderMenu: function(providerMenuSep) { let menu = providerMenuSep.parentNode; // selectable providers are inserted before the provider-menu seperator, // remove any menuitems in that area while (providerMenuSep.previousSibling.nodeName == "menuitem") { menu.removeChild(providerMenuSep.previousSibling); } // only show a selection in the sidebar header menu if there is more than one let providers = [p for (p of Social.providers) if (p.sidebarURL)]; if (providers.length < 2 && menu.id != "viewSidebarMenu") { providerMenuSep.hidden = true; return; } let topSep = providerMenuSep.previousSibling; for (let provider of providers) { let menuitem = document.createElement("menuitem"); menuitem.className = "menuitem-iconic social-provider-menuitem"; menuitem.setAttribute("image", provider.iconURL); menuitem.setAttribute("label", provider.name); menuitem.setAttribute("origin", provider.origin); if (this.opened && provider == this.provider) { menuitem.setAttribute("checked", "true"); menuitem.setAttribute("oncommand", "SocialSidebar.hide();"); } else { menuitem.setAttribute("oncommand", "SocialSidebar.show(this.getAttribute('origin'));"); } menu.insertBefore(menuitem, providerMenuSep); } topSep.hidden = topSep.nextSibling == providerMenuSep; providerMenuSep.hidden = !providerMenuSep.nextSibling; } } // this helper class is used by removable/customizable buttons to handle // widget creation/destruction // When a provider is installed we show all their UI so the user will see the // functionality of what they installed. The user can later customize the UI, // moving buttons around or off the toolbar. // // On startup, we create the button widgets of any enabled provider. // CustomizableUI handles placement and persistence of placement. function ToolbarHelper(type, createButtonFn, listener) { this._createButton = createButtonFn; this._type = type; if (listener) { CustomizableUI.addListener(listener); // remove this listener on window close window.addEventListener("unload", () => { CustomizableUI.removeListener(listener); }); } } ToolbarHelper.prototype = { idFromOrigin: function(origin) { // this id needs to pass the checks in CustomizableUI, so remove characters // that wont pass. return this._type + "-" + Services.io.newURI(origin, null, null).hostPort.replace(/[\.:]/g,'-'); }, // should be called on disable of a provider removeProviderButton: function(origin) { CustomizableUI.destroyWidget(this.idFromOrigin(origin)); }, clearPalette: function() { [this.removeProviderButton(p.origin) for (p of Social.providers)]; }, // should be called on enable of a provider populatePalette: function() { if (!Social.enabled) { this.clearPalette(); return; } // create any buttons that do not exist yet if they have been persisted // as a part of the UI (otherwise they belong in the palette). for (let provider of Social.providers) { let id = this.idFromOrigin(provider.origin); this._createButton(id, provider); } } } var SocialStatusWidgetListener = { _getNodeOrigin: function(aWidgetId) { // we rely on the button id being the same as the widget. let node = document.getElementById(aWidgetId); if (!node) return null if (!node.classList.contains("social-status-button")) return null return node.getAttribute("origin"); }, onWidgetAdded: function(aWidgetId, aArea, aPosition) { let origin = this._getNodeOrigin(aWidgetId); if (origin) SocialStatus.updateButton(origin); }, onWidgetRemoved: function(aWidgetId, aPrevArea) { let origin = this._getNodeOrigin(aWidgetId); if (!origin) return; // When a widget is demoted to the palette ('removed'), it's visual // style should change. SocialStatus.updateButton(origin); SocialStatus._removeFrame(origin); } } SocialStatus = { populateToolbarPalette: function() { this._toolbarHelper.populatePalette(); for (let provider of Social.providers) this.updateButton(provider.origin); }, removeProvider: function(origin) { this._removeFrame(origin); this._toolbarHelper.removeProviderButton(origin); }, reloadProvider: function(origin) { let button = document.getElementById(this._toolbarHelper.idFromOrigin(origin)); if (button && button.getAttribute("open") == "true") document.getElementById("social-notification-panel").hidePopup(); this._removeFrame(origin); }, _removeFrame: function(origin) { let notificationFrameId = "social-status-" + origin; let frame = document.getElementById(notificationFrameId); if (frame) { frame.parentNode.removeChild(frame); } }, get _toolbarHelper() { delete this._toolbarHelper; this._toolbarHelper = new ToolbarHelper("social-status-button", CreateSocialStatusWidget, SocialStatusWidgetListener); return this._toolbarHelper; }, updateButton: function(origin) { let id = this._toolbarHelper.idFromOrigin(origin); let widget = CustomizableUI.getWidget(id); if (!widget) return; let button = widget.forWindow(window).node; if (button) { // we only grab the first notification, ignore all others let provider = Social._getProviderFromOrigin(origin); let icons = provider.ambientNotificationIcons; let iconNames = Object.keys(icons); let notif = icons[iconNames[0]]; // The image and tooltip need to be updated for both // ambient notification and profile changes. let iconURL = provider.icon32URL || provider.iconURL; let tooltiptext; if (!notif || !widget.areaType) { button.style.listStyleImage = "url(" + iconURL + ")"; button.setAttribute("badge", ""); button.setAttribute("aria-label", ""); button.setAttribute("tooltiptext", provider.name); return; } button.style.listStyleImage = "url(" + (notif.iconURL || iconURL) + ")"; button.setAttribute("tooltiptext", notif.label || provider.name); let badge = notif.counter || ""; button.setAttribute("badge", badge); let ariaLabel = notif.label; // if there is a badge value, we must use a localizable string to insert it. if (badge) ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText", [ariaLabel, badge]); button.setAttribute("aria-label", ariaLabel); } }, _onclose: function(frame) { frame.removeEventListener("close", this._onclose, true); frame.removeEventListener("click", this._onclick, true); }, _onclick: function() { Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(1); }, showPopup: function(aToolbarButton) { // attach our notification panel if necessary let origin = aToolbarButton.getAttribute("origin"); let provider = Social._getProviderFromOrigin(origin); PanelFrame.showPopup(window, aToolbarButton, "social", origin, provider.statusURL, provider.getPageSize("status"), (frame) => { frame.addEventListener("close", () => { SocialStatus._onclose(frame) }, true); frame.addEventListener("click", this._onclick, true); }); Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(1); } }; var SocialMarksWidgetListener = { onWidgetAdded: function(aWidgetId, aArea, aPosition) { let node = document.getElementById(aWidgetId); if (!node || !node.classList.contains("social-mark-button")) return; node._receiveMessage = node.receiveMessage.bind(node); messageManager.addMessageListener("Social:ErrorPageNotify", node._receiveMessage); }, onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, isRemoval) { if (!isRemoval || !aNode || !aNode.classList.contains("social-mark-button")) return; messageManager.removeMessageListener("Social:ErrorPageNotify", aNode._receiveMessage); delete aNode._receiveMessage; } } /** * SocialMarks * * Handles updates to toolbox and signals all buttons to update when necessary. */ SocialMarks = { get nodes() { let providers = [p for (p of Social.providers) if (p.markURL)]; for (let p of providers) { let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin); let widget = CustomizableUI.getWidget(widgetId); if (!widget) continue; let node = widget.forWindow(window).node; if (node) yield node; } }, update: function() { // querySelectorAll does not work on the menu panel, so we have to do this // the hard way. for (let node of this.nodes) { // xbl binding is not complete on startup when buttons are not in toolbar, // verify update is available if (node.update) { node.update(); } } }, getProviders: function() { // only rely on providers that the user has placed in the UI somewhere. This // also means that populateToolbarPalette must be called prior to using this // method, otherwise you get a big fat zero. For our use case with context // menu's, this is ok. let tbh = this._toolbarHelper; return [p for (p of Social.providers) if (p.markURL && document.getElementById(tbh.idFromOrigin(p.origin)))]; }, populateContextMenu: function() { // only show a selection if enabled and there is more than one let providers = this.getProviders(); // remove all previous entries by class let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))]; [m.parentNode.removeChild(m) for (m of menus)]; let contextMenus = [ { type: "link", id: "context-marklinkMenu", label: "social.marklinkMenu.label" }, { type: "page", id: "context-markpageMenu", label: "social.markpageMenu.label" } ]; for (let cfg of contextMenus) { this._populateContextPopup(cfg, providers); } this.update(); }, MENU_LIMIT: 3, // adjustable for testing _populateContextPopup: function(menuInfo, providers) { let menu = document.getElementById(menuInfo.id); let popup = menu.firstChild; for (let provider of providers) { // We show up to MENU_LIMIT providers as single menuitems's at the top // level of the context menu, if we have more than that, dump them *all* // into the menu popup. let mi = document.createElement("menuitem"); mi.setAttribute("oncommand", "gContextMenu.markLink(this.getAttribute('origin'));"); mi.setAttribute("origin", provider.origin); mi.setAttribute("image", provider.iconURL); if (providers.length <= this.MENU_LIMIT) { // an extra class to make enable/disable easy mi.setAttribute("class", "menuitem-iconic context-socialmarks context-mark"+menuInfo.type); let menuLabel = gNavigatorBundle.getFormattedString(menuInfo.label, [provider.name]); mi.setAttribute("label", menuLabel); menu.parentNode.insertBefore(mi, menu); } else { mi.setAttribute("class", "menuitem-iconic context-socialmarks"); mi.setAttribute("label", provider.name); popup.appendChild(mi); } } }, populateToolbarPalette: function() { this._toolbarHelper.populatePalette(); this.populateContextMenu(); }, removeProvider: function(origin) { this._toolbarHelper.removeProviderButton(origin); }, get _toolbarHelper() { delete this._toolbarHelper; this._toolbarHelper = new ToolbarHelper("social-mark-button", CreateSocialMarkWidget, SocialMarksWidgetListener); return this._toolbarHelper; }, markLink: function(aOrigin, aUrl, aTarget) { // find the button for this provider, and open it let id = this._toolbarHelper.idFromOrigin(aOrigin); document.getElementById(id).markLink(aUrl, aTarget); } }; })();