author | Wes Kocher <wkocher@mozilla.com> |
Thu, 04 Aug 2016 13:04:57 -0700 | |
changeset 308232 | c60886d5c003908affbe74e56a7c5b88021b4c6e |
parent 308231 | d912055f8e8ad75d189bd480017fdb5a33ee4d7e |
child 308233 | 975ba208687a97ecb9fd439c1ee52bfa3350e25b |
child 308370 | fff9d624dca283235c916d8769786e04f5622282 |
push id | 31092 |
push user | cbook@mozilla.com |
push date | Fri, 05 Aug 2016 10:16:59 +0000 |
treeherder | autoland@b97dd7dd3cb9 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
bugs | 1289549 |
milestone | 51.0a1 |
backs out | 5ad07719e3bdf424673e7f1c7d0e0dc9b6880cae 27e2621947f26a189743243acfa75ed0d7753409 40d5477b89606ca1c56e99b668a7fe669f5cce89 af2c234795a96b578ba94f3d372d676b2d70e95d 6847acfd9362d6f97f21e2d9235c0db682589f30 fc771254be8ff7a9fb11365787e0e136cd4bf225 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/base/content/browser-context.inc +++ b/browser/base/content/browser-context.inc @@ -88,16 +88,20 @@ <menuitem id="context-sharelink" label="&shareLink.label;" accesskey="&shareLink.accesskey;" oncommand="gContextMenu.shareLink();"/> <menuitem id="context-savelink" label="&saveLinkCmd.label;" accesskey="&saveLinkCmd.accesskey;" oncommand="gContextMenu.saveLink();"/> + <menu id="context-marklinkMenu" label="&social.marklinkMenu.label;" + accesskey="&social.marklinkMenu.accesskey;"> + <menupopup/> + </menu> <menuitem id="context-copyemail" label="©EmailCmd.label;" accesskey="©EmailCmd.accesskey;" oncommand="gContextMenu.copyEmail();"/> <menuitem id="context-copylink" label="©LinkCmd.label;" accesskey="©LinkCmd.accesskey;" oncommand="gContextMenu.copyLink();"/> @@ -282,16 +286,20 @@ <menuseparator id="context-sep-sendpagetodevice" hidden="true"/> <menu id="context-sendpagetodevice" label="&sendPageToDevice.label;" accesskey="&sendPageToDevice.accesskey;" hidden="true"> <menupopup id="context-sendpagetodevice-popup" onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gFxAccounts.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/> </menu> + <menu id="context-markpageMenu" label="&social.markpageMenu.label;" + accesskey="&social.markpageMenu.accesskey;"> + <menupopup/> + </menu> <menuseparator id="context-sep-viewbgimage"/> <menuitem id="context-viewbgimage" label="&viewBGImageCmd.label;" accesskey="&viewBGImageCmd.accesskey;" oncommand="gContextMenu.viewBGImage(event);" onclick="checkForMiddleClick(this, event);"/> <menuitem id="context-undo" label="&undoCmd.label;"
--- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -215,16 +215,19 @@ observes="viewBookmarksSidebar"/> <menuitem id="menu_historySidebar" key="key_gotoHistory" observes="viewHistorySidebar" label="&historyButton.label;"/> <menuitem id="menu_tabsSidebar" observes="viewTabsSidebar" label="&syncedTabs.sidebar.label;"/> + <!-- Service providers with sidebars are inserted between these two menuseperators --> + <menuseparator hidden="true"/> + <menuseparator class="social-provider-menu" hidden="true"/> </menupopup> </menu> <menuseparator/> <menu id="viewFullZoomMenu" label="&fullZoom.label;" accesskey="&fullZoom.accesskey;" onpopupshowing="FullZoom.updateMenu();"> <menupopup> <menuitem id="menu_zoomEnlarge"
--- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -102,28 +102,31 @@ oncommand="OpenBrowserWindow({private: true});" reserved="true"/> #ifdef E10S_TESTING_ONLY <command id="Tools:NonRemoteWindow" oncommand="OpenBrowserWindow({remote: false});"/> #endif <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/> <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/> <command id="Social:SharePage" oncommand="SocialShare.sharePage();"/> + <command id="Social:ToggleSidebar" oncommand="SocialSidebar.toggleSidebar();" hidden="true"/> + <command id="Social:ToggleNotifications" oncommand="Social.toggleNotifications();" hidden="true"/> <command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/> + <command id="Chat:Focus" oncommand="Cu.import('resource:///modules/Chat.jsm', {}).Chat.focus(window);"/> </commandset> <commandset id="placesCommands"> <command id="Browser:ShowAllBookmarks" oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks');"/> <command id="Browser:ShowAllHistory" oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/> </commandset> <broadcasterset id="mainBroadcasterSet"> - <broadcaster id="Social:PageShareable" disabled="true"/> + <broadcaster id="Social:PageShareOrMark" disabled="true"/> <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;" type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul" oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/> <!-- for both places and non-places, the sidebar lives at chrome://browser/content/history/history-panel.xul so there are no problems when switching between versions --> <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;" @@ -173,16 +176,17 @@ <broadcaster id="sync-setup-state"/> <broadcaster id="sync-syncnow-state" hidden="true"/> <broadcaster id="sync-reauth-state" hidden="true"/> <broadcaster id="viewTabsSidebar" autoCheck="false" sidebartitle="&syncedTabs.sidebar.label;" type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/syncedtabs/sidebar.xhtml" oncommand="SidebarUI.toggle('viewTabsSidebar');"/> <broadcaster id="workOfflineMenuitemState"/> + <broadcaster id="socialSidebarBroadcaster" hidden="true"/> <broadcaster id="devtoolsMenuBroadcaster_PageSource" label="&pageSourceCmd.label;" key="key_viewSource" command="View:PageSource"> <observes element="canViewSource" attribute="disabled"/> </broadcaster> </broadcasterset> @@ -307,16 +311,27 @@ #endif <key id="viewBookmarksSidebarKb" key="&bookmarksCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/> #ifdef XP_WIN # Cmd+I is conventially mapped to Info on MacOS X, thus it should not be # overridden for other purposes there. <key id="viewBookmarksSidebarWinKb" key="&bookmarksWinCmd.commandkey;" command="viewBookmarksSidebar" modifiers="accel"/> #endif + <!--<key id="markPage" key="&markPageCmd.commandkey;" command="Social:TogglePageMark" modifiers="accel,shift"/>--> + <key id="focusChatBar" key="&social.chatBar.commandkey;" command="Chat:Focus" +#ifdef XP_MACOSX +# Sadly the devtools uses shift-accel-c on non-mac and alt-accel-c everywhere else +# So we just use the other + modifiers="accel,shift" +#else + modifiers="accel,alt" +#endif + /> + <key id="key_stop" keycode="VK_ESCAPE" command="Browser:Stop"/> #ifdef XP_MACOSX <key id="key_stop_mac" modifiers="accel" key="&stopCmd.macCommandKey;" command="Browser:Stop"/> #endif <key id="key_gotoHistory" key="&historySidebarCmd.commandKey;"
--- a/browser/base/content/browser-social.js +++ b/browser/base/content/browser-social.js @@ -1,79 +1,180 @@ /* 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: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(); + messageManager.addMessageListener("Social:Notification", this); + + // 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: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(); + messageManager.removeMessageListener("Social:Notification", this); + + document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); + document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true); this._initialized = false; }, + receiveMessage: function(aMessage) { + if (aMessage.name == "Social:Notification") { + let provider = Social._getProviderFromOrigin(aMessage.data.origin); + if (provider) { + provider.setAmbientNotification(aMessage.data.detail); + } + } + }, + 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 "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) { @@ -113,40 +214,52 @@ SocialUI = { get enabled() { // Returns whether social is enabled *for this window*. if (this._chromeless) return false; return Social.providers.length > 0; }, - canSharePage: function(aURI) { + canShareOrMarkPage: function(aURI) { 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.canSharePage(gBrowser.currentURI); + 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() { - goSetCommandEnabled("Social:PageShareable", this.canSharePage(gBrowser.currentURI)); + 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); }, @@ -162,16 +275,19 @@ SocialActivationListener = { // social.directories preference let options; if (browser == SocialShare.iframe && Services.prefs.getBoolPref("social.share.activationPanelEnabled")) { options = { bypassContentCheck: true, bypassInstallPanel: true }; } 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. @@ -198,16 +314,148 @@ SocialActivationListener = { // 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("disablehistory", "true"); + iframe.setAttribute("tooltip", "aHTMLTooltip"); + iframe.setAttribute("context", "contentAreaContextMenu"); + iframe.setAttribute("origin", SocialSidebar.provider.origin); + panel.appendChild(iframe); + this.messageManager.sendAsyncMessage("Social:SetErrorURL", + { template: "about:socialerror?mode=compactInfo&origin=%{origin}" }); + }, + + get messageManager() { + // The xbl bindings for the iframe may not exist yet, so we can't + // access iframe.messageManager directly - but can get at it with this dance. + return this.iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager; + }, + + 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); + } else { + // first time load, wait for load and dispatch after load + let mm = this.messageManager; + mm.addMessageListener("DOMContentLoaded", function panelBrowserOnload(e) { + mm.removeMessageListener("DOMContentLoaded", panelBrowserOnload); + setTimeout(function() { + if (SocialFlyout._dynamicResizer) { // may go null if hidden quickly + SocialFlyout._dynamicResizer.start(panel, iframe); + } + }, 0); + }); + } + }, + + onHidden: function(aEvent) { + this._dynamicResizer.stop(); + this._dynamicResizer = null; + this.iframe.docShellIsActive = false; + }, + + 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))) { + let mm = this.messageManager; + mm.addMessageListener("DOMContentLoaded", function documentLoaded(e) { + mm.removeMessageListener("DOMContentLoaded", documentLoaded); + cb(); + }); + 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 @@ -229,17 +477,16 @@ SocialShare = { return this.panel.lastChild.firstChild; }, uninit: function () { if (this.iframe) { let mm = this.messageManager; mm.removeMessageListener("PageVisibility:Show", this); mm.removeMessageListener("PageVisibility:Hide", this); - mm.removeMessageListener("Social:DOMWindowClose", this); this.iframe.removeEventListener("load", this); this.iframe.remove(); } }, _createFrame: function() { let panel = this.panel; if (this.iframe) @@ -257,17 +504,16 @@ SocialShare = { iframe.setAttribute("messagemanagergroup", "social"); panel.lastChild.appendChild(iframe); let mm = this.messageManager; mm.addMessageListener("PageVisibility:Show", this); mm.addMessageListener("PageVisibility:Hide", this); mm.sendAsyncMessage("Social:SetErrorURL", { template: "about:socialerror?mode=compactInfo&origin=%{origin}&url=%{url}" }); iframe.addEventListener("load", this, true); - mm.addMessageListener("Social:DOMWindowClose", this); this.populateProviderMenu(); }, get messageManager() { // The xbl bindings for the iframe may not exist yet, so we can't // access iframe.messageManager directly - but can get at it with this dance. return this.iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager; @@ -277,19 +523,16 @@ SocialShare = { let iframe = this.iframe; switch(aMessage.name) { case "PageVisibility:Show": SocialShare._dynamicResizer.start(iframe.parentNode, iframe); break; case "PageVisibility:Hide": SocialShare._dynamicResizer.stop(); break; - case "Social:DOMWindowClose": - this.panel.hidePopup(); - break; } }, handleEvent: function(event) { switch (event.type) { case "load": { let iframe = this.iframe; iframe.parentNode.removeAttribute("loading"); @@ -398,17 +641,17 @@ SocialShare = { // 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.canSharePage(sharedURI)) + 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) { @@ -500,9 +743,671 @@ SocialShare = { _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, + + get browser() { + return document.getElementById("social-sidebar-browser"); + }, + + // Whether the sidebar can be shown for this window. + get canShow() { + if (!SocialUI.enabled || document.fullscreenElement) + 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); + this.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 = this.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; + }, + + 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 = this.browser; + + if (hideSidebar) { + sbrowser.messageManager.removeMessageListener("DOMContentLoaded", SocialSidebar._loadListener); + 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) { + sbrowser.setAttribute("src", this.provider.sidebarURL); + PopupNotifications.locationChange(sbrowser); + document.getElementById("social-sidebar-button").setAttribute("loading", "true"); + sbrowser.messageManager.addMessageListener("DOMContentLoaded", SocialSidebar._loadListener); + } else { + // if the document has not loaded, delay until it is + if (sbrowser.contentDocument.readyState != "complete") { + document.getElementById("social-sidebar-button").setAttribute("loading", "true"); + sbrowser.messageManager.addMessageListener("DOMContentLoaded", SocialSidebar._loadListener); + } 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 = SocialSidebar.browser; + sbrowser.messageManager.removeMessageListener("DOMContentLoaded", SocialSidebar._loadListener); + document.getElementById("social-sidebar-button").removeAttribute("loading"); + SocialSidebar.setSidebarVisibilityState(true); + sbrowser.addEventListener("click", SocialSidebar._onclick, true); + }, + + unloadSidebar: function SocialSidebar_unloadSidebar() { + let sbrowser = SocialSidebar.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.messageManager.sendAsyncMessage("Social:ClearFrame"); + SocialFlyout.unload(); + }, + + _unloadTimeoutId: 0, + + _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 origin = this.browser.getAttribute("origin"); + let providers = Social.providers.filter(p => 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 = Social.providers.filter(p => 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() { + for (let p of Social.providers) { + this.removeProviderButton(p.origin); + } + }, + + // 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 + // ambient notification 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() { + for (let p of Social.providers.filter(p => p.markURL)) { + 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. + return Social.providers.filter(p => p.markURL && + document.getElementById(this._toolbarHelper.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 = [...document.getElementsByClassName("context-socialmarks")]; + for (let m of menus) { + m.parentNode.removeChild(m); + } + + 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); + } +}; + })();
--- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -14,16 +14,40 @@ #main-window:not([chromehidden~="toolbar"]) { %ifdef XP_MACOSX min-width: 335px; %else min-width: 300px; %endif } +/* These values are chosen to keep the Loop detached chat window from + * getting too small. When it's too small, three bad things happen: + * + * - It looks terrible + * - It's not really usable + * - It's possible for the user to be transmitting video that's cropped by the + * the edge of the window, so that they're not aware of it, which is a + * privacy problem + * + * Note that if the chat window grows more users than Loop who want this + * ability, we'll need to generalize. A partial patch for this is in + * bug 1112264. + */ + +#chat-window { + /* + * In some ideal world, we'd have a simple way to express "block resizing + * along any dimension beyond the point at which an overflow event would + * occur". But none of -moz-{fit,max,min}-content do what we want here. So.. + */ + min-width: 260px; + min-height: 315px; +} + #main-window[customize-entered] { min-width: -moz-fit-content; } searchbar { -moz-binding: url("chrome://browser/content/search/search.xml#searchbar"); } @@ -883,26 +907,101 @@ html|*#gcli-output-frame, .browserStack[responsivemode] { transition-property: min-width, max-width, min-height, max-height; } .browserStack[responsivemode][notransition] { transition: none; } +toolbarbutton[type="socialmark"] { + -moz-binding: url("chrome://browser/content/socialmarks.xml#toolbarbutton-marks"); +} + panelview > .social-panel-frame { width: auto; height: auto; } /* Translation */ notification[value="translation"] { -moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar"); } +/* Social */ +/* Note the chatbox 'width' values are duplicated in socialchat.xml */ +chatbox { + -moz-binding: url("chrome://browser/content/socialchat.xml#chatbox"); + transition: height 150ms ease-out, width 150ms ease-out; + height: 290px; + width: 300px; /* CHAT_WIDTH_OPEN in socialchat.xml */ +} + +chatbox[customSize] { + width: 350px; /* CHAT_WIDTH_OPEN_ALT in socialchat.xml */ +} + +#chat-window[customSize] { + min-width: 350px; +} + +chatbox[customSize="loopChatEnabled"] { + /* 430px as defined per UX */ + height: 430px; +} + +#chat-window[customSize="loopChatEnabled"] { + /* 325px + 30px top bar height. */ + min-height: calc(325px + 30px); +} + +chatbox[customSize="loopChatMessageAppended"] { + /* 430px as defined per UX */ + height: 430px; +} + +chatbox[customSize="loopChatDisabledMessageAppended"] { + /* 388px as defined per UX */ + height: 388px; +} + +#chat-window[customSize="loopChatMessageAppended"] { + /* 445px + 30px top bar height. */ + min-height: calc(400px + 30px); +} + +chatbox[minimized="true"] { + width: 160px; + height: 20px; /* CHAT_WIDTH_MINIMIZED in socialchat.xml */ +} + +chatbar { + -moz-binding: url("chrome://browser/content/socialchat.xml#chatbar"); + height: 0; + max-height: 0; +} + +.chatbar-innerbox { + margin: -285px 0 0; +} + +chatbar[customSize] > .chatbar-innerbox { + /* 450px to make room for the maximum custom-size chatbox; currently 'loopChatMessageAppended'. */ + margin-top: -450px; +} + +/* Apply crisp rendering for favicons at exactly 2dppx resolution */ +@media (resolution: 2dppx) { + #social-sidebar-favico, + .social-status-button, + .chat-status-icon { + image-rendering: -moz-crisp-edges; + } +} + /** See bug 872317 for why the following rule is necessary. */ #downloads-button { -moz-binding: url("chrome://browser/content/downloads/download.xml#download-toolbarbutton"); } /*** Visibility of downloads indicator controls ***/ @@ -924,16 +1023,28 @@ toolbarpaletteitem[place="palette"] > #d #downloads-button:-moz-any([progress], [counter], [paused]) #downloads-indicator-icon, #downloads-button:not(:-moz-any([progress], [counter], [paused])) #downloads-indicator-progress-area { visibility: hidden; } +/* hide chat chrome when chat is fullscreen */ +#chat-window[sizemode="fullscreen"] chatbox > .chat-titlebar { + display: none; +} + +/* hide chatbar and sidebar if browser tab is fullscreen */ +#main-window[inFullscreen][inDOMFullscreen] chatbar, +#main-window[inFullscreen][inDOMFullscreen] #social-sidebar-box, +#main-window[inFullscreen][inDOMFullscreen] #social-sidebar-splitter { + display: none; +} + /* Combobox dropdown renderer */ #ContentSelectDropdown > menupopup { /* The menupopup itself should always be rendered LTR to ensure the scrollbar aligns with * the dropdown arrow on the dropdown widget. If a menuitem is RTL, its style will be set accordingly */ direction: ltr; } /* Indent options in optgroups */
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2289,16 +2289,19 @@ function BrowserViewSourceOfDocument(aAr // that of the original URL, so disable remoteness if necessary for this // URL. let contentProcess = Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT forceNotRemote = gMultiProcessBrowser && !E10SUtils.canLoadURIInProcess(args.URL, contentProcess) } + // In the case of sidebars and chat windows, gBrowser is defined but null, + // because no #content element exists. For these cases, we need to find + // the most recent browser window. // In the case of popups, we need to find a non-popup browser window. if (!tabBrowser || !window.toolbar.visible) { // This returns only non-popup browser windows by default. let browserWindow = RecentWindow.getMostRecentBrowserWindow(); tabBrowser = browserWindow.gBrowser; } // `viewSourceInBrowser` will load the source content from the page
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -266,16 +266,33 @@ <toolbarbutton id="add-share-provider" class="toolbarbutton share-provider-button" type="radio" group="share-providers" tooltiptext="&findShareServices.label;" oncommand="SocialShare.showDirectory()"/> </arrowscrollbox> </hbox> <hbox id="share-container" flex="1"/> </panel> + <panel id="social-notification-panel" + class="social-panel" + type="arrow" + hidden="true" + noautofocus="true"/> + <panel id="social-flyout-panel" + class="social-panel" + onpopupshown="SocialFlyout.onShown()" + onpopuphidden="SocialFlyout.onHidden()" + side="right" + type="arrow" + hidden="true" + flip="slide" + rolluponmousewheel="true" + noautofocus="true" + position="topcenter topright"/> + <menupopup id="toolbar-context-menu" onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator'));"> <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)" accesskey="&customizeMenu.moveToPanel.accesskey;" label="&customizeMenu.moveToPanel.label;" contexttype="toolbaritem" class="customize-context-moveToPanel"/> <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)" @@ -1059,16 +1076,66 @@ <vbox id="appcontent" flex="1"> <notificationbox id="high-priority-global-notificationbox" notificationside="top"/> <tabbrowser id="content" flex="1" contenttooltip="aHTMLTooltip" tabcontainer="tabbrowser-tabs" contentcontextmenu="contentAreaContextMenu" autocompletepopup="PopupAutoComplete" selectmenulist="ContentSelectDropdown"/> + <chatbar id="pinnedchats" layer="true" mousethrough="always" hidden="true"/> + </vbox> + <splitter id="social-sidebar-splitter" + class="chromeclass-extrachrome sidebar-splitter" + observes="socialSidebarBroadcaster"/> + <vbox id="social-sidebar-box" + class="chromeclass-extrachrome" + observes="socialSidebarBroadcaster" + persist="width"> + + <sidebarheader id="social-sidebar-header" class="sidebar-header" align="center"> + <image id="social-sidebar-favico"/> + <label id="social-sidebar-title" class="sidebar-title" persist="value" flex="1" crop="end" control="sidebar"/> + <toolbarbutton id="social-sidebar-button" + class="toolbarbutton-1" + type="menu"> + <menupopup id="social-statusarea-popup" position="after_end"> + <menuitem class="social-toggle-sidebar-menuitem" + type="checkbox" + autocheck="false" + command="Social:ToggleSidebar" + label="&social.toggleSidebar.label;" + accesskey="&social.toggleSidebar.accesskey;"/> + <menuitem class="social-toggle-notifications-menuitem" + type="checkbox" + autocheck="false" + command="Social:ToggleNotifications" + label="&social.toggleNotifications.label;" + accesskey="&social.toggleNotifications.accesskey;"/> + <menuseparator/> + <menuseparator class="social-provider-menu" hidden="true"/> + <menuitem class="social-addons-menuitem" command="Social:Addons" + label="&social.addons.label;"/> + <menuitem label="&social.learnMore.label;" + accesskey="&social.learnMore.accesskey;" + oncommand="SocialUI.showLearnMore();"/> + </menupopup> + </toolbarbutton> + </sidebarheader> + + <browser id="social-sidebar-browser" + type="content" + context="contentAreaContextMenu" + message="true" + messagemanagergroup="social" + disableglobalhistory="true" + tooltip="aHTMLTooltip" + popupnotificationanchor="social-sidebar-favico" + flex="1" + style="min-width: 14em; width: 18em; max-width: 36em;"/> </vbox> <vbox id="browser-border-end" hidden="true" layer="true"/> </hbox> #include ../../components/customizableui/content/customizeMode.inc.xul </deck> <html:div id="fullscreen-warning" class="pointerlockfswarning" hidden="true"> <html:div class="pointerlockfswarning-domain-text">
new file mode 100644 --- /dev/null +++ b/browser/base/content/chatWindow.xul @@ -0,0 +1,170 @@ +#filter substitution +<?xml version="1.0"?> + +# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# 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/. + +<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?> +<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?> +<?xul-overlay href="chrome://browser/content/baseMenuOverlay.xul"?> +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> +<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?> + +#include browser-doctype.inc + +<window id="chat-window" + windowtype="Social:Chat" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&mainWindow.title;" + onload="gChatWindow.onLoad();" + onunload="gChatWindow.onUnload();" + macanimationtype="document" + fullscreenbutton="true" +# width and height are also used in socialchat.xml: chatbar dragend handler + width="400px" + height="420px" + persist="screenX screenY width height sizemode"> + + <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> + <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/> + <script type="application/javascript" src="chrome://browser/content/nsContextMenu.js"/> + +#include global-scripts.inc + +<script type="application/javascript"> + +var gChatWindow = { + // cargo-culted from browser.js for nonBrowserStartup, but we're slightly + // different what what we need to leave enabled + onLoad: function() { + // Disable inappropriate commands / submenus + var disabledItems = ['Browser:SavePage', 'Browser:OpenFile', + 'Browser:SendLink', 'cmd_pageSetup', 'cmd_print', + 'cmd_find', 'cmd_findAgain', 'cmd_findPrevious', + 'cmd_fullZoomReduce', 'cmd_fullZoomEnlarge', 'cmd_fullZoomReset', +#ifdef XP_MACOSX + 'viewToolbarsMenu', 'viewSidebarMenuMenu', + 'viewFullZoomMenu', 'pageStyleMenu', 'charsetMenu', +#else + 'Browser:OpenLocation', 'Tools:Search', +#endif + 'Tools:Sanitize', 'Tools:DevToolbox', + 'key_selectTab1', 'key_selectTab2', 'key_selectTab3', + 'key_selectTab4', 'key_selectTab5', 'key_selectTab6', + 'key_selectTab7', 'key_selectTab8', 'key_selectLastTab', + 'viewHistorySidebar', 'viewBookmarksSidebar', + 'Browser:AddBookmarkAs', 'Browser:BookmarkAllTabs']; + + for (let disabledItem of disabledItems) { + document.getElementById(disabledItem).setAttribute("disabled", "true"); + } + + window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = + new chatBrowserAccess(); + + // initialise the offline listener + BrowserOffline.init(); + }, + + onUnload: function() { + BrowserOffline.uninit(); + } +} + +// define a popupnotifications handler for this window. we don't use +// an iconbox here, and only support the browser frame for chat. +XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () { + let tmp = {}; + Cu.import("resource://gre/modules/PopupNotifications.jsm", tmp); + try { + return new tmp.PopupNotifications(document.getElementById("chatter").content, + document.getElementById("notification-popup"), + null); + } catch (ex) { + console.error(ex); + return null; + } +}); + +XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", + "resource:///modules/RecentWindow.jsm"); + +function chatBrowserAccess() { } + +chatBrowserAccess.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]), + + _openURIInNewTab: function(aURI, aWhere) { + if (aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) + return null; + + let win = RecentWindow.getMostRecentBrowserWindow(); + if (!win) { + // We couldn't find a suitable window, a new one needs to be opened. + return null; + } + + let loadInBackground = + Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground"); + let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank", + {inBackground: loadInBackground}); + let browser = win.gBrowser.getBrowserForTab(tab); + win.focus(); + + return browser; + }, + + openURI: function (aURI, aOpener, aWhere, aContext) { + let browser = this._openURIInNewTab(aURI, aWhere); + return browser ? browser.contentWindow : null; + }, + + openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aContext) { + let browser = this._openURIInNewTab(aURI, aWhere); + return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null; + }, + + isTabContentWindow: function (aWindow) { + return this.contentWindow == aWindow; + }, + + canClose() { + let {BrowserUtils} = Cu.import("resource://gre/modules/BrowserUtils.jsm", {}); + return BrowserUtils.canCloseWindow(window); + }, +}; + +</script> + +#include browser-sets.inc + +#ifdef XP_MACOSX +#include browser-menubar.inc +#endif + + <popupset id="mainPopupSet"> + <tooltip id="aHTMLTooltip" page="true"/> + <menupopup id="contentAreaContextMenu" pagemenu="start" + onpopupshowing="if (event.target != this) + return true; + gContextMenu = new nsContextMenu(this, event.shiftKey); + if (gContextMenu.shouldDisplay) + document.popupNode = this.triggerNode; + return gContextMenu.shouldDisplay;" + onpopuphiding="if (event.target != this) + return; + gContextMenu.hiding(); + gContextMenu = null;"> +#include browser-context.inc + </menupopup> + +#include popup-notifications.inc + + </popupset> + + <commandset id="editMenuCommands"/> + <chatbox id="chatter" flex="1"/> +</window>
--- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -708,16 +708,20 @@ var PageMetadataMessenger = { } } } } PageMetadataMessenger.init(); addEventListener("ActivateSocialFeature", function (aEvent) { let document = content.document; + if (PrivateBrowsingUtils.isContentWindowPrivate(content)) { + Cu.reportError("cannot use social providers in private windows"); + return; + } let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); if (!dwu.isHandlingUserInput) { Cu.reportError("attempt to activate provider without user input from " + document.nodePrincipal.origin); return; } let node = aEvent.target;
--- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -345,16 +345,38 @@ nsContextMenu.prototype = { // BiDi UI this.showItem("context-sep-bidi", !this.onNumeric && top.gBidiUI); this.showItem("context-bidi-text-direction-toggle", this.onTextInput && !this.onNumeric && top.gBidiUI); this.showItem("context-bidi-page-direction-toggle", !this.onTextInput && top.gBidiUI); + // SocialMarks. Marks does not work with text selections, only links. If + // there is more than MENU_LIMIT providers, we show a submenu for them, + // otherwise we have a menuitem per provider (added in SocialMarks class). + let markProviders = SocialMarks.getProviders(); + let enablePageMarks = markProviders.length > 0 && !(this.onLink || this.onImage + || this.onVideo || this.onAudio); + this.showItem("context-markpageMenu", enablePageMarks && markProviders.length > SocialMarks.MENU_LIMIT); + let enablePageMarkItems = enablePageMarks && markProviders.length <= SocialMarks.MENU_LIMIT; + let linkmenus = document.getElementsByClassName("context-markpage"); + for (let m of linkmenus) { + m.hidden = !enablePageMarkItems; + } + + let enableLinkMarks = markProviders.length > 0 && + ((this.onLink && !this.onMailtoLink) || this.onPlainTextLink); + this.showItem("context-marklinkMenu", enableLinkMarks && markProviders.length > SocialMarks.MENU_LIMIT); + let enableLinkMarkItems = enableLinkMarks && markProviders.length <= SocialMarks.MENU_LIMIT; + linkmenus = document.getElementsByClassName("context-marklink"); + for (let m of linkmenus) { + m.hidden = !enableLinkMarkItems; + } + // SocialShare let shareButton = SocialShare.shareButton; let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial; let pageShare = shareEnabled && !(this.isContentSelected || this.onTextInput || this.onLink || this.onImage || this.onVideo || this.onAudio || this.onCanvas); this.showItem("context-sharepage", pageShare); this.showItem("context-shareselect", shareEnabled && this.isContentSelected); @@ -1041,24 +1063,32 @@ nsContextMenu.prototype = { Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); let referrer = gContextMenuContentData.referrer; openUILinkIn(gContextMenuContentData.docLocation, "current", { disallowInheritPrincipal: true, referrerURI: referrer ? makeURI(referrer) : null }); }, reload: function(event) { - BrowserReloadOrDuplicate(event); + if (this.onSocial) { + // full reload of social provider + Social._getProviderFromOrigin(this.browser.getAttribute("origin")).reload(); + } else { + BrowserReloadOrDuplicate(event); + } }, // View Partial Source viewPartialSource: function(aContext) { let inWindow = !Services.prefs.getBoolPref("view_source.tab"); let openSelectionFn = inWindow ? null : function() { let tabBrowser = gBrowser; + // In the case of sidebars and chat windows, gBrowser is defined but null, + // because no #content element exists. For these cases, we need to find + // the most recent browser window. // In the case of popups, we need to find a non-popup browser window. if (!tabBrowser || !window.toolbar.visible) { // This returns only non-popup browser windows by default. let browserWindow = RecentWindow.getMostRecentBrowserWindow(); tabBrowser = browserWindow.gBrowser; } let tab = tabBrowser.loadOneTab("about:blank", { relatedToCurrent: true, @@ -1697,16 +1727,20 @@ nsContextMenu.prototype = { message.data.description) .catch(Components.utils.reportError); }; mm.addMessageListener("ContextMenu:BookmarkFrame:Result", onMessage); mm.sendAsyncMessage("ContextMenu:BookmarkFrame", null, { target: this.target }); }, + markLink: function CM_markLink(origin) { + // send link to social, if it is the page url linkURI will be null + SocialMarks.markLink(origin, this.linkURI ? this.linkURI.spec : null, this.target); + }, shareLink: function CM_shareLink() { SocialShare.sharePage(null, { url: this.linkURI.spec }, this.target); }, shareImage: function CM_shareImage() { SocialShare.sharePage(null, { url: this.imageURL, previews: [ this.mediaURL ] }, this.target); },
--- a/browser/base/content/social-content.js +++ b/browser/base/content/social-content.js @@ -39,16 +39,25 @@ addEventListener("DOMTitleChanged", func if (!gDOMTitleChangedByUs) { sendAsyncMessage("Social:DOMTitleChanged", { title: e.target.title }); } gDOMTitleChangedByUs = false; }); +addEventListener("Social:Notification", function(event) { + let frame = docShell.chromeEventHandler; + let origin = frame.getAttribute("origin"); + sendAsyncMessage("Social:Notification", { + "origin": origin, + "detail": JSON.parse(event.detail) + }); +}); + addMessageListener("Social:OpenGraphData", (message) => { let ev = new content.CustomEvent("OpenGraphData", { detail: JSON.stringify(message.data) }); content.dispatchEvent(ev); }); addMessageListener("Social:ClearFrame", (message) => { docShell.createAboutBlankContentViewer(null); });
new file mode 100644 --- /dev/null +++ b/browser/base/content/socialchat.xml @@ -0,0 +1,913 @@ +<?xml version="1.0"?> + +<bindings id="socialChatBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <binding id="chatbox"> + <content orient="vertical" mousethrough="never"> + <xul:hbox class="chat-titlebar" xbl:inherits="minimized,selected,activity" align="baseline"> + <xul:hbox flex="1" onclick="document.getBindingParent(this).onTitlebarClick(event);"> + <xul:image class="chat-status-icon" xbl:inherits="src=image"/> + <xul:label class="chat-title" flex="1" xbl:inherits="crop=titlecrop,value=label" crop="end"/> + </xul:hbox> + <xul:toolbarbutton anonid="webRTC-shareScreen-icon" + class="notification-anchor-icon chat-toolbarbutton screen-icon" + oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/> + <xul:toolbarbutton anonid="webRTC-sharingScreen-icon" + class="notification-anchor-icon chat-toolbarbutton screen-icon in-use" + oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/> + <xul:toolbarbutton anonid="notification-icon" class="notification-anchor-icon chat-toolbarbutton" + oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/> + <xul:toolbarbutton anonid="minimize" class="chat-minimize-button chat-toolbarbutton" + oncommand="document.getBindingParent(this).toggle();"/> + <xul:toolbarbutton anonid="swap" class="chat-swap-button chat-toolbarbutton" + oncommand="document.getBindingParent(this).swapWindows();"/> + <xul:toolbarbutton anonid="close" class="chat-close-button chat-toolbarbutton" + oncommand="document.getBindingParent(this).close();"/> + </xul:hbox> + <xul:browser anonid="remote-content" class="chat-frame" flex="1" + context="contentAreaContextMenu" + disableglobalhistory="true" + frameType="social" + message="true" + messagemanagergroup="social" + tooltip="aHTMLTooltip" + remote="true" + xbl:inherits="src,origin" + type="content"/> + + <xul:browser anonid="content" class="chat-frame" flex="1" + context="contentAreaContextMenu" + disableglobalhistory="true" + message="true" + messagemanagergroup="social" + tooltip="aHTMLTooltip" + xbl:inherits="src,origin" + type="content"/> + </content> + + <implementation implements="nsIDOMEventListener, nsIMessageListener"> + <constructor><![CDATA[ + const kAnchorMap = new Map([ + ["", "notification-"], + ["webRTC-shareScreen-", ""], + ["webRTC-sharingScreen-", ""] + ]); + const kBrowsers = [ + document.getAnonymousElementByAttribute(this, "anonid", "content"), + document.getAnonymousElementByAttribute(this, "anonid", "remote-content") + ]; + for (let content of kBrowsers) { + for (let [getterPrefix, idPrefix] of kAnchorMap) { + let getter = getterPrefix + "popupnotificationanchor"; + let anonid = (idPrefix || getterPrefix) + "icon"; + content.__defineGetter__(getter, () => { + delete content[getter]; + return content[getter] = document.getAnonymousElementByAttribute( + this, "anonid", anonid); + }); + } + } + + let mm = this.content.messageManager; + // process this._callbacks, then set to null so the chatbox creator + // knows to make new callbacks immediately. + if (this._callbacks) { + for (let callback of this._callbacks) { + callback(this); + } + this._callbacks = null; + } + + mm.addMessageListener("Social:DOMTitleChanged", this); + + mm.sendAsyncMessage("WaitForDOMContentLoaded"); + mm.addMessageListener("DOMContentLoaded", function DOMContentLoaded(event) { + mm.removeMessageListener("DOMContentLoaded", DOMContentLoaded); + this.isActive = !this.minimized; + this._chat.loadButtonSet(this, this.getAttribute("buttonSet")); + this._deferredChatLoaded.resolve(this); + }.bind(this)); + + this.setActiveBrowser(); + ]]></constructor> + + <field name="_deferredChatLoaded" readonly="true"> + Promise.defer(); + </field> + + <property name="promiseChatLoaded"> + <getter> + return this._deferredChatLoaded.promise; + </getter> + </property> + + <property name="content"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", + (this.remote ? "remote-" : "") + "content"); + </getter> + </property> + + <field name="_chat" readonly="true"> + Cu.import("resource:///modules/Chat.jsm", {}).Chat; + </field> + + <property name="minimized"> + <getter> + return this.getAttribute("minimized") == "true"; + </getter> + <setter><![CDATA[ + // Note that this.isActive is set via our transitionend handler so + // the content doesn't see intermediate values. + let parent = this.chatbar; + if (val) { + this.setAttribute("minimized", "true"); + // If this chat is the selected one a new one needs to be selected. + if (parent && parent.selectedChat == this) + parent._selectAnotherChat(); + } else { + this.removeAttribute("minimized"); + // this chat gets selected. + if (parent) + parent.selectedChat = this; + } + ]]></setter> + </property> + + <property name="chatbar"> + <getter> + if (this.parentNode.nodeName == "chatbar") + return this.parentNode; + return null; + </getter> + </property> + + <property name="isActive"> + <getter> + return this.content.docShellIsActive; + </getter> + <setter> + this.content.docShellIsActive = !!val; + + // Bug 1256431 to remove socialFrameShow/Hide from hello, keep this + // until that is complete. + // let the chat frame know if it is being shown or hidden + this.content.messageManager.sendAsyncMessage("Social:CustomEvent", { + name: val ? "socialFrameShow" : "socialFrameHide" + }); + </setter> + </property> + + <field name="_remote">false</field> + <property name="remote" onget="return this._remote;"> + <setter><![CDATA[ + this._remote = !!val; + + this.setActiveBrowser(); + ]]></setter> + </property> + + <method name="setActiveBrowser"> + <body><![CDATA[ + // Make sure we only show one browser element at a time. + let content = document.getAnonymousElementByAttribute(this, "anonid", "content"); + let remoteContent = document.getAnonymousElementByAttribute(this, "anonid", "remote-content"); + remoteContent.setAttribute("hidden", !this.remote); + content.setAttribute("hidden", this.remote); + remoteContent.removeAttribute("src"); + content.removeAttribute("src"); + + if (this.src) { + this.setAttribute("src", this.src); + + // Stop loading of the document - that is set before this method was + // called - in the now hidden browser. + (this.remote ? content : remoteContent).setAttribute("src", "about:blank"); + } + ]]></body> + </method> + + <method name="showNotifications"> + <parameter name="aAnchor"/> + <body><![CDATA[ + PopupNotifications._reshowNotifications(aAnchor, + this.content); + ]]></body> + </method> + + <method name="swapDocShells"> + <parameter name="aTarget"/> + <body><![CDATA[ + aTarget.setAttribute("label", this.content.contentTitle); + + aTarget.remote = this.remote; + aTarget.src = this.src; + let content = aTarget.content; + content.setAttribute("origin", this.content.getAttribute("origin")); + content.popupnotificationanchor.className = this.content.popupnotificationanchor.className; + content.swapDocShells(this.content); + + // When a chat window is attached or detached, the docShell hosting + // the chat document is swapped to the newly created chat window. + // (Be it inside a popup or back inside a chatbox element attached to + // the chatbar.) + // Since a swapDocShells call does not swap the messageManager instances + // attached to a browser, we'll need to add the message listeners to + // the new messageManager. This is not a bug in swapDocShells, merely + // a design decision. + content.messageManager.addMessageListener("Social:DOMTitleChanged", content); + ]]></body> + </method> + + <method name="setDecorationAttributes"> + <parameter name="aTarget"/> + <body><![CDATA[ + if (this.hasAttribute("customSize")) + aTarget.setAttribute("customSize", this.getAttribute("customSize")); + this._chat.loadButtonSet(aTarget, this.getAttribute("buttonSet")); + ]]></body> + </method> + + <method name="onTitlebarClick"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!this.chatbar) + return; + if (aEvent.button == 0) { // left-click: toggle minimized. + this.toggle(); + // if we restored it, we want to focus it. + if (!this.minimized) + this.chatbar.focus(); + } else if (aEvent.button == 1) // middle-click: close chat + this.close(); + ]]></body> + </method> + + <method name="close"> + <body><![CDATA[ + if (this.chatbar) + this.chatbar.remove(this); + else + window.close(); + + if (!this.swappingWindows) + this.dispatchEvent(new CustomEvent("ChatboxClosed")); + ]]></body> + </method> + + <method name="swapWindows"> + <body><![CDATA[ + let deferred = Promise.defer(); + let title = this.getAttribute("label"); + if (this.chatbar) { + this.chatbar.detachChatbox(this, { "centerscreen": "yes" }).then( + chatbox => { + chatbox.content.messageManager.sendAsyncMessage("Social:SetDocumentTitle", { + title: title + }); + deferred.resolve(chatbox); + } + ); + } else { + // attach this chatbox to the topmost browser window + let Chat = Cu.import("resource:///modules/Chat.jsm").Chat; + let win = Chat.findChromeWindowForChats(); + let chatbar = win.document.getElementById("pinnedchats"); + let origin = this.content.getAttribute("origin"); + let cb = chatbar.openChat({ + origin: origin, + title: title, + url: "about:blank" + }); + + cb.promiseChatLoaded.then( + () => { + this.setDecorationAttributes(cb); + + this.swapDocShells(cb); + + chatbar.focus(); + this.swappingWindows = true; + this.close(); + + // chatboxForURL is a map of URL -> chatbox used to avoid opening + // duplicate chat windows. Ensure reattached chat windows aren't + // registered with about:blank as their URL, otherwise reattaching + // more than one chat window isn't possible. + chatbar.chatboxForURL.delete("about:blank"); + chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb)); + + cb.content.messageManager.sendAsyncMessage("Social:CustomEvent", { + name: "socialFrameAttached" + }); + + deferred.resolve(cb); + } + ); + } + return deferred.promise; + ]]></body> + </method> + + <method name="toggle"> + <body><![CDATA[ + this.minimized = !this.minimized; + ]]></body> + </method> + + <method name="setTitle"> + <body><![CDATA[ + try { + this.setAttribute("label", this.content.contentTitle); + } catch (ex) {} + if (this.chatbar) + this.chatbar.updateTitlebar(this); + ]]></body> + </method> + + <method name="receiveMessage"> + <parameter name="aMessage" /> + <body><![CDATA[ + switch (aMessage.name) { + case "Social:DOMTitleChanged": + this.setTitle(); + break; + } + ]]></body> + </method> + </implementation> + + <handlers> + <handler event="focus" phase="capturing"> + if (this.chatbar) + this.chatbar.selectedChat = this; + </handler> + <handler event="DOMTitleChanged"> + this.setTitle(); + </handler> + <handler event="DOMLinkAdded"><![CDATA[ + // Much of this logic is from DOMLinkHandler in browser.js. + // This sets the presence icon for a chat user, we simply use favicon + // style updating. + let link = event.originalTarget; + let rel = link.rel && link.rel.toLowerCase(); + if (!link || !link.ownerDocument || !rel || !link.href) + return; + if (link.rel.indexOf("icon") < 0) + return; + + let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}) + .ContentLinkHandler; + let uri = ContentLinkHandler.getLinkIconURI(link); + if (!uri) + return; + + // We made it this far, use it. + this.setAttribute("image", uri.spec); + if (this.chatbar) + this.chatbar.updateTitlebar(this); + ]]></handler> + <handler event="transitionend"> + if (this.isActive == this.minimized) + this.isActive = !this.minimized; + </handler> + </handlers> + </binding> + + <binding id="chatbar"> + <content> + <xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1"> + <xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/> + <xul:toolbarbutton anonid="nub" class="chatbar-button" type="menu" collapsed="true" mousethrough="never"> + <xul:menupopup anonid="nubMenu" oncommand="document.getBindingParent(this).showChat(event.target.chat)"/> + </xul:toolbarbutton> + <children/> + </xul:hbox> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor> + // to avoid reflows we cache the width of the nub. + this.cachedWidthNub = 0; + this._selectedChat = null; + </constructor> + + <field name="innerbox" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "innerbox"); + </field> + + <field name="menupopup" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "nubMenu"); + </field> + + <field name="nub" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "nub"); + </field> + + <method name="focus"> + <body><![CDATA[ + if (!this.selectedChat) + return; + this.selectedChat.content.messageManager.sendAsyncMessage("Social:EnsureFocus"); + ]]></body> + </method> + + <method name="_isChatFocused"> + <parameter name="aChatbox"/> + <body><![CDATA[ + // If there are no XBL bindings for the chat it can't be focused. + if (!aChatbox.content) + return false; + let fw = Services.focus.focusedWindow; + if (!fw) + return false; + // We want to see if the focused window is in the subtree below our browser... + let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + return containingBrowser == aChatbox.content; + ]]></body> + </method> + + <property name="selectedChat"> + <getter><![CDATA[ + return this._selectedChat; + ]]></getter> + <setter><![CDATA[ + // this is pretty horrible, but we: + // * want to avoid doing touching 'selected' attribute when the + // specified chat is already selected. + // * remove 'activity' attribute on newly selected tab *even if* + // newly selected is already selected. + // * need to handle either current or new being null. + if (this._selectedChat != val) { + if (this._selectedChat) { + this._selectedChat.removeAttribute("selected"); + } + this._selectedChat = val; + if (val) { + this._selectedChat.setAttribute("selected", "true"); + } + } + if (val) { + this._selectedChat.removeAttribute("activity"); + } + ]]></setter> + </property> + + <field name="menuitemMap">new WeakMap()</field> + <field name="chatboxForURL">new Map();</field> + + <property name="hasCollapsedChildren"> + <getter><![CDATA[ + return !!this.querySelector("[collapsed]"); + ]]></getter> + </property> + + <property name="collapsedChildren"> + <getter><![CDATA[ + // A generator yielding all collapsed chatboxes, in the order in + // which they should be restored. + return function*() { + let child = this.lastElementChild; + while (child) { + if (child.collapsed) + yield child; + child = child.previousElementSibling; + } + } + ]]></getter> + </property> + + <property name="visibleChildren"> + <getter><![CDATA[ + // A generator yielding all non-collapsed chatboxes. + return function*() { + let child = this.firstElementChild; + while (child) { + if (!child.collapsed) + yield child; + child = child.nextElementSibling; + } + } + ]]></getter> + </property> + + <property name="collapsibleChildren"> + <getter><![CDATA[ + // A generator yielding all children which are able to be collapsed + // in the order in which they should be collapsed. + // (currently this is all visible ones other than the selected one.) + return function*() { + for (let child of this.visibleChildren()) + if (child != this.selectedChat) + yield child; + } + ]]></getter> + </property> + + <method name="_selectAnotherChat"> + <body><![CDATA[ + // Select a different chat (as the currently selected one is no + // longer suitable as the selection - maybe it is being minimized or + // closed.) We only select non-minimized and non-collapsed chats, + // and if none are found, set the selectedChat to null. + // It's possible in the future we will track most-recently-selected + // chats or similar to find the "best" candidate - for now though + // the choice is somewhat arbitrary. + let moveFocus = this.selectedChat && this._isChatFocused(this.selectedChat); + for (let other of this.children) { + if (other != this.selectedChat && !other.minimized && !other.collapsed) { + this.selectedChat = other; + if (moveFocus) + this.focus(); + return; + } + } + // can't find another - so set no chat as selected. + this.selectedChat = null; + ]]></body> + </method> + + <method name="updateTitlebar"> + <parameter name="aChatbox"/> + <body><![CDATA[ + if (aChatbox.collapsed) { + let menuitem = this.menuitemMap.get(aChatbox); + if (aChatbox.getAttribute("activity")) { + menuitem.setAttribute("activity", true); + this.nub.setAttribute("activity", true); + } + menuitem.setAttribute("label", aChatbox.getAttribute("label")); + menuitem.setAttribute("image", aChatbox.getAttribute("image")); + } + ]]></body> + </method> + + <method name="calcTotalWidthOf"> + <parameter name="aElement"/> + <body><![CDATA[ + let cs = document.defaultView.getComputedStyle(aElement); + let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight); + return aElement.getBoundingClientRect().width + margins; + ]]></body> + </method> + + <method name="getTotalChildWidth"> + <parameter name="aChatbox"/> + <body><![CDATA[ + // These are from the CSS for the chatbox and must be kept in sync. + // We can't use calcTotalWidthOf due to the transitions... + const CHAT_WIDTH_OPEN = 300; + const CHAT_WIDTH_OPEN_ALT = 350; + const CHAT_WIDTH_MINIMIZED = 160; + let openWidth = aChatbox.hasAttribute("customSize") ? + CHAT_WIDTH_OPEN_ALT : CHAT_WIDTH_OPEN; + + return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : openWidth; + ]]></body> + </method> + + <method name="collapseChat"> + <parameter name="aChatbox"/> + <body><![CDATA[ + // we ensure that the cached width for a child of this type is + // up-to-date so we can use it when resizing. + this.getTotalChildWidth(aChatbox); + aChatbox.collapsed = true; + aChatbox.isActive = false; + let menu = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem"); + menu.setAttribute("class", "menuitem-iconic"); + menu.setAttribute("label", aChatbox.content.contentTitle); + menu.setAttribute("image", aChatbox.getAttribute("image")); + menu.chat = aChatbox; + this.menuitemMap.set(aChatbox, menu); + this.menupopup.appendChild(menu); + this.nub.collapsed = false; + ]]></body> + </method> + + <method name="showChat"> + <parameter name="aChatbox"/> + <parameter name="aMode"/> + <body><![CDATA[ + if ((aMode != "minimized") && aChatbox.minimized) + aChatbox.minimized = false; + if (this.selectedChat != aChatbox) + this.selectedChat = aChatbox; + if (!aChatbox.collapsed) + return; // already showing - no more to do. + this._showChat(aChatbox); + // showing a collapsed chat might mean another needs to be collapsed + // to make room... + this.resize(); + ]]></body> + </method> + + <method name="_showChat"> + <parameter name="aChatbox"/> + <body><![CDATA[ + // the actual implementation - doesn't check for overflow, assumes + // collapsed, etc. + let menuitem = this.menuitemMap.get(aChatbox); + this.menuitemMap.delete(aChatbox); + this.menupopup.removeChild(menuitem); + aChatbox.collapsed = false; + aChatbox.isActive = !aChatbox.minimized; + ]]></body> + </method> + + <method name="remove"> + <parameter name="aChatbox"/> + <body><![CDATA[ + this._remove(aChatbox); + // The removal of a chat may mean a collapsed one can spring up, + // or that the popup should be hidden. We also defer the selection + // of another chat until after a resize, as a new candidate may + // become uncollapsed after the resize. + this.resize(); + if (this.selectedChat == aChatbox) { + this._selectAnotherChat(); + } + ]]></body> + </method> + + <method name="_remove"> + <parameter name="aChatbox"/> + <body><![CDATA[ + this.removeChild(aChatbox); + // child might have been collapsed. + let menuitem = this.menuitemMap.get(aChatbox); + if (menuitem) { + this.menuitemMap.delete(aChatbox); + this.menupopup.removeChild(menuitem); + } + this.chatboxForURL.delete(aChatbox.src); + ]]></body> + </method> + + <method name="openChat"> + <parameter name="aOptions"/> + <parameter name="aCallback"/> + <body><![CDATA[ + let {origin, title, url, mode} = aOptions; + let cb = this.chatboxForURL.get(url); + if (cb && (cb = cb.get())) { + // A chatbox is still alive to us when it's parented and still has + // content. + if (cb.parentNode) { + this.showChat(cb, mode); + if (aCallback) { + if (cb._callbacks == null) { + // Chatbox has already been created, so callback now. + aCallback(cb); + } else { + // Chatbox is yet to have bindings created... + cb._callbacks.push(aCallback); + } + } + return cb; + } + this.chatboxForURL.delete(url); + } + cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox"); + cb._callbacks = []; + if (aCallback) { + // _callbacks is a javascript property instead of a <field> as it + // must exist before the (possibly delayed) bindings are created. + cb._callbacks.push(aCallback); + } + + cb.remote = !!aOptions.remote; + // src also a javascript property; the src attribute is set in the ctor. + cb.src = url; + if (mode == "minimized") + cb.setAttribute("minimized", "true"); + cb.setAttribute("origin", origin); + cb.setAttribute("label", title); + this.insertBefore(cb, this.firstChild); + this.selectedChat = cb; + this.chatboxForURL.set(url, Cu.getWeakReference(cb)); + this.resize(); + return cb; + ]]></body> + </method> + + <method name="resize"> + <body><![CDATA[ + // Checks the current size against the collapsed state of children + // and collapses or expands as necessary such that as many as possible + // are shown. + // So 2 basic strategies: + // * Collapse/Expand one at a time until we can't collapse/expand any + // more - but this is one reflow per change. + // * Calculate the dimensions ourself and choose how many to collapse + // or expand based on this, then do them all in one go. This is one + // reflow regardless of how many we change. + // So we go the more complicated but more efficient second option... + let availWidth = this.getBoundingClientRect().width; + let currentWidth = 0; + if (!this.nub.collapsed) { // the nub is visible. + if (!this.cachedWidthNub) + this.cachedWidthNub = this.calcTotalWidthOf(this.nub); + currentWidth += this.cachedWidthNub; + } + for (let child of this.visibleChildren()) { + currentWidth += this.getTotalChildWidth(child); + } + + if (currentWidth > availWidth) { + // we need to collapse some. + let toCollapse = []; + for (let child of this.collapsibleChildren()) { + if (currentWidth <= availWidth) + break; + toCollapse.push(child); + currentWidth -= this.getTotalChildWidth(child); + } + if (toCollapse.length) { + for (let child of toCollapse) + this.collapseChat(child); + } + } else if (currentWidth < availWidth) { + // we *might* be able to expand some - see how many. + // XXX - if this was clever, it could know when removing the nub + // leaves enough space to show all collapsed + let toShow = []; + for (let child of this.collapsedChildren()) { + currentWidth += this.getTotalChildWidth(child); + if (currentWidth > availWidth) + break; + toShow.push(child); + } + for (let child of toShow) + this._showChat(child); + + // If none remain collapsed remove the nub. + if (!this.hasCollapsedChildren) { + this.nub.collapsed = true; + } + } + // else: achievement unlocked - we are pixel-perfect! + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.type == "resize") { + this.resize(); + } + ]]></body> + </method> + + <method name="_getDragTarget"> + <parameter name="event"/> + <body><![CDATA[ + return event.target.localName == "chatbox" ? event.target : null; + ]]></body> + </method> + + <!-- Moves a chatbox to a new window. Returns a promise that is resolved + once the move to the other window is complete. + --> + <method name="detachChatbox"> + <parameter name="aChatbox"/> + <parameter name="aOptions"/> + <body><![CDATA[ + let deferred = Promise.defer(); + let chatbar = this; + let options = ""; + for (let name in aOptions) + options += "," + name + "=" + aOptions[name]; + + let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul", + "_blank", "chrome,all,dialog=no" + options); + + otherWin.addEventListener("load", function _chatLoad(event) { + if (event.target != otherWin.document) + return; + + if (aChatbox.hasAttribute("customSize")) { + otherWin.document.getElementById("chat-window"). + setAttribute("customSize", aChatbox.getAttribute("customSize")); + } + + otherWin.removeEventListener("load", _chatLoad, true); + let otherChatbox = otherWin.document.getElementById("chatter"); + aChatbox.setDecorationAttributes(otherChatbox); + aChatbox.swapDocShells(otherChatbox); + + aChatbox.swappingWindows = true; + aChatbox.close(); + let url = aChatbox.src; + chatbar.chatboxForURL.set(url, Cu.getWeakReference(otherChatbox)); + + // All processing is done, now we can fire the event. + otherChatbox.content.messageManager.sendAsyncMessage("Social:CustomEvent", { + name: "socialFrameDetached" + }); + + Services.obs.addObserver(function onDOMWindowClosed(subject) { + if (subject !== otherWin) + return; + + Services.obs.removeObserver(onDOMWindowClosed, "domwindowclosed"); + chatbar.chatboxForURL.delete(url); + + if (!otherChatbox.swappingWindows) + otherChatbox.dispatchEvent(new CustomEvent("ChatboxClosed")); + }, "domwindowclosed", false); + + deferred.resolve(otherChatbox); + }, true); + return deferred.promise; + ]]></body> + </method> + + </implementation> + + <handlers> + <handler event="popupshown"><![CDATA[ + this.nub.removeAttribute("activity"); + ]]></handler> + <handler event="load"><![CDATA[ + window.addEventListener("resize", this, true); + ]]></handler> + <handler event="unload"><![CDATA[ + window.removeEventListener("resize", this, true); + ]]></handler> + + <handler event="dragstart"><![CDATA[ + // chat window dragging is essentially duplicated from tabbrowser.xml + // to acheive the same visual experience + let chatbox = this._getDragTarget(event); + if (!chatbox) { + return; + } + + let dt = event.dataTransfer; + // we do not set a url in the drag data to prevent moving to tabbrowser + // or otherwise having unexpected drop handlers do something with our + // chatbox + dt.mozSetDataAt("application/x-moz-chatbox", chatbox, 0); + + // Set the cursor to an arrow during tab drags. + dt.mozCursor = "default"; + + // Create a canvas to which we capture the current tab. + // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired + // canvas size (in CSS pixels) to the window's backing resolution in order + // to get a full-resolution drag image for use on HiDPI displays. + let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); + let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom; + let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.mozOpaque = true; + canvas.width = 160 * scale; + canvas.height = 90 * scale; + PageThumbs.captureToCanvas(chatbox, canvas); + dt.setDragImage(canvas, -16 * scale, -16 * scale); + + event.stopPropagation(); + ]]></handler> + + <handler event="dragend"><![CDATA[ + let dt = event.dataTransfer; + let draggedChat = dt.mozGetDataAt("application/x-moz-chatbox", 0); + if (dt.mozUserCancelled || dt.dropEffect != "none") { + return; + } + + let eX = event.screenX; + let eY = event.screenY; + // screen.availLeft et. al. only check the screen that this window is on, + // but we want to look at the screen the tab is being dropped onto. + let sX = {}, sY = {}, sWidth = {}, sHeight = {}; + Cc["@mozilla.org/gfx/screenmanager;1"] + .getService(Ci.nsIScreenManager) + .screenForRect(eX, eY, 1, 1) + .GetAvailRect(sX, sY, sWidth, sHeight); + // default size for the chat window as used in chatWindow.xul, use them + // here to attempt to keep the window fully within the screen when + // opening at the drop point. If the user has resized the window to + // something larger (which gets persisted), at least a good portion of + // the window should still be within the screen. + let winWidth = 400; + let winHeight = 420; + // ensure new window entirely within screen + let left = Math.min(Math.max(eX, sX.value), + sX.value + sWidth.value - winWidth); + let top = Math.min(Math.max(eY, sY.value), + sY.value + sHeight.value - winHeight); + + this.detachChatbox(draggedChat, { screenX: left, screenY: top }); + event.stopPropagation(); + ]]></handler> + </handlers> + </binding> + +</bindings>
new file mode 100644 --- /dev/null +++ b/browser/base/content/socialmarks.xml @@ -0,0 +1,366 @@ +<?xml version="1.0"?> + +<bindings id="socialMarkBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:xbl="http://www.mozilla.org/xbl" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + + <binding id="toolbarbutton-marks" display="xul:button" + extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton"> + <content> + <xul:panel anonid="panel" hidden="true" type="arrow" class="social-panel"/> + <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label"/> + <xul:label class="toolbarbutton-text" crop="right" flex="1" + xbl:inherits="value=label,accesskey,crop,wrap"/> + <xul:label class="toolbarbutton-multiline-text" flex="1" + xbl:inherits="xbl:text=label,accesskey,wrap"/> + </content> + <implementation implements="nsIDOMEventListener, nsIObserver"> + <constructor> + // if we overflow, we have to reset the button. unfortunately we cannot + // use a widget listener because we need to do this *after* the node is + // moved, and the event happens before the node is moved. + this.update(); + </constructor> + <property name="_anchor"> + <getter> + let widgetGroup = CustomizableUI.getWidget(this.getAttribute("id")); + return widgetGroup.forWindow(window).anchor; + </getter> + </property> + <property name="_useDynamicResizer"> + <getter> + let provider = Social._getProviderFromOrigin(this.getAttribute("origin")); + return !provider.getPageSize("marks"); + </getter> + </property> + + <property name="panel"> + <getter> + return document.getAnonymousElementByAttribute(this, "anonid", "panel"); + </getter> + </property> + + <property name="content"> + <getter><![CDATA[ + if (this._frame) + return this._frame; + let provider = Social._getProviderFromOrigin(this.getAttribute("origin")); + let size = provider.getPageSize("marks"); + let {width, height} = size ? size : {width: 330, height: 100}; + + let iframe = this._frame = document.createElement("iframe"); + 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", provider.origin); + iframe.setAttribute("style", "width: " + width + "px; height: " + height + "px;"); + this.panel.appendChild(iframe); + + this._frame.addEventListener("DOMLinkAdded", this); + return this._frame; + ]]></getter> + </property> + + <property name="messageManager"> + <getter> + return this.content.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager; + </getter> + </property> + + <property name="contentWindow"> + <getter> + return this.content.contentWindow; + </getter> + </property> + + <property name="contentDocument"> + <getter> + return this.content.contentDocument; + </getter> + </property> + + <property name="provider"> + <getter> + return Social._getProviderFromOrigin(this.getAttribute("origin")); + </getter> + </property> + + <property name="isMarked"> + <setter><![CDATA[ + this._isMarked = val; + let provider = this.provider; + // we cannot size the image when we apply it via listStyleImage, so + // use the toolbar image + let widgetGroup = CustomizableUI.getWidget(this.getAttribute("id")); + val = val && !!widgetGroup.areaType; + let icon = val ? provider.markedIcon : provider.unmarkedIcon; + let iconURL = icon || provider.icon32URL || provider.iconURL; + this.setAttribute("image", iconURL); + ]]></setter> + <getter> + return this._isMarked; + </getter> + </property> + + <method name="update"> + <body><![CDATA[ + // update the button for use with the current tab + let provider = this.provider; + if (this._dynamicResizer) { + this._dynamicResizer.stop(); + this._dynamicResizer = null; + } + this.content.setAttribute("src", "about:blank"); + // called during onhidden, make sure the docshell is updated + if (this._frame.docShell) + this._frame.docShell.createAboutBlankContentViewer(null); + + // disabled attr is set by Social:PageShareOrMark command + if (this.hasAttribute("disabled")) { + this.isMarked = false; + } else { + Social.isURIMarked(provider.origin, gBrowser.currentURI, (isMarked) => { + this.isMarked = isMarked; + }); + } + + this.content.setAttribute("origin", provider.origin); + + let panel = this.panel; + // if customization is currently happening, we may not have a panel + // that we can hide + if (panel.hidePopup) { + panel.hidePopup(); + } + this.pageData = null; + ]]></body> + </method> + + <method name="receiveMessage"> + <parameter name="message"/> + <body><![CDATA[ + if (message.name != "Social:ErrorPageNotify" || message.target != this.content) + return; + this.openPanel(); + ]]></body> + </method> + + <method name="loadPanel"> + <parameter name="pageData"/> + <parameter name="target"/> + <body><![CDATA[ + let provider = this.provider; + let panel = this.panel; + panel.hidden = false; + + // reparent the iframe if we've been customized to a new location + if (this.content.parentNode != panel) + panel.appendChild(this.content); + + let URLTemplate = provider.markURL; + let _dataFn; + if (!pageData) { + messageManager.addMessageListener("PageMetadata:PageDataResult", _dataFn = (msg) => { + messageManager.removeMessageListener("PageMetadata:PageDataResult", _dataFn); + this.loadPanel(msg.json, target); + }); + gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetPageData", null, { target }); + return; + } + // if this is a share of a selected item, get any microformats + if (!pageData.microformats && target) { + messageManager.addMessageListener("PageMetadata:MicroformatsResult", _dataFn = (msg) => { + messageManager.removeMessageListener("PageMetadata:MicroformatsResult", _dataFn); + pageData.microformats = msg.data; + this.loadPanel(pageData, target); + }); + gBrowser.selectedBrowser.messageManager.sendAsyncMessage("PageMetadata:GetMicroformats", null, { target }); + return; + } + this.pageData = pageData; + + let endpoint = OpenGraphBuilder.generateEndpointURL(URLTemplate, this.pageData); + // setup listeners + let DOMContentLoaded = (event) => { + this._loading = false; + this.messageManager.removeMessageListener("DOMContentLoaded", DOMContentLoaded); + // add our resizer after the dom is ready + if (this._useDynamicResizer) { + let DynamicResizeWatcher = Cu.import("resource:///modules/Social.jsm", {}).DynamicResizeWatcher; + this._dynamicResizer = new DynamicResizeWatcher(); + this._dynamicResizer.start(this.panel, this.content); + } else if (this._dynamicResizer) { + this._dynamicResizer.stop(); + this._dynamicResizer = null; + } + + let contentWindow = this.contentWindow; + let markUpdate = function(event) { + // update the annotation based on this event, then update the + // icon as well + this.isMarked = JSON.parse(event.detail).marked; + if (this.isMarked) { + Social.markURI(provider.origin, gBrowser.currentURI); + } else { + Social.unmarkURI(provider.origin, gBrowser.currentURI, () => { + this.update(); + }); + } + }.bind(this); + let unload = () => { + contentWindow.removeEventListener("unload", unload); + contentWindow.removeEventListener("socialMarkUpdate", markUpdate); + } + contentWindow.addEventListener("socialMarkUpdate", markUpdate); + contentWindow.addEventListener("unload", unload); + + // send the opengraph data + this.messageManager.sendAsyncMessage("Social:OpenGraphData", pageData); + } + this.messageManager.addMessageListener("DOMContentLoaded", DOMContentLoaded); + this._loading = true; + this.content.setAttribute("src", endpoint); + ]]></body> + </method> + + <method name="openPanel"> + <parameter name="aResetOnClose"/> + <body><![CDATA[ + let panel = this.panel; + let anchor = document.getAnonymousElementByAttribute(this._anchor, "class", "toolbarbutton-icon"); + // Bug 849216 - open the popup in a setTimeout so we avoid the auto-rollup + // handling from preventing it being opened in some cases. + setTimeout(() => { + panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); + }, 0); + Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(2); + ]]></body> + </method> + + <method name="markCurrentPage"> + <parameter name="aOpenPanel"/> + <body><![CDATA[ + // we always set the src on click if it has not been set for this tab, + // but we only want to open the panel if it was previously annotated. + let openPanel = this.isMarked || aOpenPanel; + let src = this.content.getAttribute("src"); + if (!src || src == "about:blank") { + this.loadPanel(); + } + if (openPanel) + this.openPanel(); + ]]></body> + </method> + + <method name="markLink"> + <parameter name="aUrl"/> + <parameter name="aTarget"/> + <body><![CDATA[ + if (!aUrl) { + this.markCurrentPage(true); + return; + } + // initiated form an external source, such as gContextMenu, where + // pageData is passed into us. In this case, we always load the iframe + // and show it since the url may not be the browser tab, but an image, + // link, etc. inside the page. We also "update" the iframe to the + // previous url when it is closed. + this.content.setAttribute("src", "about:blank"); + this.loadPanel({ url: aUrl }, aTarget); + this.openPanel(true); + ]]></body> + </method> + + <method name="dispatchPanelEvent"> + <parameter name="name"/> + <body><![CDATA[ + let evt = this.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent(name, true, true, {}); + this.contentDocument.documentElement.dispatchEvent(evt); + ]]></body> + </method> + + <method name="onShown"> + <body><![CDATA[ + // because the panel may be preloaded, we need to size the panel when + // showing as well as after load + if (!this._useDynamicResizer) { + return; + } + let sizeSocialPanelToContent = Cu.import("resource:///modules/Social.jsm", {}).sizeSocialPanelToContent; + if (!this._loading && this.contentDocument && + this.contentDocument.readyState == "complete") { + sizeSocialPanelToContent(this.panel, this.content); + } else { + let panelBrowserOnload = (message) => { + if (message.target != this.content) + return; + this.messageManager.removeMessageListener("PageVisibility:Show", panelBrowserOnload, true); + sizeSocialPanelToContent(this.panel, this.content); + }; + this.messageManager.addMessageListener("PageVisibility:Show", panelBrowserOnload); + } + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (aEvent.eventPhase != aEvent.BUBBLING_PHASE) + return; + switch(aEvent.type) { + case "DOMLinkAdded": { + // much of this logic is from DOMLinkHandler in browser.js, this sets + // the presence icon for a chat user, we simply use favicon style + // updating + let link = aEvent.originalTarget; + let rel = link.rel && link.rel.toLowerCase(); + if (!link || !link.ownerDocument || !rel || !link.href) + return; + if (link.rel.indexOf("icon") < 0) + return; + + let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}).ContentLinkHandler; + let uri = ContentLinkHandler.getLinkIconURI(link); + if (!uri) + return; + + // we cannot size the image when we apply it via listStyleImage, so + // use the toolbar image + this.setAttribute("image", uri.spec); + } + break; + case "click": + Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(2); + break; + } + ]]></body> + </method> + + </implementation> + <handlers> + <handler event="popupshowing"><![CDATA[ + this._anchor.setAttribute("open", "true"); + this.content.addEventListener("click", this); + ]]></handler> + <handler event="popupshown"><![CDATA[ + this.onShown(); + ]]></handler> + <handler event="popuphidden"><![CDATA[ + this._anchor.removeAttribute("open"); + this.update(); + this.content.removeEventListener("click", this); + ]]></handler> + <handler event="command"><![CDATA[ + this.markCurrentPage(); + ]]></handler> + </handlers> + </binding> + +</bindings>
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/chat/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": [ + "../../../../../testing/mochitest/browser.eslintrc" + ] +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/chat/browser.ini @@ -0,0 +1,9 @@ +[DEFAULT] +skip-if = buildapp == 'mulet' +support-files = + head.js + chat.html + +[browser_chatwindow.js] +[browser_focus.js] +[browser_tearoff.js]
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/chat/browser_chatwindow.js @@ -0,0 +1,197 @@ +/* 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/. */ + +requestLongerTimeout(2); + +var chatbar = document.getElementById("pinnedchats"); + +add_chat_task(function* testOpenCloseChat() { + let chatbox = yield promiseOpenChat("http://example.com"); + Assert.strictEqual(chatbox, chatbar.selectedChat); + // we requested a "normal" chat, so shouldn't be minimized + Assert.ok(!chatbox.minimized, "chat is not minimized"); + Assert.equal(chatbar.childNodes.length, 1, "should be 1 chat open"); + + + // now request the same URL again - we should get the same chat. + let chatbox2 = yield promiseOpenChat("http://example.com"); + Assert.strictEqual(chatbox2, chatbox, "got the same chat"); + Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open"); + + chatbox.toggle(); + is(chatbox.minimized, true, "chat is now minimized"); + // was no other chat to select, so selected becomes null. + is(chatbar.selectedChat, null); + + // We check the content gets an unload event as we close it. + chatbox.close(); +}); + +// In this case we open a chat minimized, then request the same chat again +// without specifying minimized. On that second call the chat should open, +// selected, and no longer minimized. +add_chat_task(function* testMinimized() { + let chatbox = yield promiseOpenChat("http://example.com", "minimized"); + Assert.strictEqual(chatbox, chatbar.selectedChat); + Assert.ok(chatbox.minimized, "chat is minimized"); + Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open"); + yield promiseOpenChat("http://example.com"); + Assert.ok(!chatbox.minimized, false, "chat is no longer minimized"); +}); + +// open enough chats to overflow the window, then check +// if the menupopup is visible +add_chat_task(function* testManyChats() { + Assert.ok(chatbar.menupopup.parentNode.collapsed, "popup nub collapsed at start"); + // we should *never* find a test box that needs more than this to cause + // an overflow! + let maxToOpen = 20; + let numOpened = 0; + for (let i = 0; i < maxToOpen; i++) { + yield promiseOpenChat("http://example.com#" + i); + if (!chatbar.menupopup.parentNode.collapsed) { + info("the menu popup appeared"); + return; + } + } + Assert.ok(false, "We didn't find a collapsed chat after " + maxToOpen + "chats!"); +}); + +// Check that closeAll works as expected. +add_chat_task(function* testOpenTwiceCallbacks() { + yield promiseOpenChat("http://example.com#1"); + yield promiseOpenChat("http://example.com#2"); + yield promiseOpenChat("http://test2.example.com"); + Assert.equal(numChatsInWindow(window), 3, "should be 3 chats open"); + Chat.closeAll("http://example.com"); + Assert.equal(numChatsInWindow(window), 1, "should have closed 2 chats"); + Chat.closeAll("http://test2.example.com"); + Assert.equal(numChatsInWindow(window), 0, "should have closed last chat"); +}); + +// Check that when we open the same chat twice, the callbacks are called back +// twice. +add_chat_task(function* testOpenTwiceCallbacks() { + yield promiseOpenChatCallback("http://example.com"); + yield promiseOpenChatCallback("http://example.com"); +}); + +// Bug 817782 - check chats work in new top-level windows. +add_chat_task(function* testSecondTopLevelWindow() { + const chatUrl = "http://example.com"; + let winPromise = BrowserTestUtils.waitForNewWindow(); + OpenBrowserWindow(); + let secondWindow = yield winPromise; + yield promiseOpenChat(chatUrl); + // the chat was created - let's make sure it was created in the second window. + Assert.equal(numChatsInWindow(window), 0, "main window has no chats"); + Assert.equal(numChatsInWindow(secondWindow), 1, "second window has 1 chat"); + secondWindow.close(); +}); + +// Test that findChromeWindowForChats() returns the expected window. +add_chat_task(function* testChatWindowChooser() { + let chat = yield promiseOpenChat("http://example.com"); + Assert.equal(numChatsInWindow(window), 1, "first window has the chat"); + // create a second window - this will be the "most recent" and will + // therefore be the window that hosts the new chat (see bug 835111) + let secondWindow = OpenBrowserWindow(); + yield promiseOneEvent(secondWindow, "load"); + Assert.equal(secondWindow, Chat.findChromeWindowForChats(null), "Second window is the preferred chat window"); + + // focus the first window, and check it will be selected for future chats. + // Bug 1090633 - there are lots of issues around focus, especially when the + // browser itself doesn't have focus. We can end up with + // Services.wm.getMostRecentWindow("navigator:browser") returning a different + // window than, say, Services.focus.activeWindow. But the focus manager isn't + // the point of this test. + // So we simply keep focusing the first window until it is reported as the + // most recent. + do { + dump("trying to force window to become the most recent.\n"); + secondWindow.focus(); + window.focus(); + yield promiseWaitForFocus(); + } while (Services.wm.getMostRecentWindow("navigator:browser") != window) + + Assert.equal(window, Chat.findChromeWindowForChats(null), "First window now the preferred chat window"); + + let privateWindow = OpenBrowserWindow({private: true}); + yield promiseOneEvent(privateWindow, "load") + + // The focused window can't accept chats (it's a private window), so the + // chat should open in the window that was selected before. This will be + // either window or secondWindow (linux may choose a different one) but the + // point is that the window is *not* the private one. + Assert.ok(Chat.findChromeWindowForChats(null) == window || + Chat.findChromeWindowForChats(null) == secondWindow, + "Private window isn't selected for new chats."); + privateWindow.close(); + secondWindow.close(); +}); + +add_chat_task(function* testButtonSet() { + let chatbox = yield promiseOpenChat("http://example.com#1"); + let document = chatbox.ownerDocument; + + // Expect all default buttons to be visible. + for (let buttonId of kDefaultButtonSet) { + let button = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId); + Assert.ok(!button.hidden, "Button '" + buttonId + "' should be visible"); + } + + let visible = new Set(["minimize", "close"]); + chatbox = yield promiseOpenChat("http://example.com#2", null, null, [...visible].join(",")); + + for (let buttonId of kDefaultButtonSet) { + let button = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId); + if (visible.has(buttonId)) { + Assert.ok(!button.hidden, "Button '" + buttonId + "' should be visible"); + } else { + Assert.ok(button.hidden, "Button '" + buttonId + "' should NOT be visible"); + } + } +}); + +add_chat_task(function* testCustomButton() { + let commanded = 0; + let customButton = { + id: "custom", + onCommand: function() { + ++commanded; + } + }; + + Chat.registerButton(customButton); + + let chatbox = yield promiseOpenChat("http://example.com#1"); + let document = chatbox.ownerDocument; + let titlebarNode = document.getAnonymousElementByAttribute(chatbox, "class", + "chat-titlebar"); + + Assert.equal(titlebarNode.getElementsByClassName("chat-custom")[0], null, + "Custom chat button should not be in the toolbar yet."); + + let visible = new Set(["minimize", "close", "custom"]); + Chat.loadButtonSet(chatbox, [...visible].join(",")); + + for (let buttonId of kDefaultButtonSet) { + let button = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId); + if (visible.has(buttonId)) { + Assert.ok(!button.hidden, "Button '" + buttonId + "' should be visible"); + } else { + Assert.ok(button.hidden, "Button '" + buttonId + "' should NOT be visible"); + } + } + + let customButtonNode = titlebarNode.getElementsByClassName("chat-custom")[0]; + Assert.ok(!customButtonNode.hidden, "Custom button should be visible"); + + let ev = document.createEvent("XULCommandEvent"); + ev.initCommandEvent("command", true, true, document.defaultView, 0, false, + false, false, false, null); + customButtonNode.dispatchEvent(ev); + + Assert.equal(commanded, 1, "Button should have been commanded once"); +});
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/chat/browser_focus.js @@ -0,0 +1,262 @@ +/* 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/. */ + +// Tests the focus functionality. + +Cu.import("resource://testing-common/ContentTask.jsm", this); +const CHAT_URL = "https://example.com/browser/browser/base/content/test/chat/chat.html"; + +requestLongerTimeout(2); + +// Is the currently opened tab focused? +function isTabFocused() { + let tabb = gBrowser.getBrowserForTab(gBrowser.selectedTab); + // focus sucks in tests - our window may have lost focus. + let elt = Services.focus.getFocusedElementForWindow(window, false, {}); + return elt == tabb; +} + +// Is the specified chat focused? +function isChatFocused(chat) { + // focus sucks in tests - our window may have lost focus. + let elt = Services.focus.getFocusedElementForWindow(window, false, {}); + return elt == chat.content; +} + +var chatbar = document.getElementById("pinnedchats"); + +function* setUp() { + // Note that (probably) due to bug 604289, if a tab is focused but the + // focused element is null, our chat windows can "steal" focus. This is + // avoided if we explicitly focus an element in the tab. + // So we load a page with an <input> field and focus that before testing. + let html = '<input id="theinput"><button id="chat-opener"></button>'; + let url = "data:text/html;charset=utf-8," + encodeURI(html); + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let browser = tab.linkedBrowser; + yield ContentTask.spawn(browser, null, function* () { + content.document.getElementById("theinput").focus(); + }); + + registerCleanupFunction(function() { + gBrowser.removeTab(tab); + }); +} + +// Test default focus - not user input. +add_chat_task(function* testDefaultFocus() { + yield setUp(); + let chat = yield promiseOpenChat("http://example.com"); + // we used the default focus behaviour, which means that because this was + // not the direct result of user action the chat should not be focused. + Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open"); + Assert.ok(isTabFocused(), "the tab should remain focused."); + Assert.ok(!isChatFocused(chat), "the chat should not be focused."); +}); + +// Test default focus via user input. +add_chat_task(function* testDefaultFocusUserInput() { + todo(false, "BrowserTestUtils.synthesizeMouseAtCenter doesn't move the user " + + "focus to the chat window, even though we're recording a click correctly."); + return; + + yield setUp(); + let browser = gBrowser.selectedTab.linkedBrowser; + let mm = browser.messageManager; + + let promise = new Promise(resolve => { + mm.addMessageListener("ChatOpenerClicked", function handler() { + mm.removeMessageListener("ChatOpenerClicked", handler); + promiseOpenChat("http://example.com").then(resolve); + }); + }); + + yield ContentTask.spawn(browser, null, function* () { + let button = content.document.getElementById("chat-opener"); + button.addEventListener("click", function onclick() { + button.removeEventListener("click", onclick); + sendAsyncMessage("ChatOpenerClicked"); + }); + }); + // Note we must use synthesizeMouseAtCenter() rather than calling + // .click() directly as this causes nsIDOMWindowUtils.isHandlingUserInput + // to be true. + yield BrowserTestUtils.synthesizeMouseAtCenter("#chat-opener", {}, browser); + let chat = yield promise; + + // we use the default focus behaviour but the chat was opened via user input, + // so the chat should be focused. + Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open"); + yield promiseWaitForCondition(() => !isTabFocused()); + Assert.ok(!isTabFocused(), "the tab should have lost focus."); + Assert.ok(isChatFocused(chat), "the chat should have got focus."); +}); + +// We explicitly ask for the chat to be focused. +add_chat_task(function* testExplicitFocus() { + yield setUp(); + let chat = yield promiseOpenChat("http://example.com", undefined, true); + // we use the default focus behaviour, which means that because this was + // not the direct result of user action the chat should not be focused. + Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open"); + yield promiseWaitForCondition(() => !isTabFocused()); + Assert.ok(!isTabFocused(), "the tab should have lost focus."); + Assert.ok(isChatFocused(chat), "the chat should have got focus."); +}); + +// Open a minimized chat via default focus behaviour - it will open and not +// have focus. Then open the same chat without 'minimized' - it will be +// restored but should still not have grabbed focus. +add_chat_task(function* testNoFocusOnAutoRestore() { + yield setUp(); + let chat = yield promiseOpenChat("http://example.com", "minimized"); + Assert.ok(chat.minimized, "chat is minimized"); + Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open"); + Assert.ok(isTabFocused(), "the tab should remain focused."); + Assert.ok(!isChatFocused(chat), "the chat should not be focused."); + yield promiseOpenChat("http://example.com"); + Assert.ok(!chat.minimized, "chat should be restored"); + Assert.ok(isTabFocused(), "the tab should remain focused."); + Assert.ok(!isChatFocused(chat), "the chat should not be focused."); +}); + +// Here we open a chat, which will not be focused. Then we minimize it and +// restore it via a titlebar clock - it should get focus at that point. +add_chat_task(function* testFocusOnExplicitRestore() { + yield setUp(); + let chat = yield promiseOpenChat("http://example.com"); + Assert.ok(!chat.minimized, "chat should have been opened restored"); + Assert.ok(isTabFocused(), "the tab should remain focused."); + Assert.ok(!isChatFocused(chat), "the chat should not be focused."); + chat.minimized = true; + Assert.ok(isTabFocused(), "tab should still be focused"); + Assert.ok(!isChatFocused(chat), "the chat should not be focused."); + + let promise = promiseOneMessage(chat.content, "Social:FocusEnsured"); + // pretend we clicked on the titlebar + chat.onTitlebarClick({button: 0}); + yield promise; // wait for focus event. + Assert.ok(!chat.minimized, "chat should have been restored"); + Assert.ok(isChatFocused(chat), "chat should be focused"); + Assert.strictEqual(chat, chatbar.selectedChat, "chat is marked selected"); +}); + +// Open 2 chats and give 1 focus. Minimize the focused one - the second +// should get focus. +add_chat_task(function* testMinimizeFocused() { + yield setUp(); + let chat1 = yield promiseOpenChat("http://example.com#1"); + let chat2 = yield promiseOpenChat("http://example.com#2"); + Assert.equal(numChatsInWindow(window), 2, "2 chats open"); + Assert.strictEqual(chatbar.selectedChat, chat2, "chat2 is selected"); + let promise = promiseOneMessage(chat1.content, "Social:FocusEnsured"); + chatbar.selectedChat = chat1; + chatbar.focus(); + yield promise; // wait for chat1 to get focus. + Assert.strictEqual(chat1, chatbar.selectedChat, "chat1 is marked selected"); + Assert.notStrictEqual(chat2, chatbar.selectedChat, "chat2 is not marked selected"); + + todo(false, "Bug 1245803 should re-enable the test below to have a chat window " + + "re-gain focus when another chat window is minimized."); + return; + + promise = promiseOneMessage(chat2.content, "Social:FocusEnsured"); + chat1.minimized = true; + yield promise; // wait for chat2 to get focus. + Assert.notStrictEqual(chat1, chatbar.selectedChat, "chat1 is not marked selected"); + Assert.strictEqual(chat2, chatbar.selectedChat, "chat2 is marked selected"); +}); + +// Open 2 chats, select and focus the second. Pressing the TAB key should +// cause focus to move between all elements in our chat window before moving +// to the next chat window. +add_chat_task(function* testTab() { + yield setUp(); + + function sendTabAndWaitForFocus(chat, eltid) { + EventUtils.sendKey("tab"); + + return ContentTask.spawn(chat.content, { eltid: eltid }, function* (args) { + let doc = content.document; + + // ideally we would use the 'focus' event here, but that doesn't work + // as expected for the iframe - the iframe itself never gets the focus + // event (apparently the sub-document etc does.) + // So just poll for the correct element getting focus... + yield new Promise(function(resolve, reject) { + let tries = 0; + let interval = content.setInterval(function() { + if (tries >= 30) { + clearInterval(interval); + reject("never got focus"); + return; + } + tries++; + let elt = args.eltid ? doc.getElementById(args.eltid) : doc.documentElement; + if (doc.activeElement == elt) { + content.clearInterval(interval); + resolve(); + } + info("retrying wait for focus: " + tries); + info("(the active element is " + doc.activeElement + "/" + + doc.activeElement.getAttribute("id") + ")"); + }, 100); + info("waiting for element " + args.eltid + " to get focus"); + }); + }); + } + + let chat1 = yield promiseOpenChat(CHAT_URL + "#1"); + let chat2 = yield promiseOpenChat(CHAT_URL + "#2"); + chatbar.selectedChat = chat2; + let promise = promiseOneMessage(chat2.content, "Social:FocusEnsured"); + chatbar.focus(); + info("waiting for second chat to get focus"); + yield promise; + + // Our chats have 3 focusable elements, so it takes 4 TABs to move + // to the new chat. + yield sendTabAndWaitForFocus(chat2, "input1"); + Assert.ok(isChatFocused(chat2), "new chat still focused after first tab"); + + yield sendTabAndWaitForFocus(chat2, "input2"); + Assert.ok(isChatFocused(chat2), "new chat still focused after tab"); + + yield sendTabAndWaitForFocus(chat2, "iframe"); + Assert.ok(isChatFocused(chat2), "new chat still focused after tab"); + + // this tab now should move to the next chat, but focus the + // document element itself (hence the null eltid) + yield sendTabAndWaitForFocus(chat1, null); + Assert.ok(isChatFocused(chat1), "first chat is focused"); +}); + +// Open a chat and focus an element other than the first. Move focus to some +// other item (the tab itself in this case), then focus the chatbar - the +// same element that was previously focused should still have focus. +add_chat_task(function* testFocusedElement() { + yield setUp(); + + // open a chat with focus requested. + let chat = yield promiseOpenChat(CHAT_URL, undefined, true); + + yield ContentTask.spawn(chat.content, null, function* () { + content.document.getElementById("input2").focus(); + }); + + // set focus to the main window. + let tabb = gBrowser.getBrowserForTab(gBrowser.selectedTab); + let promise = promiseOneEvent(window, "focus"); + Services.focus.moveFocus(window, null, Services.focus.MOVEFOCUS_ROOT, 0); + yield promise; + + promise = promiseOneMessage(chat.content, "Social:FocusEnsured"); + chatbar.focus(); + yield promise; + + yield ContentTask.spawn(chat.content, null, function* () { + Assert.equal(content.document.activeElement.getAttribute("id"), "input2", + "correct input field still has focus"); + }); +});
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/chat/browser_tearoff.js @@ -0,0 +1,135 @@ +/* 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 chatbar = document.getElementById("pinnedchats"); + +function promiseNewWindowLoaded() { + return new Promise(resolve => { + Services.wm.addListener({ + onWindowTitleChange: function() {}, + onCloseWindow: function(xulwindow) {}, + onOpenWindow: function(xulwindow) { + var domwindow = xulwindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindow); + Services.wm.removeListener(this); + // wait for load to ensure the window is ready for us to test + domwindow.addEventListener("load", function _load(event) { + let doc = domwindow.document; + if (event.target != doc) + return; + domwindow.removeEventListener("load", _load); + resolve(domwindow); + }); + }, + }); + }); +} + +add_chat_task(function* testTearoffChat() { + let chatbox = yield promiseOpenChat("http://example.com"); + Assert.equal(numChatsInWindow(window), 1, "should be 1 chat open"); + + let chatTitle = yield ContentTask.spawn(chatbox.content, null, function* () { + let chatDoc = content.document; + + // Mutate the chat document a bit before we tear it off. + let div = chatDoc.createElement("div"); + div.setAttribute("id", "testdiv"); + div.setAttribute("test", "1"); + chatDoc.body.appendChild(div); + + return chatDoc.title; + }); + + Assert.equal(chatbox.getAttribute("label"), chatTitle, + "the new chatbox should show the title of the chat window"); + + // chatbox is open, lets detach. The new chat window will be caught in + // the window watcher below + let promise = promiseNewWindowLoaded(); + + let swap = document.getAnonymousElementByAttribute(chatbox, "anonid", "swap"); + swap.click(); + + // and wait for the new window. + let domwindow = yield promise; + + Assert.equal(domwindow.document.documentElement.getAttribute("windowtype"), "Social:Chat", "Social:Chat window opened"); + Assert.equal(numChatsInWindow(window), 0, "should be no chats in the chat bar"); + + // get the chatbox from the new window. + chatbox = domwindow.document.getElementById("chatter") + Assert.equal(chatbox.getAttribute("label"), chatTitle, "window should have same title as chat"); + + yield ContentTask.spawn(chatbox.content, null, function* () { + let div = content.document.getElementById("testdiv"); + Assert.equal(div.getAttribute("test"), "1", "docshell should have been swapped"); + div.setAttribute("test", "2"); + }); + + // swap the window back to the chatbar + promise = promiseOneEvent(domwindow, "unload"); + swap = domwindow.document.getAnonymousElementByAttribute(chatbox, "anonid", "swap"); + swap.click(); + + yield promise; + + Assert.equal(numChatsInWindow(window), 1, "chat should be docked back in the window"); + chatbox = chatbar.selectedChat; + Assert.equal(chatbox.getAttribute("label"), chatTitle, + "the new chatbox should show the title of the chat window again"); + + yield ContentTask.spawn(chatbox.content, null, function* () { + let div = content.document.getElementById("testdiv"); + Assert.equal(div.getAttribute("test"), "2", "docshell should have been swapped"); + }); +}); + +// Similar test but with 2 chats. +add_chat_task(function* testReattachTwice() { + let chatbox1 = yield promiseOpenChat("http://example.com#1"); + let chatbox2 = yield promiseOpenChat("http://example.com#2"); + Assert.equal(numChatsInWindow(window), 2, "both chats should be docked in the window"); + + info("chatboxes are open, detach from window"); + let promise = promiseNewWindowLoaded(); + document.getAnonymousElementByAttribute(chatbox1, "anonid", "swap").click(); + let domwindow1 = yield promise; + chatbox1 = domwindow1.document.getElementById("chatter"); + Assert.equal(numChatsInWindow(window), 1, "only second chat should be docked in the window"); + + promise = promiseNewWindowLoaded(); + document.getAnonymousElementByAttribute(chatbox2, "anonid", "swap").click(); + let domwindow2 = yield promise; + chatbox2 = domwindow2.document.getElementById("chatter"); + Assert.equal(numChatsInWindow(window), 0, "should be no docked chats"); + + promise = promiseOneEvent(domwindow2, "unload"); + domwindow2.document.getAnonymousElementByAttribute(chatbox2, "anonid", "swap").click(); + yield promise; + Assert.equal(numChatsInWindow(window), 1, "one chat should be docked back in the window"); + + promise = promiseOneEvent(domwindow1, "unload"); + domwindow1.document.getAnonymousElementByAttribute(chatbox1, "anonid", "swap").click(); + yield promise; + Assert.equal(numChatsInWindow(window), 2, "both chats should be docked back in the window"); +}); + +// Check that Chat.closeAll() also closes detached windows. +add_chat_task(function* testCloseAll() { + let chatbox1 = yield promiseOpenChat("http://example.com#1"); + let chatbox2 = yield promiseOpenChat("http://example.com#2"); + + let promise = promiseNewWindowLoaded(); + document.getAnonymousElementByAttribute(chatbox1, "anonid", "swap").click(); + let domwindow = yield promise; + chatbox1 = domwindow.document.getElementById("chatter"); + + let promiseWindowUnload = promiseOneEvent(domwindow, "unload"); + + Assert.equal(numChatsInWindow(window), 1, "second chat should still be docked"); + Chat.closeAll("http://example.com"); + yield promiseWindowUnload; + Assert.equal(numChatsInWindow(window), 0, "should be no chats left"); +});
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/chat/chat.html @@ -0,0 +1,14 @@ +<html> + <head> + <meta charset="utf-8"> + <title>test chat window</title> + </head> + <body> + <p>This is a test chat window.</p> + <!-- a couple of input fields to help with focus testing --> + <input id="input1"/> + <input id="input2"/> + <!-- an iframe here so this one page generates multiple load events --> + <iframe id="iframe" src="data:text/plain:this is an iframe"></iframe> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/chat/head.js @@ -0,0 +1,130 @@ +/* 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/. */ + +// Utility functions for Chat tests. + +var Chat = Cu.import("resource:///modules/Chat.jsm", {}).Chat; +const kDefaultButtonSet = new Set(["minimize", "swap", "close"]); + +function promiseOpenChat(url, mode, focus, buttonSet = null) { + let uri = Services.io.newURI(url, null, null); + let origin = uri.prePath; + let title = origin; + return new Promise(resolve => { + // we just through a few hoops to ensure the content document is fully + // loaded, otherwise tests that rely on that content may intermittently fail. + let callback = function(chatbox) { + let mm = chatbox.content.messageManager; + mm.sendAsyncMessage("WaitForDOMContentLoaded"); + mm.addMessageListener("DOMContentLoaded", function cb() { + mm.removeMessageListener("DOMContentLoaded", cb); + resolve(chatbox); + }); + } + let chatbox = Chat.open(null, { + origin: origin, + title: title, + url: url, + mode: mode, + focus: focus + }, callback); + if (buttonSet) { + chatbox.setAttribute("buttonSet", buttonSet); + } + }); +} + +// Opens a chat, returns a promise resolved when the chat callback fired. +function promiseOpenChatCallback(url, mode) { + let uri = Services.io.newURI(url, null, null); + let origin = uri.prePath; + let title = origin; + return new Promise(resolve => { + Chat.open(null, { origin, title, url, mode }, resolve); + }); +} + +// Opens a chat, returns the chat window's promise which fires when the chat +// starts loading. +function promiseOneEvent(target, eventName, capture) { + return new Promise(resolve => { + target.addEventListener(eventName, function handler(event) { + target.removeEventListener(eventName, handler, capture); + resolve(); + }, capture); + }); +} + +function promiseOneMessage(target, messageName) { + return new Promise(resolve => { + let mm = target.messageManager; + mm.addMessageListener(messageName, function handler() { + mm.removeMessageListener(messageName, handler); + resolve(); + }); + }); +} + +// Return the number of chats in a browser window. +function numChatsInWindow(win) { + let chatbar = win.document.getElementById("pinnedchats"); + return chatbar.childElementCount; +} + +function promiseWaitForFocus() { + return new Promise(resolve => waitForFocus(resolve)); +} + +// A simple way to clean up after each test. +function add_chat_task(genFunction) { + add_task(function* () { + info("Starting chat test " + genFunction.name); + try { + yield genFunction(); + } finally { + info("Finished chat test " + genFunction.name + " - cleaning up."); + // close all docked chats. + while (chatbar.childNodes.length) { + chatbar.childNodes[0].close(); + } + // and non-docked chats. + let winEnum = Services.wm.getEnumerator("Social:Chat"); + while (winEnum.hasMoreElements()) { + let win = winEnum.getNext(); + if (win.closed) { + continue; + } + win.close(); + } + } + }); +} + +function waitForCondition(condition, nextTest, errorMsg) { + var tries = 0; + var interval = setInterval(function() { + if (tries >= 100) { + ok(false, errorMsg); + moveOn(); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + ok(false, e + "\n" + e.stack); + conditionPassed = false; + } + if (conditionPassed) { + moveOn(); + } + tries++; + }, 100); + var moveOn = function() { clearInterval(interval); nextTest(); }; +} + +function promiseWaitForCondition(aConditionFn) { + return new Promise((resolve, reject) => { + waitForCondition(aConditionFn, resolve, "Condition didn't pass."); + }); +}
--- a/browser/base/content/test/social/browser.ini +++ b/browser/base/content/test/social/browser.ini @@ -1,24 +1,48 @@ [DEFAULT] skip-if = buildapp == "mulet" support-files = blocklist.xml + checked.jpg head.js opengraph/og_invalid_url.html opengraph/opengraph.html opengraph/shortlink_linkrel.html opengraph/shorturl_link.html opengraph/shorturl_linkrel.html microformats.html share.html share_activate.html social_activate.html social_activate_basic.html social_activate_iframe.html + social_chat.html + social_crash_content_helper.js + social_flyout.html + social_mark.html + social_panel.html social_postActivation.html + social_sidebar.html + social_sidebar_empty.html + unchecked.jpg !/browser/base/content/test/plugins/blockNoPlugins.xml [browser_aboutHome_activation.js] [browser_addons.js] [browser_blocklist.js] [browser_share.js] [browser_social_activation.js] +[browser_social_chatwindow.js] +[browser_social_chatwindow_resize.js] +[browser_social_chatwindowfocus.js] +skip-if = asan # Bug 1260177 +[browser_social_contextmenu.js] +skip-if = (os == 'linux' && e10s) # Bug 1072669 context menu relies on target element +[browser_social_errorPage.js] +[browser_social_flyout.js] +[browser_social_isVisible.js] +[browser_social_marks.js] +[browser_social_marks_context.js] +[browser_social_multiprovider.js] +[browser_social_sidebar.js] +[browser_social_status.js] +[browser_social_window.js]
--- a/browser/base/content/test/social/browser_aboutHome_activation.js +++ b/browser/base/content/test/social/browser_aboutHome_activation.js @@ -1,28 +1,28 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; +var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AboutHomeUtils", "resource:///modules/AboutHome.jsm"); var snippet = ' <script>' + ' var manifest = {' + ' "name": "Demo Social Service",' + ' "origin": "https://example.com",' + ' "iconURL": "chrome://branding/content/icon16.png",' + ' "icon32URL": "chrome://branding/content/icon32.png",' + ' "icon64URL": "chrome://branding/content/icon64.png",' + -' "shareURL": "https://example.com/browser/browser/base/content/test/social/social_share.html",' + +' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' + ' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' + ' };' + ' function activateProvider(node) {' + ' node.setAttribute("data-service", JSON.stringify(manifest));' + ' var event = new CustomEvent("ActivateSocialFeature");' + ' node.dispatchEvent(event);' + ' }' + ' </script>' + @@ -34,17 +34,17 @@ var snippet = var snippet2 = ' <script>' + ' var manifest = {' + ' "name": "Demo Social Service",' + ' "origin": "https://example.com",' + ' "iconURL": "chrome://branding/content/icon16.png",' + ' "icon32URL": "chrome://branding/content/icon32.png",' + ' "icon64URL": "chrome://branding/content/icon64.png",' + -' "shareURL": "https://example.com/browser/browser/base/content/test/social/social_share.html",' + +' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' + ' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' + ' "oneclick": true' + ' };' + ' function activateProvider(node) {' + ' node.setAttribute("data-service", JSON.stringify(manifest));' + ' var event = new CustomEvent("ActivateSocialFeature");' + ' node.dispatchEvent(event);' + ' }' + @@ -96,18 +96,20 @@ function test() // ensure our activation snippet is indeed available yield ContentTask.spawn(tab.linkedBrowser, {}, function*(arg) { ok(!!content.document.getElementById("snippets"), "Found snippets element"); ok(!!content.document.getElementById("activationSnippet"), "The snippet is present."); }); yield new Promise(resolve => { activateProvider(tab, test.panel).then(() => { + ok(SocialSidebar.provider, "provider activated"); checkSocialUI(); - SocialService.uninstallProvider("https://example.com", function () { + is(gBrowser.currentURI.spec, SocialSidebar.provider.manifest.postActivationURL, "postActivationURL was loaded"); + SocialService.uninstallProvider(SocialSidebar.provider.origin, function () { info("provider uninstalled"); resolve(); }); }); }); // activation opened a post-activation info tab, close it. yield BrowserTestUtils.removeTab(gBrowser.selectedTab); @@ -203,27 +205,30 @@ function sendActivationEvent(tab) { doc = doc.defaultView.frames[0].document; let button = doc.getElementById("activationSnippet"); BrowserTestUtils.synthesizeMouseAtCenter(button, {}, tab.linkedBrowser); } function activateProvider(tab, expectPanel, aCallback) { return new Promise(resolve => { if (expectPanel) { - BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => { + ensureEventFired(PopupNotifications.panel, "popupshown").then(() => { let panel = document.getElementById("servicesInstall-notification"); panel.button.click(); }); } waitForProviderLoad().then(() => { + ok(SocialSidebar.provider, "new provider is active"); + ok(SocialSidebar.opened, "sidebar is open"); checkSocialUI(); resolve(); }); sendActivationEvent(tab); }); } function waitForProviderLoad(cb) { return Promise.all([ promiseObserverNotified("social:provider-enabled"), ensureFrameLoaded(gBrowser, "https://example.com/browser/browser/base/content/test/social/social_postActivation.html"), + ensureFrameLoaded(SocialSidebar.browser) ]); }
--- a/browser/base/content/test/social/browser_addons.js +++ b/browser/base/content/test/social/browser_addons.js @@ -1,28 +1,28 @@ var AddonManager = Cu.import("resource://gre/modules/AddonManager.jsm", {}).AddonManager; -var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; +var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; var manifest = { name: "provider 1", origin: "https://example.com", - shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html", iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" }; var manifest2 = { // used for testing install name: "provider 2", origin: "https://test1.example.com", - shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar_empty.html", iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png", version: "1.0" }; var manifestUpgrade = { // used for testing install name: "provider 3", origin: "https://test2.example.com", - shareURL: "https://test2.example.com/browser/browser/base/content/test/social/social_share.html", + sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html", iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png", version: "1.0" }; function test() { waitForExplicitFinish(); PopupNotifications.panel.setAttribute("animate", "false"); registerCleanupFunction(function () {
--- a/browser/base/content/test/social/browser_blocklist.js +++ b/browser/base/content/test/social/browser_blocklist.js @@ -1,29 +1,29 @@ /* 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/. */ // a place for miscellaneous social tests -var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; +var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul"; var blocklistURL = "http://example.com/browser/browser/base/content/test/social/blocklist.xml"; var manifest = { // normal provider name: "provider ok", origin: "https://example.com", - shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" }; var manifest_bad = { // normal provider name: "provider blocked", origin: "https://test1.example.com", - shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html", iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png" }; // blocklist testing function updateBlocklist() { var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"] .getService(Ci.nsITimerCallback); let promise = promiseObserverNotified("blocklist-updated");
--- a/browser/base/content/test/social/browser_share.js +++ b/browser/base/content/test/social/browser_share.js @@ -1,10 +1,10 @@ -var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; +var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; var baseURL = "https://example.com/browser/browser/base/content/test/social/"; var manifest = { // normal provider name: "provider 1", origin: "https://example.com", iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png", shareURL: "https://example.com/browser/browser/base/content/test/social/share.html"
--- a/browser/base/content/test/social/browser_social_activation.js +++ b/browser/base/content/test/social/browser_social_activation.js @@ -5,17 +5,17 @@ /////////////////// // // Whitelisting this test. // As part of bug 1077403, the leaking uncaught rejection should be fixed. // thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: Assert is null"); -var SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; +var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; var tabsToRemove = []; function removeProvider(provider) { return new Promise(resolve => { // a full install sets the manifest into a pref, addProvider alone doesn't, // make sure we uninstall if the manifest was added. if (provider.manifest) { @@ -68,16 +68,17 @@ function activateIFrameProvider(domain, newTab(activationURL).then(tab => { sendActivationEvent(tab, callback, false); }); } function waitForProviderLoad(origin) { return Promise.all([ ensureFrameLoaded(gBrowser, origin + "/browser/browser/base/content/test/social/social_postActivation.html"), + ensureFrameLoaded(SocialSidebar.browser) ]); } function getAddonItemInList(aId, aList) { var item = aList.firstChild; while (item) { if ("mAddon" in item && item.mAddon.id == aId) { aList.ensureElementIsVisible(item); @@ -107,32 +108,33 @@ function clickAddonRemoveButton(tab, aCa executeSoon(function() { aCallback(addon); }); }); BrowserTestUtils.synthesizeMouseAtCenter(button, {}, tab.linkedBrowser); }); } function activateOneProvider(manifest, finishActivation, aCallback) { - info("activating provider "+manifest.name); let panel = document.getElementById("servicesInstall-notification"); BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => { ok(!panel.hidden, "servicesInstall-notification panel opened"); if (finishActivation) panel.button.click(); else panel.closebutton.click(); }); BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden").then(() => { ok(panel.hidden, "servicesInstall-notification panel hidden"); if (!finishActivation) { ok(panel.hidden, "activation panel is not showing"); executeSoon(aCallback); } else { waitForProviderLoad(manifest.origin).then(() => { + is(SocialSidebar.provider.origin, manifest.origin, "new provider is active"); + ok(SocialSidebar.opened, "sidebar is open"); checkSocialUI(); executeSoon(aCallback); }); } }); // the test will continue as the popup events fire... activateProvider(manifest.origin, function() { @@ -140,29 +142,29 @@ function activateOneProvider(manifest, f }); } var gTestDomains = ["https://example.com", "https://test1.example.com", "https://test2.example.com"]; var gProviders = [ { name: "provider 1", origin: "https://example.com", - shareURL: "https://example.com/browser/browser/base/content/test/social/social_share.html?provider1", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html?provider1", iconURL: "chrome://branding/content/icon48.png" }, { name: "provider 2", origin: "https://test1.example.com", - shareURL: "https://test1.example.com/browser/browser/base/content/test/social/social_share.html?provider2", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar_empty.html?provider2", iconURL: "chrome://branding/content/icon64.png" }, { name: "provider 3", origin: "https://test2.example.com", - shareURL: "https://test2.example.com/browser/browser/base/content/test/social/social_share.html?provider2", + sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar_empty.html?provider2", iconURL: "chrome://branding/content/about-logo.png" } ]; function test() { PopupNotifications.panel.setAttribute("animate", "false"); registerCleanupFunction(function () { @@ -184,27 +186,29 @@ var tests = { Services.prefs.clearUserPref("social.remote-install.enabled"); next(); }); }, testIFrameActivation: function(next) { activateIFrameProvider(gTestDomains[0], function() { is(SocialUI.enabled, false, "SocialUI is not enabled"); + ok(!SocialSidebar.provider, "provider is not installed"); let panel = document.getElementById("servicesInstall-notification"); ok(panel.hidden, "activation panel still hidden"); checkSocialUI(); next(); }); }, testActivationFirstProvider: function(next) { // first up we add a manifest entry for a single provider. activateOneProvider(gProviders[0], false, function() { // we deactivated leaving no providers left, so Social is disabled. + ok(!SocialSidebar.provider, "should be no provider left after disabling"); checkSocialUI(); next(); }); }, testActivationMultipleProvider: function(next) { // The trick with this test is to make sure that Social.providers[1] is // the current provider when doing the undo - this makes sure that the @@ -212,16 +216,17 @@ var tests = { // do in some cases (but those cases do not include what this test does) // first enable the 2 providers SocialService.addProvider(gProviders[0], function() { SocialService.addProvider(gProviders[1], function() { checkSocialUI(); // activate the last provider. activateOneProvider(gProviders[2], false, function() { // we deactivated - the first provider should be enabled. + is(SocialSidebar.provider.origin, Social.providers[1].origin, "original provider should have been reactivated"); checkSocialUI(); next(); }); }); }); }, testAddonManagerDoubleInstall: function(next) {
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_chatwindow.js @@ -0,0 +1,141 @@ +/* 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 SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +var manifests = [ + { + name: "provider@example.com", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html?example.com", + iconURL: "chrome://branding/content/icon48.png" + }, + { + name: "provider@test1", + origin: "https://test1.example.com", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?test1", + iconURL: "chrome://branding/content/icon48.png" + }, + { + name: "provider@test2", + origin: "https://test2.example.com", + sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html?test2", + iconURL: "chrome://branding/content/icon48.png" + } +]; + +var chatId = 0; +function openChat(provider) { + return new Promise(resolve => { + SocialSidebar.provider = provider; + let chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html"; + let url = chatUrl + "?id=" + (chatId++); + makeChat("normal", "chat " + chatId, (cb) => { resolve(cb); }); + }); +} + +function windowHasChats(win) { + return !!getChatBar().firstElementChild; +} + +function test() { + requestLongerTimeout(2); // only debug builds seem to need more time... + waitForExplicitFinish(); + + let frameScript = "data:,(" + function frame_script() { + addMessageListener("socialTest-CloseSelf", function(e) { + content.close(); + }, true); + }.toString() + ")();"; + let mm = getGroupMessageManager("social"); + mm.loadFrameScript(frameScript, true); + + let oldwidth = window.outerWidth; // we futz with these, so we restore them + let oldleft = window.screenX; + window.moveTo(0, window.screenY) + let postSubTest = function(cb) { + let chats = document.getElementById("pinnedchats"); + ok(chats.children.length == 0, "no chatty children left behind"); + cb(); + }; + runSocialTestWithProvider(manifests, function (finishcb) { + ok(Social.enabled, "Social is enabled"); + SocialSidebar.show(); + runSocialTests(tests, undefined, postSubTest, function() { + window.moveTo(oldleft, window.screenY) + window.resizeTo(oldwidth, window.outerHeight); + mm.removeDelayedFrameScript(frameScript); + finishcb(); + }); + }); +} + +var tests = { + testOpenCloseChat: function(next) { + openChat(SocialSidebar.provider).then((cb) => { + BrowserTestUtils.waitForCondition(() => { return cb.minimized; }, + "chatbox is minimized").then(() => { + ok(cb.minimized, "chat is minimized after toggle"); + BrowserTestUtils.waitForCondition(() => { return !cb.minimized; }, + "chatbox is not minimized").then(() => { + ok(!cb.minimized, "chat is not minimized after toggle"); + promiseNodeRemoved(cb).then(next); + let mm = cb.content.messageManager; + mm.sendAsyncMessage("socialTest-CloseSelf", {}); + info("close chat window requested"); + }); + cb.toggle(); + }); + + ok(!cb.minimized, "chat is not minimized on open"); + // toggle to minimize chat + cb.toggle(); + }); + }, + + // Check what happens when you close the only visible chat. + testCloseOnlyVisible: function(next) { + let chatbar = getChatBar(); + let chatWidth = undefined; + let num = 0; + is(chatbar.childNodes.length, 0, "chatbar starting empty"); + is(chatbar.menupopup.childNodes.length, 0, "popup starting empty"); + + makeChat("normal", "first chat", function() { + // got the first one. + checkPopup(); + ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible"); + // we kinda cheat here and get the width of the first chat, assuming + // that all future chats will have the same width when open. + chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat); + let desired = chatWidth * 1.5; + resizeWindowToChatAreaWidth(desired, function(sizedOk) { + ok(sizedOk, "can't do any tests without this width"); + checkPopup(); + makeChat("normal", "second chat", function() { + is(chatbar.childNodes.length, 2, "now have 2 chats"); + let first = chatbar.childNodes[0]; + let second = chatbar.childNodes[1]; + is(chatbar.selectedChat, first, "first chat is selected"); + ok(second.collapsed, "second chat is currently collapsed"); + // closing the first chat will leave enough room for the second + // chat to appear, and thus become selected. + chatbar.selectedChat.close(); + is(chatbar.selectedChat, second, "second chat is selected"); + Task.spawn(closeAllChats).then(next); + }); + }); + }); + }, + + testShowWhenCollapsed: function(next) { + get3ChatsForCollapsing("normal", function(first, second, third) { + let chatbar = getChatBar(); + chatbar.showChat(first); + ok(!first.collapsed, "first should no longer be collapsed"); + is(second.collapsed || third.collapsed, true, "one of the others should be collapsed"); + Task.spawn(closeAllChats).then(next); + }); + } +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_chatwindow_resize.js @@ -0,0 +1,82 @@ +/* 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/. */ + +function test() { + requestLongerTimeout(2); // only debug builds seem to need more time... + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png", + // added for test purposes + chatURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html" + }; + let oldwidth = window.outerWidth; // we futz with these, so we restore them + let oldleft = window.screenX; + window.moveTo(0, window.screenY) + let postSubTest = function(cb) { + let chats = document.getElementById("pinnedchats"); + ok(chats.children.length == 0, "no chatty children left behind"); + cb(); + }; + + runSocialTestWithProvider(manifest, function (finishcb) { + let sbrowser = document.getElementById("social-sidebar-browser"); + ensureFrameLoaded(sbrowser).then(() => { + let provider = SocialSidebar.provider; + provider.chatURL = manifest.chatURL; + ok(provider, "provider is set"); + ok(provider.chatURL, "provider has chatURL"); + // executeSoon to let the browser UI observers run first + runSocialTests(tests, undefined, postSubTest, function() { + window.moveTo(oldleft, window.screenY) + window.resizeTo(oldwidth, window.outerHeight); + finishcb(); + }); + }); + SocialSidebar.show(); + }); +} + +var tests = { + + // resize and collapse testing. + testBrowserResize: function(next, mode) { + let chats = document.getElementById("pinnedchats"); + get3ChatsForCollapsing(mode || "normal", function(first, second, third) { + let chatWidth = chats.getTotalChildWidth(first); + ok(chatWidth, "have a chatwidth"); + let popupWidth = getPopupWidth(); + ok(popupWidth, "have a popupwidth"); + info("starting resize tests - each chat's width is " + chatWidth + + " and the popup width is " + popupWidth); + // Note that due to a difference between "device", "app" and "css" pixels + // we allow use 2 pixels as the minimum size difference. + resizeAndCheckWidths(first, second, third, [ + [chatWidth-2, 1, "to < 1 chat width - only last should be visible."], + [chatWidth+2, 1, "2 pixels more then one fully exposed (not counting popup) - still only 1."], + [chatWidth+popupWidth+2, 1, "2 pixels more than one fully exposed (including popup) - still only 1."], + [chatWidth*2-2, 1, "second not showing by 2 pixels (not counting popup) - only 1 exposed."], + [chatWidth*2+popupWidth-2, 1, "second not showing by 2 pixelx (including popup) - only 1 exposed."], + [chatWidth*2+popupWidth+2, 2, "big enough to fit 2 - nub remains visible as first is still hidden"], + [chatWidth*3+popupWidth-2, 2, "one smaller than the size necessary to display all three - first still hidden"], + [chatWidth*3+popupWidth+2, 3, "big enough to fit all - all exposed (which removes the nub)"], + [chatWidth*3+2, 3, "now the nub is hidden we can resize back down to chatWidth*3 before overflow."], + [chatWidth*3-2, 2, "2 pixels less and the first is again collapsed (and the nub re-appears)"], + [chatWidth*2+popupWidth+2, 2, "back down to just big enough to fit 2"], + [chatWidth*2+popupWidth-2, 1, "back down to just not enough to fit 2"], + [chatWidth*3+popupWidth+2, 3, "now a large jump to make all 3 visible (ie, affects 2)"], + [chatWidth*1.5, 1, "and a large jump back down to 1 visible (ie, affects 2)"], + ], function() { + Task.spawn(closeAllChats).then(next); + }); + }); + }, + + testBrowserResizeMinimized: function(next) { + this.testBrowserResize(next); + } +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_chatwindowfocus.js @@ -0,0 +1,66 @@ +/* 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/. */ + +function isChatFocused(chat) { + return getChatBar()._isChatFocused(chat); +} + +var manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" +}; + +function test() { + waitForExplicitFinish(); + + // Note that (probably) due to bug 604289, if a tab is focused but the + // focused element is null, our chat windows can "steal" focus. This is + // avoided if we explicitly focus an element in the tab. + // So we load a page with an <input> field and focus that before testing. + let url = "data:text/html;charset=utf-8," + encodeURI('<input id="theinput">'); + let tab = gBrowser.selectedTab = gBrowser.addTab(url, {skipAnimation: true}); + let browser = tab.linkedBrowser; + browser.addEventListener("load", function tabLoad(event) { + browser.removeEventListener("load", tabLoad, true); + // before every test we focus the input field. + let preSubTest = function(cb) { + ContentTask.spawn(browser, null, function* () { + content.focus(); + content.document.getElementById("theinput").focus(); + + yield ContentTaskUtils.waitForCondition( + () => Services.focus.focusedWindow == content, "tab should have focus"); + }).then(cb); + } + let postSubTest = function(cb) { + Task.spawn(closeAllChats).then(cb); + } + // and run the tests. + runSocialTestWithProvider(manifest, function (finishcb) { + SocialSidebar.show(); + runSocialTests(tests, preSubTest, postSubTest, function () { + BrowserTestUtils.removeTab(tab).then(finishcb); + }); + }); + }, true); +} + +var tests = { + // In this test we arrange for the sidebar to open the chat via a simulated + // click. This should cause the new chat to be opened and focused. + testFocusWhenViaUser: function(next) { + ensureFrameLoaded(document.getElementById("social-sidebar-browser")).then(() => { + let chatbar = getChatBar(); + openChatViaUser(); + ok(chatbar.firstElementChild, "chat opened"); + BrowserTestUtils.waitForCondition(() => isChatFocused(chatbar.selectedChat), + "chat should be focused").then(() => { + is(chatbar.selectedChat, chatbar.firstElementChild, "chat is selected"); + next(); + }); + }); + }, +};
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_contextmenu.js @@ -0,0 +1,74 @@ +/* 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 SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +var manifest = { // used for testing install + name: "provider test1", + origin: "https://test1.example.com", + markURL: "https://test1.example.com/browser/browser/base/content/test/social/social_mark.html?url=%{url}", + markedIcon: "https://test1.example.com/browser/browser/base/content/test/social/unchecked.jpg", + unmarkedIcon: "https://test1.example.com/browser/browser/base/content/test/social/checked.jpg", + + iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png", + version: "1.0" +}; + +function test() { + waitForExplicitFinish(); + let frameScript = "data:,(" + function frame_script() { + addEventListener("OpenGraphData", function (aEvent) { + sendAsyncMessage("sharedata", aEvent.detail); + }, true, true); + }.toString() + ")();"; + let mm = getGroupMessageManager("social"); + mm.loadFrameScript(frameScript, true); + + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, function () { + mm.removeDelayedFrameScript(frameScript); + finishcb(); + }); + }); +} + +var tests = { + testMarkMicroformats: function(next) { + // emulates context menu action using target element, calling SocialMarks.markLink + let provider = Social._getProviderFromOrigin(manifest.origin); + let target, testTab; + + // browser_share tests microformats on the full page, this is testing a + // specific target element. + let expecting = JSON.stringify({ + "url": "https://example.com/browser/browser/base/content/test/social/microformats.html", + "microformats": { + "items": [{ + "type": ["h-review"], + "properties": { + "rating": ["4.5"] + } + } + ], + "rels": {}, + "rel-urls": {} + } + }); + + let mm = getGroupMessageManager("social"); + mm.addMessageListener("sharedata", function handler(msg) { + is(msg.data, expecting, "microformats data ok"); + mm.removeMessageListener("sharedata", handler); + BrowserTestUtils.removeTab(testTab).then(next); + }); + + let url = "https://example.com/browser/browser/base/content/test/social/microformats.html" + BrowserTestUtils.openNewForegroundTab(gBrowser, url).then(tab => { + testTab = tab; + let doc = tab.linkedBrowser.contentDocument; + target = doc.getElementById("test-review"); + SocialMarks.markLink(manifest.origin, url, target); + }); + } +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_errorPage.js @@ -0,0 +1,187 @@ +/* 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/. */ + +function gc() { + Cu.forceGC(); + let wu = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + wu.garbageCollect(); +} + +var openChatWindow = Cu.import("resource://gre/modules/MozSocialAPI.jsm", {}).openChatWindow; + +function openPanel(url, panelCallback, loadCallback) { + // open a flyout + SocialFlyout.open(url, 0, panelCallback); + // wait for both open and loaded before callback. Since the test doesn't close + // the panel between opens, we cannot rely on events here. We need to ensure + // popupshown happens before we finish out the tests. + BrowserTestUtils.waitForCondition(function() { + return SocialFlyout.panel.state == "open" && + SocialFlyout.iframe.contentDocument.readyState == "complete"; + },"flyout is open and loaded").then(() => { executeSoon(loadCallback) }); +} + +function openChat(url, panelCallback, loadCallback) { + // open a chat window + let chatbar = getChatBar(); + openChatWindow(null, SocialSidebar.provider, url, panelCallback); + chatbar.firstChild.addEventListener("DOMContentLoaded", function panelLoad() { + chatbar.firstChild.removeEventListener("DOMContentLoaded", panelLoad, true); + executeSoon(loadCallback); + }, true); +} + +function onSidebarLoad(callback) { + let sbrowser = document.getElementById("social-sidebar-browser"); + sbrowser.addEventListener("load", function load() { + sbrowser.removeEventListener("load", load, true); + executeSoon(callback); + }, true); +} + +var manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" +}; + +function test() { + waitForExplicitFinish(); + + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, function(next) { goOnline().then(next) }, finishcb); + }); +} + +var tests = { + testSidebar: function(next) { + let sbrowser = document.getElementById("social-sidebar-browser"); + onSidebarLoad(function() { + ok(sbrowser.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly")==0, "sidebar is on social error page"); + gc(); + // Add a new load listener, then find and click the "try again" button. + onSidebarLoad(function() { + // should still be on the error page. + ok(sbrowser.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly")==0, "sidebar is still on social error page"); + // go online and try again - this should work. + goOnline().then(function () { + onSidebarLoad(function() { + // should now be on the correct page. + is(sbrowser.contentDocument.documentURI, manifest.sidebarURL, "sidebar is now on social sidebar page"); + next(); + }); + sbrowser.contentDocument.getElementById("btnTryAgain").click(); + }); + }); + sbrowser.contentDocument.getElementById("btnTryAgain").click(); + }); + // go offline then attempt to load the sidebar - it should fail. + goOffline().then(function() { + SocialSidebar.show(); + }); + }, + + testFlyout: function(next) { + let panelCallbackCount = 0; + let panel = document.getElementById("social-flyout-panel"); + goOffline().then(function() { + openPanel( + manifest.sidebarURL, /* empty html page */ + function() { // the panel api callback + panelCallbackCount++; + }, + function() { // the "load" callback. + todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads."); + let href = panel.firstChild.contentDocument.documentURI; + ok(href.indexOf("about:socialerror?mode=compactInfo")==0, "flyout is on social error page"); + // Bug 832943 - the listeners previously stopped working after a GC, so + // force a GC now and try again. + gc(); + openPanel( + manifest.sidebarURL, /* empty html page */ + function() { // the panel api callback + panelCallbackCount++; + }, + function() { // the "load" callback. + todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads."); + let href = panel.firstChild.contentDocument.documentURI; + ok(href.indexOf("about:socialerror?mode=compactInfo")==0, "flyout is on social error page"); + gc(); + SocialFlyout.unload(); + next(); + } + ); + } + ); + }); + }, + + testChatWindow: function(next) { + todo(false, "Bug 1245799 is needed to make error pages work again for chat windows."); + next(); + return; + + let panelCallbackCount = 0; + // chatwindow tests throw errors, which muddy test output, if the worker + // doesn't get test-init + goOffline().then(function() { + openChat( + manifest.sidebarURL, /* empty html page */ + function() { // the panel api callback + panelCallbackCount++; + }, + function() { // the "load" callback. + todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads."); + let chat = getChatBar().selectedChat; + BrowserTestUtils.waitForCondition(() => chat.content != null && chat.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly")==0, + "error page didn't appear").then(() => { + chat.close(); + next(); + }); + } + ); + }); + }, + + testChatWindowAfterTearOff: function(next) { + todo(false, "Bug 1245799 is needed to make error pages work again for chat windows."); + next(); + return; + + // Ensure that the error listener survives the chat window being detached. + let url = manifest.sidebarURL; /* empty html page */ + let panelCallbackCount = 0; + // chatwindow tests throw errors, which muddy test output, if the worker + // doesn't get test-init + // open a chat while we are still online. + openChat( + url, + null, + function() { // the "load" callback. + let chat = getChatBar().selectedChat; + is(chat.contentDocument.documentURI, url, "correct url loaded"); + // toggle to a detached window. + chat.swapWindows().then(chat => { + ok(!!chat.content, "we have chat content 1"); + BrowserTestUtils.waitForCondition(() => chat.content != null && chat.contentDocument.readyState == "complete", + "swapped window loaded").then(() => { + // now go offline and reload the chat - about:socialerror should be loaded. + goOffline().then(() => { + ok(!!chat.content, "we have chat content 2"); + chat.contentDocument.location.reload(); + info("chat reload called"); + BrowserTestUtils.waitForCondition(() => chat.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly")==0, + "error page didn't appear").then(() => { + chat.close(); + next(); + }); + }); + }); + }); + } + ); + } +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_flyout.js @@ -0,0 +1,102 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + let frameScript = "data:,(" + function frame_script() { + addMessageListener("socialTest-CloseSelf", function(e) { + content.close(); + }); + addMessageListener("socialTest-sendEvent", function(msg) { + let data = msg.data; + let evt = content.document.createEvent("CustomEvent"); + evt.initCustomEvent(data.name, true, true, JSON.stringify(data.data)); + content.document.documentElement.dispatchEvent(evt); + }); + + }.toString() + ")();"; + let mm = getGroupMessageManager("social"); + mm.loadFrameScript(frameScript, true); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" + }; + runSocialTestWithProvider(manifest, function (finishcb) { + SocialSidebar.show(); + ensureFrameLoaded(SocialSidebar.browser, manifest.sidebarURL).then(() => { + // disable transitions for the test + registerCleanupFunction(function () { + SocialFlyout.panel.removeAttribute("animate"); + }); + SocialFlyout.panel.setAttribute("animate", "false"); + runSocialTests(tests, undefined, undefined, finishcb); + }); + }); +} + +var tests = { + testResizeFlyout: function(next) { + let panel = document.getElementById("social-flyout-panel"); + + BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => { + is(panel.firstChild.contentDocument.readyState, "complete", "panel is loaded prior to showing"); + // The width of the flyout should be 400px initially + let iframe = panel.firstChild; + let body = iframe.contentDocument.body; + let cs = iframe.contentWindow.getComputedStyle(body); + + is(cs.width, "400px", "should be 400px wide"); + is(iframe.boxObject.width, 400, "iframe should now be 400px wide"); + is(cs.height, "400px", "should be 400px high"); + is(iframe.boxObject.height, 400, "iframe should now be 400px high"); + + BrowserTestUtils.waitForEvent(iframe.contentWindow, "resize").then(() => { + cs = iframe.contentWindow.getComputedStyle(body); + + is(cs.width, "500px", "should now be 500px wide"); + is(iframe.boxObject.width, 500, "iframe should now be 500px wide"); + is(cs.height, "500px", "should now be 500px high"); + is(iframe.boxObject.height, 500, "iframe should now be 500px high"); + BrowserTestUtils.waitForEvent(panel, "popuphidden").then(next); + panel.hidePopup(); + }); + SocialFlyout.dispatchPanelEvent("socialTest-MakeWider"); + }); + + SocialSidebar.browser.messageManager.sendAsyncMessage("socialTest-sendEvent", { name: "test-flyout-open", data: {} }); + }, + + testCloseSelf: function(next) { + let panel = document.getElementById("social-flyout-panel"); + BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => { + is(panel.firstChild.contentDocument.readyState, "complete", "panel is loaded prior to showing"); + BrowserTestUtils.waitForEvent(panel, "popuphidden").then(next); + let mm = panel.firstChild.messageManager; + mm.sendAsyncMessage("socialTest-CloseSelf", {}); + }); + SocialSidebar.browser.messageManager.sendAsyncMessage("socialTest-sendEvent", { name: "test-flyout-open", data: {} }); + }, + + testCloseOnLinkTraversal: function(next) { + + BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen", true).then(event => { + BrowserTestUtils.waitForCondition(function() { return panel.state == "closed" }, + "panel should close after tab open").then(() => { + BrowserTestUtils.removeTab(event.target).then(next); + }); + }); + + let panel = document.getElementById("social-flyout-panel"); + BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => { + is(panel.firstChild.contentDocument.readyState, "complete", "panel is loaded prior to showing"); + is(panel.state, "open", "flyout should be open"); + let iframe = panel.firstChild; + iframe.contentDocument.getElementById('traversal').click(); + }); + SocialSidebar.browser.messageManager.sendAsyncMessage("socialTest-sendEvent", { name: "test-flyout-open", data: {} }); + } +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_isVisible.js @@ -0,0 +1,51 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + let manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" + }; + + let frameScript = "data:,(" + function frame_script() { + addEventListener("visibilitychange", function() { + sendAsyncMessage("visibility", content.document.hidden ? "hidden" : "shown"); + }); + }.toString() + ")();"; + let mm = getGroupMessageManager("social"); + mm.loadFrameScript(frameScript, true); + + registerCleanupFunction(function () { + mm.removeDelayedFrameScript(frameScript); + }); + + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +var tests = { + testIsVisible: function(next) { + let mm = getGroupMessageManager("social"); + mm.addMessageListener("visibility", function handler(msg) { + mm.removeMessageListener("visibility", handler); + is(msg.data, "shown", "sidebar is visible"); + next(); + }); + SocialSidebar.show(); + }, + testIsNotVisible: function(next) { + let mm = getGroupMessageManager("social"); + mm.addMessageListener("visibility", function handler(msg) { + mm.removeMessageListener("visibility", handler); + is(msg.data, "hidden", "sidebar is hidden"); + next(); + }); + SocialSidebar.hide(); + } +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_marks.js @@ -0,0 +1,232 @@ +/* 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 SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +var manifest2 = { // used for testing install + name: "provider test1", + origin: "https://test1.example.com", + markURL: "https://test1.example.com/browser/browser/base/content/test/social/social_mark.html?url=%{url}", + markedIcon: "https://test1.example.com/browser/browser/base/content/test/social/unchecked.jpg", + unmarkedIcon: "https://test1.example.com/browser/browser/base/content/test/social/checked.jpg", + + iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png", + version: "1.0" +}; +var manifest3 = { // used for testing install + name: "provider test2", + origin: "https://test2.example.com", + sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png", + version: "1.0" +}; + +function test() { + waitForExplicitFinish(); + + let frameScript = "data:,(" + function frame_script() { + addEventListener("visibilitychange", function() { + sendAsyncMessage("visibility", content.document.hidden ? "hidden" : "shown"); + }); + }.toString() + ")();"; + let mm = getGroupMessageManager("social"); + mm.loadFrameScript(frameScript, true); + + PopupNotifications.panel.setAttribute("animate", "false"); + registerCleanupFunction(function () { + PopupNotifications.panel.removeAttribute("animate"); + mm.removeDelayedFrameScript(frameScript); + }); + + runSocialTests(tests, undefined, undefined, finish); +} + +var tests = { + testButtonDisabledOnActivate: function(next) { + // starting on about:blank page, share should be visible but disabled when + // adding provider + is(gBrowser.selectedBrowser.currentURI.spec, "about:blank"); + SocialService.addProvider(manifest2, function(provider) { + is(provider.origin, manifest2.origin, "provider is installed"); + let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin); + let widget = CustomizableUI.getWidget(id).forWindow(window) + ok(widget.node, "button added to widget set"); + + // bypass widget go directly to dom, check attribute states + let button = document.getElementById(id); + is(button.disabled, true, "mark button is disabled"); + // verify the attribute for proper css + is(button.getAttribute("disabled"), "true", "mark button attribute is disabled"); + // button should be visible + is(button.hidden, false, "mark button is visible"); + + checkSocialUI(window); + SocialService.disableProvider(manifest2.origin, next); + }); + }, + testNoButtonOnEnable: function(next) { + // we expect the addon install dialog to appear, we need to accept the + // install from the dialog. + let panel = document.getElementById("servicesInstall-notification"); + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => { + info("servicesInstall-notification panel opened"); + panel.button.click(); + }); + + let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html" + BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => { + let doc = tab.linkedBrowser.contentDocument; + let data = { + origin: doc.nodePrincipal.origin, + url: doc.location.href, + manifest: manifest3, + window: window + } + + Social.installProvider(data, function(addonManifest) { + // enable the provider so we know the button would have appeared + SocialService.enableProvider(manifest3.origin, function(provider) { + is(provider.origin, manifest3.origin, "provider is installed"); + let id = SocialMarks._toolbarHelper.idFromOrigin(provider.origin); + let widget = CustomizableUI.getWidget(id); + ok(!widget || !widget.forWindow(window).node, "no button added to widget set"); + Social.uninstallProvider(manifest3.origin, function() { + BrowserTestUtils.removeTab(tab).then(next); + }); + }); + }); + }); + }, + + testButtonOnEnable: function(next) { + let panel = document.getElementById("servicesInstall-notification"); + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => { + info("servicesInstall-notification panel opened"); + panel.button.click(); + }); + + // enable the provider now + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" + BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => { + let doc = tab.linkedBrowser.contentDocument; + let data = { + origin: doc.nodePrincipal.origin, + url: doc.location.href, + manifest: manifest2, + window: window + } + + Social.installProvider(data, function(addonManifest) { + SocialService.enableProvider(manifest2.origin, function(provider) { + is(provider.origin, manifest2.origin, "provider is installed"); + let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin); + let widget = CustomizableUI.getWidget(id).forWindow(window) + ok(widget.node, "button added to widget set"); + + // bypass widget go directly to dom, check attribute states + let button = document.getElementById(id); + is(button.disabled, false, "mark button is disabled"); + // verify the attribute for proper css + ok(!button.hasAttribute("disabled"), "mark button attribute is disabled"); + // button should be visible + is(button.hidden, false, "mark button is visible"); + + checkSocialUI(window); + BrowserTestUtils.removeTab(tab).then(next); + }); + }); + }); + }, + + testMarkPanel: function(next) { + // click on panel to open and wait for visibility + let provider = Social._getProviderFromOrigin(manifest2.origin); + ok(provider.enabled, "provider is enabled"); + let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin); + let widget = CustomizableUI.getWidget(id); + let btn = widget.forWindow(window).node; + ok(btn, "got a mark button"); + let ourTab; + + BrowserTestUtils.waitForEvent(btn.panel, "popupshown").then(() => { + info("marks panel shown"); + let doc = btn.contentDocument; + let unmarkBtn = doc.getElementById("unmark"); + ok(unmarkBtn, "testMarkPanel - got the panel unmark button"); + EventUtils.sendMouseEvent({type: "click"}, unmarkBtn, btn.contentWindow); + }); + + BrowserTestUtils.waitForEvent(btn.panel, "popuphidden").then(() => { + BrowserTestUtils.removeTab(ourTab).then(() => { + ok(btn.disabled, "button is disabled"); + next(); + }); + }); + + // verify markbutton is disabled when there is no browser url + ok(btn.disabled, "button is disabled"); + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" + BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => { + ourTab = tab; + ok(!btn.disabled, "button is enabled"); + // first click marks the page, second click opens the page. We have to + // synthesize so the command event happens + EventUtils.synthesizeMouseAtCenter(btn, {}); + // wait for the button to be marked, click to open panel + is(btn.panel.state, "closed", "panel should not be visible yet"); + BrowserTestUtils.waitForCondition(() => btn.isMarked, "button is marked").then(() => { + EventUtils.synthesizeMouseAtCenter(btn, {}); + }); + }); + }, + + testMarkPanelOffline: function(next) { + // click on panel to open and wait for visibility + let provider = Social._getProviderFromOrigin(manifest2.origin); + ok(provider.enabled, "provider is enabled"); + let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin); + let widget = CustomizableUI.getWidget(id); + let btn = widget.forWindow(window).node; + ok(btn, "got a mark button"); + + // verify markbutton is disabled when there is no browser url + ok(btn.disabled, "button is disabled"); + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"; + BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => { + ok(!btn.disabled, "button is enabled"); + goOffline().then(function() { + info("testing offline error page"); + // wait for popupshown + BrowserTestUtils.waitForEvent(btn.panel, "popupshown").then(() => { + info("marks panel is open"); + ensureFrameLoaded(btn.content).then(() => { + is(btn.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly"), 0, "social error page is showing "+btn.contentDocument.documentURI); + // cleanup after the page has been unmarked + BrowserTestUtils.removeTab(tab).then(() => { + ok(btn.disabled, "button is disabled"); + goOnline().then(next); + }); + }); + }); + btn.markCurrentPage(); + }); + }); + }, + + testButtonOnDisable: function(next) { + // enable the provider now + let provider = Social._getProviderFromOrigin(manifest2.origin); + ok(provider, "provider is installed"); + SocialService.disableProvider(manifest2.origin, function() { + let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin); + BrowserTestUtils.waitForCondition(() => { + // getWidget now returns null since we've destroyed the widget + return !CustomizableUI.getWidget(id) + }, "button does not exist after disabling the provider").then(() => { + checkSocialUI(window); + Social.uninstallProvider(manifest2.origin, next); + }); + }); + } +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_marks_context.js @@ -0,0 +1,106 @@ +var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +function makeMarkProvider(origin) { + return { // used for testing install + name: "mark provider " + origin, + origin: "https://" + origin + ".example.com", + markURL: "https://" + origin + ".example.com/browser/browser/base/content/test/social/social_mark.html?url=%{url}", + markedIcon: "https://" + origin + ".example.com/browser/browser/base/content/test/social/unchecked.jpg", + unmarkedIcon: "https://" + origin + ".example.com/browser/browser/base/content/test/social/checked.jpg", + iconURL: "https://" + origin + ".example.com/browser/browser/base/content/test/general/moz.png", + version: "1.0" + } +} + +function test() { + waitForExplicitFinish(); + PopupNotifications.panel.setAttribute("animate", "false"); + registerCleanupFunction(function () { + PopupNotifications.panel.removeAttribute("animate"); + }); + + runSocialTests(tests, undefined, undefined, finish); +} + +var tests = { + testContextSubmenu: function(next) { + // install 4 providers to test that the menu's are added as submenus + let manifests = [ + makeMarkProvider("sub1.test1"), + makeMarkProvider("sub2.test1"), + makeMarkProvider("sub1.test2"), + makeMarkProvider("sub2.test2") + ]; + let installed = []; + let markLinkMenu = document.getElementById("context-marklinkMenu").firstChild; + let markPageMenu = document.getElementById("context-markpageMenu").firstChild; + + function addProviders(callback) { + let manifest = manifests.pop(); + if (!manifest) { + info("INSTALLATION FINISHED"); + executeSoon(callback); + return; + } + info("INSTALLING " + manifest.origin); + let panel = document.getElementById("servicesInstall-notification"); + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => { + info("servicesInstall-notification panel opened"); + panel.button.click(); + }); + + let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html" + let id = SocialMarks._toolbarHelper.idFromOrigin(manifest.origin); + let toolbar = document.getElementById("nav-bar"); + BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => { + let doc = tab.linkedBrowser.contentDocument; + let data = { + origin: doc.nodePrincipal.origin, + url: doc.location.href, + manifest: manifest, + window: window + } + + Social.installProvider(data, function(addonManifest) { + // enable the provider so we know the button would have appeared + SocialService.enableProvider(manifest.origin, function(provider) { + BrowserTestUtils.waitForCondition(() => { return CustomizableUI.getWidget(id) }, + "button exists after enabling social").then(() => { + BrowserTestUtils.removeTab(tab).then(() => { + installed.push(manifest.origin); + // checkSocialUI will properly check where the menus are located + checkSocialUI(window); + executeSoon(function() { + addProviders(callback); + }); + }); + }); + }); + }); + }); + } + + function removeProviders(callback) { + let origin = installed.pop(); + if (!origin) { + executeSoon(callback); + return; + } + Social.uninstallProvider(origin, function(provider) { + executeSoon(function() { + removeProviders(callback); + }); + }); + } + + addProviders(function() { + removeProviders(function() { + is(SocialMarks.getProviders().length, 0, "mark providers removed"); + is(markLinkMenu.childNodes.length, 0, "marklink menu ok"); + is(markPageMenu.childNodes.length, 0, "markpage menu ok"); + checkSocialUI(window); + next(); + }); + }); + } +} \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_multiprovider.js @@ -0,0 +1,78 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + runSocialTestWithProvider(gProviders, function (finishcb) { + SocialSidebar.provider = Social.providers[0]; + SocialSidebar.show(); + is(Social.providers[0].origin, SocialSidebar.provider.origin, "selected provider in sidebar"); + runSocialTests(tests, undefined, undefined, finishcb); + }); +} + +var gProviders = [ + { + name: "provider 1", + origin: "https://test1.example.com", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider1", + iconURL: "chrome://branding/content/icon48.png" + }, + { + name: "provider 2", + origin: "https://test2.example.com", + sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider2", + iconURL: "chrome://branding/content/icon48.png" + } +]; + +var tests = { + testProviderSwitch: function(next) { + let sbrowser = document.getElementById("social-sidebar-browser"); + let menu = document.getElementById("social-statusarea-popup"); + let button = document.getElementById("social-sidebar-button"); + function checkProviderMenu(selectedProvider) { + let menuProviders = menu.querySelectorAll(".social-provider-menuitem"); + is(menuProviders.length, gProviders.length, "correct number of providers listed in the menu"); + // Find the selectedProvider's menu item + let el = menu.getElementsByAttribute("origin", selectedProvider.origin); + is(el.length, 1, "selected provider menu item exists"); + is(el[0].getAttribute("checked"), "true", "selected provider menu item is checked"); + } + + // the menu is not populated until onpopupshowing, so wait for popupshown + BrowserTestUtils.waitForEvent(menu, "popupshown", true).then(()=>{ + menu.hidePopup(); // doesn't need visibility + // first provider should already be visible in the sidebar + is(Social.providers[0].origin, SocialSidebar.provider.origin, "selected provider in sidebar"); + checkProviderMenu(Social.providers[0]); + + // Now activate "provider 2" + BrowserTestUtils.waitForEvent(sbrowser, "load", true).then(()=>{ + checkUIStateMatchesProvider(Social.providers[1]); + + BrowserTestUtils.waitForEvent(sbrowser, "load", true).then(()=>{ + checkUIStateMatchesProvider(Social.providers[0]); + next(); + }); + + // show the menu again so the menu is updated with the correct commands + BrowserTestUtils.waitForEvent(menu, "popupshown", true).then(()=>{ + // click on the provider menuitem to switch providers + let el = menu.getElementsByAttribute("origin", Social.providers[0].origin); + is(el.length, 1, "selected provider menu item exists"); + EventUtils.synthesizeMouseAtCenter(el[0], {}); + }); + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + SocialSidebar.provider = Social.providers[1]; + }); + EventUtils.synthesizeMouseAtCenter(button, {}); + } +} + +function checkUIStateMatchesProvider(provider) { + // Sidebar + is(document.getElementById("social-sidebar-browser").getAttribute("src"), provider.sidebarURL, "side bar URL is set"); +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_sidebar.js @@ -0,0 +1,98 @@ +/* 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 SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +var manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" +}; + +function test() { + waitForExplicitFinish(); + + let frameScript = "data:,(" + function frame_script() { + addEventListener("visibilitychange", function() { + sendAsyncMessage("visibility", content.document.hidden ? "hidden" : "shown"); + }); + }.toString() + ")();"; + let mm = getGroupMessageManager("social"); + mm.loadFrameScript(frameScript, true); + + registerCleanupFunction(function () { + mm.removeDelayedFrameScript(frameScript); + }); + + SocialService.addProvider(manifest, function() { + // the test will remove the provider + doTest(); + }); +} + +function doTest() { + ok(SocialSidebar.canShow, "social sidebar should be able to be shown"); + ok(!SocialSidebar.opened, "social sidebar should not be open by default"); + + let command = document.getElementById("Social:ToggleSidebar"); + let sidebar = document.getElementById("social-sidebar-box"); + let browser = sidebar.lastChild; + ok(!browser.docShellIsActive, "sidebar is not active"); + is(sidebar.hidden, true, "sidebar should be hidden"); + is(command.getAttribute("checked"), "false", "toggle command should be unchecked"); + + function checkShown(shouldBeShown) { + is(command.getAttribute("checked"), shouldBeShown ? "true" : "false", + "toggle command should be " + (shouldBeShown ? "checked" : "unchecked")); + is(sidebar.hidden, !shouldBeShown, + "sidebar should be " + (shouldBeShown ? "visible" : "hidden")); + is(browser.docShellIsActive, shouldBeShown, "sidebar isActive in correct state"); + if (shouldBeShown) { + is(browser.getAttribute('src'), SocialSidebar.provider.sidebarURL, "sidebar url should be set"); + // We don't currently check docShellIsActive as this is only set + // after load event fires, and the tests below explicitly wait for this + // anyway. + } + else { + ok(!browser.docShellIsActive, "sidebar should have an inactive docshell"); + // sidebar will only be immediately unloaded (and thus set to + // about:blank) when canShow is false. + if (SocialSidebar.canShow) { + // should not have unloaded so will still be the provider URL. + is(browser.getAttribute('src'), SocialSidebar.provider.sidebarURL, "sidebar url should be set"); + } else { + // should have been an immediate unload. + is(browser.getAttribute('src'), "about:blank", "sidebar url should be blank"); + } + } + } + ensureFrameLoaded(browser).then(() => { + // First check the the sidebar is initially visible, and loaded + ok(!command.hidden, "toggle command should be visible"); + let mm = getGroupMessageManager("social"); + mm.addMessageListener("visibility", function shown(msg) { + if (msg.data == "shown") { + mm.removeMessageListener("visibility", shown); + checkShown(true); + info("Toggling sidebar to closed"); + SocialSidebar.toggleSidebar(); + } + }); + mm.addMessageListener("visibility", function handler(msg) { + if (msg.data == "hidden") { + mm.removeMessageListener("visibility", handler); + // disable social. + SocialService.disableProvider(SocialSidebar.provider.origin, function() { + checkShown(false); + is(Social.providers.length, 0, "no providers left"); + defaultFinishChecks(); + // Finish the test + executeSoon(finish); + }); + } + }); + }); + SocialSidebar.show(); +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_status.js @@ -0,0 +1,216 @@ +/* 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 SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +var manifest = { // builtin provider + name: "provider example.com", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" +}; +var manifest2 = { // used for testing install + name: "provider test1", + origin: "https://test1.example.com", + statusURL: "https://test1.example.com/browser/browser/base/content/test/social/social_panel.html", + iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png", + version: "1.0" +}; +var manifest3 = { // used for testing install + name: "provider test2", + origin: "https://test2.example.com", + sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://test2.example.com/browser/browser/base/content/test/general/moz.png", + version: "1.0" +}; + + +function openWindowAndWaitForInit(callback) { + let topic = "browser-delayed-startup-finished"; + let w = OpenBrowserWindow(); + Services.obs.addObserver(function providerSet(subject, topic, data) { + Services.obs.removeObserver(providerSet, topic); + executeSoon(() => callback(w)); + }, topic, false); +} + +function test() { + waitForExplicitFinish(); + + let frameScript = "data:,(" + function frame_script() { + addMessageListener("socialTest-sendEvent", function(msg) { + let data = msg.data; + let evt = content.document.createEvent("CustomEvent"); + evt.initCustomEvent(data.name, true, true, JSON.stringify(data.data)); + content.document.documentElement.dispatchEvent(evt); + }); + }.toString() + ")();"; + let mm = getGroupMessageManager("social"); + mm.loadFrameScript(frameScript, true); + + PopupNotifications.panel.setAttribute("animate", "false"); + registerCleanupFunction(function () { + PopupNotifications.panel.removeAttribute("animate"); + mm.removeDelayedFrameScript(frameScript); + }); + + runSocialTestWithProvider(manifest, function (finishcb) { + runSocialTests(tests, undefined, undefined, function () { + Services.prefs.clearUserPref("social.remote-install.enabled"); + // just in case the tests failed, clear these here as well + Services.prefs.clearUserPref("social.whitelist"); + CustomizableUI.reset(); + finishcb(); + }); + }); +} + +var tests = { + testNoButtonOnEnable: function(next) { + // we expect the addon install dialog to appear, we need to accept the + // install from the dialog. + let panel = document.getElementById("servicesInstall-notification"); + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => { + info("servicesInstall-notification panel opened"); + panel.button.click(); + }) + + let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html" + BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => { + let doc = tab.linkedBrowser.contentDocument; + let data = { + origin: doc.nodePrincipal.origin, + url: doc.location.href, + manifest: manifest3, + window: window + } + Social.installProvider(data, function(addonManifest) { + // enable the provider so we know the button would have appeared + SocialService.enableProvider(manifest3.origin, function(provider) { + is(provider.origin, manifest3.origin, "provider is installed"); + let id = SocialStatus._toolbarHelper.idFromOrigin(provider.origin); + let widget = CustomizableUI.getWidget(id); + ok(!widget || !widget.forWindow(window).node, "no button added to widget set"); + Social.uninstallProvider(manifest3.origin, function() { + BrowserTestUtils.removeTab(tab).then(next); + }); + }); + }); + }); + }, + testButtonOnEnable: function(next) { + let panel = document.getElementById("servicesInstall-notification"); + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => { + info("servicesInstall-notification panel opened"); + panel.button.click(); + }); + + // enable the provider now + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" + BrowserTestUtils.openNewForegroundTab(gBrowser, activationURL).then(tab => { + let doc = tab.linkedBrowser.contentDocument; + let data = { + origin: doc.nodePrincipal.origin, + url: doc.location.href, + manifest: manifest2, + window: window + } + + Social.installProvider(data, function(addonManifest) { + SocialService.enableProvider(manifest2.origin, function(provider) { + is(provider.origin, manifest2.origin, "provider is installed"); + let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin); + let widget = CustomizableUI.getWidget(id).forWindow(window); + ok(widget.node, "button added to widget set"); + checkSocialUI(window); + BrowserTestUtils.removeTab(tab).then(next); + }); + }); + }); + }, + testStatusPanel: function(next) { + let icon = { + name: "testIcon", + iconURL: "chrome://browser/skin/Info.png", + counter: 1 + }; + + // click on panel to open and wait for visibility + let provider = Social._getProviderFromOrigin(manifest2.origin); + let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin); + let widget = CustomizableUI.getWidget(id); + let btn = widget.forWindow(window).node; + + // Disable the transition + let panel = document.getElementById("social-notification-panel"); + panel.setAttribute("animate", "false"); + BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => { + ensureFrameLoaded(panel.firstChild).then(() => { + let mm = panel.firstChild.messageManager; + mm.sendAsyncMessage("socialTest-sendEvent", { name: "Social:Notification", data: icon }); + BrowserTestUtils.waitForCondition( + () => { return btn.getAttribute("badge"); }, "button updated by notification").then(() => { + is(btn.style.listStyleImage, "url(\"" + icon.iconURL + "\")", "notification icon updated"); + panel.hidePopup(); + }); + }); + }); + BrowserTestUtils.waitForEvent(panel, "popuphidden").then(() => { + panel.removeAttribute("animate"); + next(); + }); + btn.click(); // open the panel + }, + + testPanelOffline: function(next) { + // click on panel to open and wait for visibility + let provider = Social._getProviderFromOrigin(manifest2.origin); + ok(provider.enabled, "provider is enabled"); + let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin); + let widget = CustomizableUI.getWidget(id); + let btn = widget.forWindow(window).node; + ok(btn, "got a status button"); + let frameId = btn.getAttribute("notificationFrameId"); + let frame = document.getElementById(frameId); + + goOffline().then(function() { + info("testing offline error page"); + // wait for popupshown + let panel = document.getElementById("social-notification-panel"); + BrowserTestUtils.waitForEvent(panel, "popupshown").then(() => { + ensureFrameLoaded(frame).then(() => { + is(frame.contentDocument.documentURI.indexOf("about:socialerror?mode=tryAgainOnly"), 0, "social error page is showing "+frame.contentDocument.documentURI); + // We got our error page, reset to avoid test leak. + BrowserTestUtils.waitForEvent(frame, "load", true).then(() => { + is(frame.contentDocument.documentURI, "about:blank", "closing error panel"); + BrowserTestUtils.waitForEvent(panel, "popuphidden").then(next); + panel.hidePopup(); + }); + goOnline().then(() => { + info("resetting error panel"); + frame.setAttribute("src", "about:blank"); + }); + }); + }); + // reload after going offline, wait for unload to open panel + BrowserTestUtils.waitForEvent(frame, "unload", true).then(() => { + btn.click(); + }); + frame.contentDocument.location.reload(); + }); + }, + + testButtonOnDisable: function(next) { + // enable the provider now + let provider = Social._getProviderFromOrigin(manifest2.origin); + ok(provider, "provider is installed"); + SocialService.disableProvider(manifest2.origin, function() { + let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin); + BrowserTestUtils.waitForCondition(() => { return !document.getElementById(id) }, + "button does not exist after disabling the provider").then(() => { + Social.uninstallProvider(manifest2.origin, next); + }); + }); + } +}
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_window.js @@ -0,0 +1,251 @@ +// 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/. + +// Test the top-level window UI for social. + +var SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +// This function should "reset" Social such that the next time Social.init() +// is called (eg, when a new window is opened), it re-performs all +// initialization. +function resetSocial() { + Social.initialized = false; + Social.providers = []; + // *sob* - listeners keep getting added... + SocialService._providerListeners.clear(); +} + +var createdWindows = []; + +function openWindowAndWaitForInit(parentWin, callback) { + // this notification tells us SocialUI.init() has been run... + let topic = "browser-delayed-startup-finished"; + let w = parentWin.OpenBrowserWindow(); + createdWindows.push(w); + Services.obs.addObserver(function providerSet(subject, topic, data) { + Services.obs.removeObserver(providerSet, topic); + info(topic + " observer was notified - continuing test"); + executeSoon(() => callback(w)); + }, topic, false); +} + +function closeWindow(w, cb) { + waitForNotification("domwindowclosed", cb); + w.close(); +} + +function closeOneWindow(cb) { + let w = createdWindows.pop(); + if (!w || w.closed) { + cb(); + return; + } + closeWindow(w, function() { + closeOneWindow(cb); + }); + w.close(); +} + +function postTestCleanup(cb) { + closeOneWindow(cb); +} + +var manifest = { // normal provider + name: "provider 1", + origin: "https://example.com", + sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png" +}; +var manifest2 = { // used for testing install + name: "provider test1", + origin: "https://test1.example.com", + sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png", +}; + +function test() { + waitForExplicitFinish(); + requestLongerTimeout(2); + runSocialTests(tests, undefined, postTestCleanup); +} + +var tests = { + // check when social is totally disabled at startup (ie, no providers enabled) + testInactiveStartup: function(cbnext) { + is(Social.providers.length, 0, "needs zero providers to start this test."); + ok(!SocialService.hasEnabledProviders, "no providers are enabled"); + resetSocial(); + openWindowAndWaitForInit(window, function(w1) { + checkSocialUI(w1); + // Now social is (re-)initialized, open a secondary window and check that. + openWindowAndWaitForInit(window, function(w2) { + checkSocialUI(w2); + checkSocialUI(w1); + cbnext(); + }); + }); + }, + + // Check when providers are enabled and social is turned on at startup. + testEnabledStartup: function(cbnext) { + setManifestPref("social.manifest.test", manifest); + ok(!SocialSidebar.opened, "sidebar is closed initially"); + SocialService.addProvider(manifest, function() { + SocialService.addProvider(manifest2, function (provider) { + SocialSidebar.show(); + BrowserTestUtils.waitForCondition( + () => SocialSidebar.opened, "sidebar did not open").then(() => { + ok(SocialSidebar.opened, "first window sidebar is open"); + openWindowAndWaitForInit(window, function(w1) { + ok(w1.SocialSidebar.opened, "new window sidebar is open"); + ok(SocialService.hasEnabledProviders, "providers are enabled"); + checkSocialUI(w1); + // now init is complete, open a second window + openWindowAndWaitForInit(window, function(w2) { + ok(w1.SocialSidebar.opened, "w1 sidebar is open"); + ok(w2.SocialSidebar.opened, "w2 sidebar is open"); + checkSocialUI(w2); + checkSocialUI(w1); + + // disable social and re-check + SocialService.disableProvider(manifest.origin, function() { + SocialService.disableProvider(manifest2.origin, function() { + ok(!Social.enabled, "social is disabled"); + is(Social.providers.length, 0, "no providers"); + ok(!w1.SocialSidebar.opened, "w1 sidebar is closed"); + ok(!w2.SocialSidebar.opened, "w2 sidebar is closed"); + checkSocialUI(w2); + checkSocialUI(w1); + Services.prefs.clearUserPref("social.manifest.test"); + cbnext(); + }); + }); + }); + }); + }); + }, cbnext); + }, cbnext); + }, + + testGlobalState: function(cbnext) { + setManifestPref("social.manifest.test", manifest); + ok(!SocialSidebar.opened, "sidebar is closed initially"); + ok(!Services.prefs.prefHasUserValue("social.sidebar.provider"), "global state unset"); + // mimick no session state in opener so we exercise the global state via pref + SessionStore.deleteWindowValue(window, "socialSidebar"); + ok(!SessionStore.getWindowValue(window, "socialSidebar"), "window state unset"); + SocialService.addProvider(manifest, function() { + openWindowAndWaitForInit(window, function(w1) { + w1.SocialSidebar.show(); + BrowserTestUtils.waitForCondition(() => w1.SocialSidebar.opened, "sidebar opened").then(() => { + ok(Services.prefs.prefHasUserValue("social.sidebar.provider"), "global state set"); + ok(!SocialSidebar.opened, "1. main sidebar is still closed"); + ok(w1.SocialSidebar.opened, "1. window sidebar is open"); + closeWindow(w1, function() { + // this time, the global state should cause the sidebar to be opened + // in the new window + openWindowAndWaitForInit(window, function(w1) { + ok(!SocialSidebar.opened, "2. main sidebar is still closed"); + ok(w1.SocialSidebar.opened, "2. window sidebar is open"); + w1.SocialSidebar.hide(); + ok(!w1.SocialSidebar.opened, "2. window sidebar is closed"); + ok(!Services.prefs.prefHasUserValue("social.sidebar.provider"), "2. global state unset"); + // global state should now be no sidebar gets opened on new window + closeWindow(w1, function() { + ok(!Services.prefs.prefHasUserValue("social.sidebar.provider"), "3. global state unset"); + ok(!SocialSidebar.opened, "3. main sidebar is still closed"); + openWindowAndWaitForInit(window, function(w1) { + ok(!Services.prefs.prefHasUserValue("social.sidebar.provider"), "4. global state unset"); + ok(!SocialSidebar.opened, "4. main sidebar is still closed"); + ok(!w1.SocialSidebar.opened, "4. window sidebar is closed"); + SocialService.disableProvider(manifest.origin, function() { + Services.prefs.clearUserPref("social.manifest.test"); + cbnext(); + }); + }); + }); + }); + }); + }); + }); + }); + }, + + // Check per window sidebar functionality, including migration from using + // prefs to using session state, and state inheritance of windows (new windows + // inherit state from the opener). + testPerWindowSidebar: function(cbnext) { + function finishCheck() { + // disable social and re-check + SocialService.disableProvider(manifest.origin, function() { + SocialService.disableProvider(manifest2.origin, function() { + ok(!Social.enabled, "social is disabled"); + is(Social.providers.length, 0, "no providers"); + Services.prefs.clearUserPref("social.manifest.test"); + cbnext(); + }); + }); + } + + setManifestPref("social.manifest.test", manifest); + ok(!SocialSidebar.opened, "sidebar is closed initially"); + SocialService.addProvider(manifest, function() { + SocialService.addProvider(manifest2, function (provider) { + // test the migration of the social.sidebar.open pref. We'll set a user + // level pref to indicate it was open (along with the old + // social.provider.current pref), then we'll open a window. During the + // restoreState of the window, those prefs should be migrated, and the + // sidebar should be opened. Both prefs are then removed. + Services.prefs.setCharPref("social.provider.current", "https://example.com"); + Services.prefs.setBoolPref("social.sidebar.open", true); + + openWindowAndWaitForInit(window, function(w1) { + ok(w1.SocialSidebar.opened, "new window sidebar is open"); + ok(SocialService.hasEnabledProviders, "providers are enabled"); + ok(!Services.prefs.prefHasUserValue("social.provider.current"), "social.provider.current pref removed"); + ok(!Services.prefs.prefHasUserValue("social.sidebar.open"), "social.sidebar.open pref removed"); + checkSocialUI(w1); + // now init is complete, open a second window, it's state should be the same as the opener + openWindowAndWaitForInit(w1, function(w2) { + ok(w1.SocialSidebar.opened, "w1 sidebar is open"); + ok(w2.SocialSidebar.opened, "w2 sidebar is open"); + checkSocialUI(w2); + checkSocialUI(w1); + + // change the sidebar in w2 + w2.SocialSidebar.show(manifest2.origin); + let sbrowser1 = w1.document.getElementById("social-sidebar-browser"); + is(manifest.origin, sbrowser1.getAttribute("origin"), "w1 sidebar origin matches"); + let sbrowser2 = w2.document.getElementById("social-sidebar-browser"); + is(manifest2.origin, sbrowser2.getAttribute("origin"), "w2 sidebar origin matches"); + + // hide sidebar, w1 sidebar should still be open + w2.SocialSidebar.hide(); + ok(w1.SocialSidebar.opened, "w1 sidebar is opened"); + ok(!w2.SocialSidebar.opened, "w2 sidebar is closed"); + ok(sbrowser2.parentNode.hidden, "w2 sidebar is hidden"); + + // open a 3rd window from w2, it should inherit the state of w2 + openWindowAndWaitForInit(w2, function(w3) { + // since the sidebar is not open, we need to ensure the provider + // is selected to test we inherited the provider from the opener + w3.SocialSidebar.ensureProvider(); + is(w3.SocialSidebar.provider, w2.SocialSidebar.provider, "w3 has same provider as w2"); + ok(!w3.SocialSidebar.opened, "w2 sidebar is closed"); + + // open a 4th window from w1, it should inherit the state of w1 + openWindowAndWaitForInit(w1, function(w4) { + is(w4.SocialSidebar.provider, w1.SocialSidebar.provider, "w4 has same provider as w1"); + ok(w4.SocialSidebar.opened, "w4 sidebar is opened"); + + finishCheck(); + }); + }); + + }); + }); + }, cbnext); + }, cbnext); + } +}
new file mode 100644 index 0000000000000000000000000000000000000000..4cbbe18e6125b6591a6a5eb38b036e38b97b235a GIT binary patch literal 785 zc$@(d1Md8ZP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!qe(<TRCwBA z{Qv(y10?_;fLL%zw7b|zDSi7iUFO#hga1tb{=Z;myLwlb^Tq$qFOz`MTYz|a#qvkn z00M{s<N|E?r5yLaUonCI|D8Ym|KFxH|Nqz-{=aUi_y70z_y2ddnEh{$klhSY2M|Ck z?RFMS`L8dmVfgpYhvDCUR))Vov;Y2Q5U!oTz-;frz`!Q}6kuXt?wQ8$^3_cS?xWKg zUcUPQa^!jt0T4heY=8f7GW}%|<*1#)%D@ct(eFPD3^K9|3<5xn5AQQDNJ=v>T)V`; z@ckRZ^-BvFRDz=zUcdbe(z^ph00a<=;QwFJOuv}<!5-hfiGcwWsCtGB4AOE849cnu z3}3%60A0uM@#gJcqTCE@-{0M5`0?`(1JM7^Kw1C-h)LwncP%D%ZUK-p8JL(E7+BdE z7#JBD7}(et7&ti@7(RUhdg(92b9v?OBEq~3EFAnOfejEqOftW|m@*es@H0Gq0Mx+2 zz`(%{F^G*7=nWx=K|pW(VqxVM6BA*0ar-1l-&L3a00G1z_xp=3BNsO}!_k8bK({e4 z7#K4!D5)?oh=?)#J$B&Vx9wYhvj6_c`kr5qMa-Ik;pLkz@Bjn>fB<6R`S)M^|L<SS z48Fk(455(>|BcNUeogIRcpKyM|I+$37dS1glimmkFD}})gW=zwzYMS5d<5y+4Kn~B zfLOK&$X(K!+@vG^?F;8Wpujsuj^CH~#m<J_|7;};RkSEl?sb-*SRxY(GsD|=pFjb4 z2xb640I`G|zm^K5Q(*$4K&+0LFx^B&1+b=AfB<45GRgZ(iVI?u00<yr4N&Ld=45#B z>OBKAAHV@X05Or0^WMD&r@*7gVgLa|tO3(EZadC!`|i`}AU3iDK!5=N56SIJ3p?Tv P00000NkvXXu0mjf%wuI)
--- a/browser/base/content/test/social/head.js +++ b/browser/base/content/test/social/head.js @@ -51,25 +51,27 @@ function checkProviderPrefsEmpty(isError function defaultFinishChecks() { checkProviderPrefsEmpty(true); finish(); } function runSocialTestWithProvider(manifest, callback, finishcallback) { - let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; + let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; let manifests = Array.isArray(manifest) ? manifest : [manifest]; // Check that none of the provider's content ends up in history. function finishCleanUp() { + ok(!SocialSidebar.provider, "no provider in sidebar"); + SessionStore.setWindowValue(window, "socialSidebar", ""); for (let i = 0; i < manifests.length; i++) { let m = manifests[i]; - for (let what of ['iconURL', 'shareURL']) { + for (let what of ['sidebarURL', 'iconURL', 'shareURL', 'markURL']) { if (m[what]) { yield promiseSocialUrlNotRemembered(m[what]); } }; } for (let i = 0; i < gURLsNotRemembered.length; i++) { yield promiseSocialUrlNotRemembered(gURLsNotRemembered[i]); } @@ -188,55 +190,397 @@ function runSocialTests(tests, cbPreTest }); } runNextTest(); } // A fairly large hammer which checks all aspects of the SocialUI for // internal consistency. function checkSocialUI(win) { - let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; + let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + win = win || window; + let doc = win.document; + let enabled = win.SocialUI.enabled; + let active = Social.providers.length > 0 && !win.SocialUI._chromeless && + !PrivateBrowsingUtils.isWindowPrivate(win); + let sidebarEnabled = win.SocialSidebar.provider ? enabled : false; + // if we have enabled providers, we should also have instances of those // providers if (SocialService.hasEnabledProviders) { ok(Social.providers.length > 0, "providers are enabled"); } else { is(Social.providers.length, 0, "providers are not enabled"); } + + // some local helpers to avoid log-spew for the many checks made here. + let numGoodTests = 0, numTests = 0; + function _ok(what, msg) { + numTests++; + if (!ok) + ok(what, msg) + else + ++numGoodTests; + } + function _is(a, b, msg) { + numTests++; + if (a != b) + is(a, b, msg) + else + ++numGoodTests; + } + function isbool(a, b, msg) { + _is(!!a, !!b, msg); + } + isbool(win.SocialSidebar.canShow, sidebarEnabled, "social sidebar active?"); + + let contextMenus = [ + { + type: "link", + id: "context-marklinkMenu", + label: "social.marklinkMenu.label" + }, + { + type: "page", + id: "context-markpageMenu", + label: "social.markpageMenu.label" + } + ]; + + for (let c of contextMenus) { + let leMenu = document.getElementById(c.id); + let parent, menus; + let markProviders = SocialMarks.getProviders(); + if (markProviders.length > SocialMarks.MENU_LIMIT) { + // menus should be in a submenu, not in the top level of the context menu + parent = leMenu.firstChild; + menus = document.getElementsByClassName("context-mark" + c.type); + _is(menus.length, 0, "menu's are not in main context menu\n"); + menus = parent.childNodes; + _is(menus.length, markProviders.length, c.id + " menu exists for each mark provider"); + } else { + // menus should be in the top level of the context menu, not in a submenu + parent = leMenu.parentNode; + menus = document.getElementsByClassName("context-mark" + c.type); + _is(menus.length, markProviders.length, c.id + " menu exists for each mark provider"); + menus = leMenu.firstChild.childNodes; + _is(menus.length, 0, "menu's are not in context submenu\n"); + } + for (let m of menus) + _is(m.parentNode, parent, "menu has correct parent"); + } + + // and for good measure, check all the social commands. + isbool(!doc.getElementById("Social:ToggleSidebar").hidden, sidebarEnabled, "Social:ToggleSidebar visible?"); + isbool(!doc.getElementById("Social:ToggleNotifications").hidden, enabled, "Social:ToggleNotifications visible?"); + + // and report on overall success of failure of the various checks here. + is(numGoodTests, numTests, "The Social UI tests succeeded.") +} + +function waitForNotification(topic, cb) { + function observer(subject, topic, data) { + Services.obs.removeObserver(observer, topic); + cb(); + } + Services.obs.addObserver(observer, topic, false); } function setManifestPref(name, manifest) { let string = Cc["@mozilla.org/supports-string;1"]. createInstance(Ci.nsISupportsString); string.data = JSON.stringify(manifest); Services.prefs.setComplexValue(name, Ci.nsISupportsString, string); } function getManifestPrefname(aManifest) { // is same as the generated name in SocialServiceInternal.getManifestPrefname let originUri = Services.io.newURI(aManifest.origin, null, null); return "social.manifest." + originUri.hostPort.replace('.','-'); } +function setBuiltinManifestPref(name, manifest) { + // we set this as a default pref, it must not be a user pref + manifest.builtin = true; + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(manifest); + Services.prefs.getDefaultBranch(null).setComplexValue(name, Ci.nsISupportsString, string); + // verify this is set on the default branch + let stored = Services.prefs.getComplexValue(name, Ci.nsISupportsString).data; + is(stored, string.data, "manifest '"+name+"' stored in default prefs"); + // don't dirty our manifest, we'll need it without this flag later + delete manifest.builtin; + // verify we DO NOT have a user-level pref + ok(!Services.prefs.prefHasUserValue(name), "manifest '"+name+"' is not in user-prefs"); +} + +function resetBuiltinManifestPref(name) { + Services.prefs.getDefaultBranch(null).deleteBranch(name); + is(Services.prefs.getDefaultBranch(null).getPrefType(name), + Services.prefs.PREF_INVALID, "default manifest removed"); +} + +function ensureEventFired(elem, event) { + return BrowserTestUtils.waitForEvent(elem, event, true); +} + function ensureFrameLoaded(frame, uri) { return new Promise(resolve => { if (frame.contentDocument && frame.contentDocument.readyState == "complete" && (!uri || frame.contentDocument.location.href == uri)) { resolve(); } else { frame.addEventListener("load", function handler() { if (uri && frame.contentDocument.location.href != uri) return; frame.removeEventListener("load", handler, true); resolve() }, true); } }); } +// chat test help functions + +// And lots of helpers for the resize tests. +function get3ChatsForCollapsing(mode, cb) { + // We make one chat, then measure its size. We then resize the browser to + // ensure a second can be created fully visible but a third can not - then + // create the other 2. first will will be collapsed, second fully visible + // and the third also visible and the "selected" one. + let chatbar = getChatBar(); + let chatWidth = undefined; + let num = 0; + is(chatbar.childNodes.length, 0, "chatbar starting empty"); + is(chatbar.menupopup.childNodes.length, 0, "popup starting empty"); + + makeChat(mode, "first chat", function() { + // got the first one. + checkPopup(); + ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible"); + // we kinda cheat here and get the width of the first chat, assuming + // that all future chats will have the same width when open. + chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat); + let desired = chatWidth * 2.5; + resizeWindowToChatAreaWidth(desired, function(sizedOk) { + ok(sizedOk, "can't do any tests without this width"); + checkPopup(); + makeChat(mode, "second chat", function() { + is(chatbar.childNodes.length, 2, "now have 2 chats"); + checkPopup(); + // and create the third. + makeChat(mode, "third chat", function() { + is(chatbar.childNodes.length, 3, "now have 3 chats"); + checkPopup(); + // XXX - this is a hacky implementation detail around the order of + // the chats. Ideally things would be a little more sane wrt the + // other in which the children were created. + let second = chatbar.childNodes[2]; + let first = chatbar.childNodes[1]; + let third = chatbar.childNodes[0]; + is(first.collapsed, true, "first collapsed state as promised"); + is(second.collapsed, false, "second collapsed state as promised"); + is(third.collapsed, false, "third collapsed state as promised"); + is(chatbar.selectedChat, third, "third is selected as promised") + info("have 3 chats for collapse testing - starting actual test..."); + cb(first, second, third); + }, mode); + }, mode); + }); + }, mode); +} + +function makeChat(mode, uniqueid, cb) { + info("making a chat window '" + uniqueid +"'"); + let provider = SocialSidebar.provider; + let chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html"; + // chatURL is not a part of the provider class, but is added by tests if we + // want to use a specific url (different than above) for testing + if (provider.chatURL) { + chatUrl = provider.chatURL; + } + // Note that we use promiseChatLoaded instead of the callback to ensure the + // content has started loading. + let chatbox = getChatBar().openChat({ + origin: provider.origin, + title: provider.name,url: chatUrl + "?id=" + uniqueid, + mode: mode + }); + chatbox.promiseChatLoaded.then( + () => { + info("chat window has opened"); + chatbox.content.messageManager.sendAsyncMessage("Social:SetDocumentTitle", { + title: uniqueid + }); + cb(chatbox); + }); +} + +function checkPopup() { + // popup only showing if any collapsed popup children. + let chatbar = getChatBar(); + let numCollapsed = 0; + for (let chat of chatbar.childNodes) { + if (chat.collapsed) { + numCollapsed += 1; + // and it have a menuitem weakmap + is(chatbar.menuitemMap.get(chat).nodeName, "menuitem", "collapsed chat has a menu item"); + } else { + ok(!chatbar.menuitemMap.has(chat), "open chat has no menu item"); + } + } + is(chatbar.menupopup.parentNode.collapsed, numCollapsed == 0, "popup matches child collapsed state"); + is(chatbar.menupopup.childNodes.length, numCollapsed, "popup has correct count of children"); + // todo - check each individual elt is what we expect? +} +// Resize the main window so the chat area's boxObject is |desired| wide. +// Does a callback passing |true| if the window is now big enough or false +// if we couldn't resize large enough to satisfy the test requirement. +function resizeWindowToChatAreaWidth(desired, cb, count = 0) { + let current = getChatBar().getBoundingClientRect().width; + let delta = desired - current; + info(count + ": resizing window so chat area is " + desired + " wide, currently it is " + + current + ". Screen avail is " + window.screen.availWidth + + ", current outer width is " + window.outerWidth); + + // WTF? Sometimes we will get fractional values due to the - err - magic + // of DevPointsPerCSSPixel etc, so we allow a couple of pixels difference. + let widthDeltaCloseEnough = function(d) { + return Math.abs(d) < 2; + } + + // attempting to resize by (0,0), unsurprisingly, doesn't cause a resize + // event - so just callback saying all is well. + if (widthDeltaCloseEnough(delta)) { + info(count + ": skipping this as screen width is close enough"); + executeSoon(function() { + cb(true); + }); + return; + } + // On lo-res screens we may already be maxed out but still smaller than the + // requested size, so asking to resize up also will not cause a resize event. + // So just callback now saying the test must be skipped. + if (window.screen.availWidth - window.outerWidth < delta) { + info(count + ": skipping this as screen available width is less than necessary"); + executeSoon(function() { + cb(false); + }); + return; + } + function resize_handler(event) { + // we did resize - but did we get far enough to be able to continue? + let newSize = getChatBar().getBoundingClientRect().width; + let sizedOk = widthDeltaCloseEnough(newSize - desired); + if (!sizedOk) + return; + window.removeEventListener("resize", resize_handler, true); + info(count + ": resized window width is " + newSize); + executeSoon(function() { + cb(sizedOk); + }); + } + // Otherwise we request resize and expect a resize event + window.addEventListener("resize", resize_handler, true); + window.resizeBy(delta, 0); +} + +function resizeAndCheckWidths(first, second, third, checks, cb) { + if (checks.length == 0) { + cb(); // nothing more to check! + return; + } + let count = checks.length; + let [width, numExpectedVisible, why] = checks.shift(); + info("<< Check " + count + ": " + why); + info(count + ": " + "resizing window to " + width + ", expect " + numExpectedVisible + " visible items"); + resizeWindowToChatAreaWidth(width, function(sizedOk) { + checkPopup(); + ok(sizedOk, count+": window resized correctly"); + function collapsedObserver(r, m) { + if ([first, second, third].filter(item => !item.collapsed).length == numExpectedVisible) { + if (m) { + m.disconnect(); + } + ok(true, count + ": " + "correct number of chats visible"); + info(">> Check " + count); + executeSoon(function() { + resizeAndCheckWidths(first, second, third, checks, cb); + }); + } + } + let m = new MutationObserver(collapsedObserver); + m.observe(first, {attributes: true }); + m.observe(second, {attributes: true }); + m.observe(third, {attributes: true }); + // and just in case we are already at the right size, explicitly call the + // observer. + collapsedObserver(undefined, m); + }, count); +} + +function getChatBar() { + let cb = document.getElementById("pinnedchats"); + cb.hidden = false; + return cb; +} + +function getPopupWidth() { + let chatbar = getChatBar(); + let popup = chatbar.menupopup; + ok(!popup.parentNode.collapsed, "asking for popup width when it is visible"); + let cs = document.defaultView.getComputedStyle(popup.parentNode); + let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight); + return popup.parentNode.getBoundingClientRect().width + margins; +} + +function promiseNodeRemoved(aNode) { + return new Promise(resolve => { + let parent = aNode.parentNode; + + let observer = new MutationObserver(function onMutatations(mutations) { + for (let mutation of mutations) { + for (let i = 0; i < mutation.removedNodes.length; i++) { + let node = mutation.removedNodes.item(i); + if (node != aNode) { + continue; + } + observer.disconnect(); + resolve(); + } + } + }); + observer.observe(parent, {childList: true}); + }); +} + +function promiseCloseChat(chat) { + let promise = promiseNodeRemoved(chat); + chat.close(); + return promise; +} + +function closeAllChats() { + let chatbar = getChatBar(); + while (chatbar.selectedChat) { + yield promiseCloseChat(chatbar.selectedChat); + } +} + +function openChatViaUser() { + let sidebarDoc = document.getElementById("social-sidebar-browser").contentDocument; + let button = sidebarDoc.getElementById("chat-opener"); + // Note we must use synthesizeMouseAtCenter() rather than calling + // .click() directly as this causes nsIDOMWindowUtils.isHandlingUserInput + // to be true. + EventUtils.synthesizeMouseAtCenter(button, {}, sidebarDoc.defaultView); +} + + // Support for going on and offline. // (via browser/base/content/test/browser_bookmark_titles.js) var origProxyType = Services.prefs.getIntPref('network.proxy.type'); function toggleOfflineStatus(goOffline) { // Bug 968887 fix. when going on/offline, wait for notification before continuing return new Promise(resolve => { if (!goOffline) {
--- a/browser/base/content/test/social/social_activate.html +++ b/browser/base/content/test/social/social_activate.html @@ -8,17 +8,18 @@ var data = { // currently required "name": "Demo Social Service", "iconURL": "chrome://branding/content/icon16.png", "icon32URL": "chrome://branding/content/favicon32.png", "icon64URL": "chrome://branding/content/icon64.png", // at least one of these must be defined - "shareURL": "/browser/browser/base/content/test/social/social_share.html", + "sidebarURL": "/browser/browser/base/content/test/social/social_sidebar.html", + "statusURL": "/browser/browser/base/content/test/social/social_panel.html", "postActivationURL": "/browser/browser/base/content/test/social/social_postActivation.html", // should be available for display purposes "description": "A short paragraph about this provider", "author": "Shane Caraveo, Mozilla", // optional "version": "1.0"
--- a/browser/base/content/test/social/social_activate_basic.html +++ b/browser/base/content/test/social/social_activate_basic.html @@ -8,17 +8,17 @@ var data = { // currently required "name": "Demo Social Service", "iconURL": "chrome://branding/content/icon16.png", "icon32URL": "chrome://branding/content/favicon32.png", "icon64URL": "chrome://branding/content/icon64.png", // at least one of these must be defined - "shareURL": "/browser/browser/base/content/test/social/social_share.html", + "sidebarURL": "/browser/browser/base/content/test/social/social_sidebar_empty.html", "postActivationURL": "/browser/browser/base/content/test/social/social_postActivation.html", // should be available for display purposes "description": "A short paragraph about this provider", "author": "Shane Caraveo, Mozilla", // optional "version": "1.0"
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/social_chat.html @@ -0,0 +1,15 @@ +<html> + <head> + <meta charset="utf-8"> + <title>test chat window</title> + </head> + <body> + <p>This is a test social chat window.</p> + <!-- a couple of input fields to help with focus testing --> + <input id="input1"/> + <input id="input2"/> + + <!-- an iframe here so this one page generates multiple load events --> + <iframe id="iframe" src="data:text/plain:this is an iframe"></iframe> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/social_flyout.html @@ -0,0 +1,25 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + window.addEventListener("socialTest-MakeWider", function(e) { + document.body.setAttribute("style", "width: 500px; height: 500px; margin: 0; overflow: hidden;"); + document.body.offsetWidth; // force a layout flush + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent("SocialTest-DoneMakeWider", true, true, {}); + document.documentElement.dispatchEvent(evt); + }, false); + window.addEventListener("socialTest-CloseSelf", function(e) { + window.close(); + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent("SocialTest-DoneCloseSelf", true, true, {}); + document.documentElement.dispatchEvent(evt); + }, false); + </script> + </head> + <body style="width: 400px; height: 400px; margin: 0; overflow: hidden;"> + <p>This is a test social flyout panel.</p> + <a id="traversal" href="https://test.example.com">test link</a> + </body> +</html> +
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/social_mark.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<head> + <link id="siteicon" rel="icon" href="./icon.png"/> + <title>Demo Mark Window</title> + <script type="text/javascript"> + + function updateTextNode(parent, text) { + var textNode = parent.childNodes[0]; + if (textNode) + parent.removeChild(textNode); + textNode = document.createTextNode(text); + parent.appendChild(textNode); + } + function onLoad() { + updateTextNode(document.getElementById("shared"), location.search); + socialMarkUpdate(true); + } + function socialMarkUpdate(isMarked) { + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent("socialMarkUpdate", true, true, JSON.stringify({marked: isMarked})); + document.documentElement.dispatchEvent(evt); + } + var shareData; + addEventListener("OpenGraphData", function(e) { + shareData = JSON.parse(e.detail); + updateTextNode(document.getElementById("shared"), shareData.url); + socialMarkUpdate(true); + }); + </script> +</head> + +<body onload="onLoad()"> + <div id="content"> + <h3>This window shows the mark data</h3> + <div>Page Marked: <div id="shared" class="textbox"></div></div> + <button id="unmark" onclick="socialMarkUpdate(false); window.close()">Unmark</button> + <button onclick="window.close();">Close</button> + </div> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/social_panel.html @@ -0,0 +1,8 @@ +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <p>This is a test social panel.</p> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/social_sidebar.html @@ -0,0 +1,17 @@ +<html> + <head> + <meta charset="utf-8"> + <script> + addEventListener("test-flyout-open", function(e) { + navigator.mozSocial.openPanel("social_flyout.html"); + }, false); + addEventListener("test-flyout-close", function(e) { + navigator.mozSocial.closePanel(); + }, false); + </script> + </head> + <body> + <p>This is a test social sidebar.</p> + <button id="chat-opener" onclick="navigator.mozSocial.openChatWindow('./social_chat.html');"/> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/social_sidebar_empty.html @@ -0,0 +1,8 @@ +<html> + <head> + <meta charset="utf-8"> + </head> + <body> + <p>This is a test social sidebar.</p> + </body> +</html>
new file mode 100644 index 0000000000000000000000000000000000000000..4d3e72b8018ea0be59ca08a45c2ab7c1b6dcd73c GIT binary patch literal 779 zc$@(X1N8ifP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004XF*Lt006JZ zHwB960000PbVXQnQ*UN;cVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!ok>JNRCwBA z{Qv(y10?_;fLL%z*k!g$ed1M`{)1n};NP$R|JlC2xElMZ^Tq$qFOz`MTYz|a#qvkn z00M{s<N|D%yXWP<!0o^OpMLr8|C+o1{u{3T^Ivbu_5Z)WzyH6x#q583gzRRJI)DIT zu}E)cx*(sw=HGt?pZ|;utPD&H4F7?6f`>4JiySk906PN%6Eg$DG&^R7pQleUJU{x3 z;pMv@AV;nT5dZ<i@`sU)lZl;4bc#0zD+^HLA7GHkax*XpurV;)|IEN3{f~j+(pv_G zZ-0OW?V8G<5*)?w`t4_s-W?zUAb?nYu?k8vb2IUSIGY{<Z3ohZ!VC;@ybKJgd<+a< zelsutUB__y^~YZ}+zf2r-`!{U@$(M@(ErasdH@25={u{47B?r8029!5W*}w<1}h^_ zf(^*#1Umf_(15={1C;+i|1KiT%fQ0Hj}q7b0mSr$L&mhih?)NZ&{Z5D0~jF&u>xr! z4v0b0Krj7yagAS0OoZXZ?UNvVS78PK1Q5#?E;(IpRz~iFFBlkp0{v?&!oZ*cbh;QP z1H*yGfB$X0_wDCTMt0T&?td%{9~c;3zWD+VKo9^3Ag2EuJmSCp{$~zWWMGI?Vqh>6 z{m;;I_7{WC^0)uj9J+MDT81-8_}81ot=ZWO|Ni`Cc=hHZNZ)Rl0RRESBKK^|rKXiy zI$t=&IT`-{W8nDr?zh;7OJ_g#gjz8$2!j=MbXdH$@e@mAVqs=@`|cAc01v?o00<zK zYkeW9Kspr>ETTZHj-D{xL`4O#rdWUgVj?oh`%8)oVwC_0AYu(r=i%mLc=75z12iAN z0YCsTk&^S?y$7ejqsU?a0Yt0;(>HEA&T#wg)9D~KvIIbY0RX^P>0zF&!}|aL002ov JPDHLkV1h<LU9$iH
--- a/browser/base/jar.mn +++ b/browser/base/jar.mn @@ -96,16 +96,17 @@ browser.jar: * content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml) #ifdef CAN_DRAW_IN_TITLEBAR content/browser/browser-tabsintitlebar.js (content/browser-tabsintitlebar.js) #else content/browser/browser-tabsintitlebar.js (content/browser-tabsintitlebar-stub.js) #endif content/browser/browser-thumbnails.js (content/browser-thumbnails.js) content/browser/browser-trackingprotection.js (content/browser-trackingprotection.js) +* content/browser/chatWindow.xul (content/chatWindow.xul) content/browser/tab-content.js (content/tab-content.js) content/browser/content.js (content/content.js) content/browser/social-content.js (content/social-content.js) content/browser/defaultthemes/1.footer.jpg (content/defaultthemes/1.footer.jpg) content/browser/defaultthemes/1.header.jpg (content/defaultthemes/1.header.jpg) content/browser/defaultthemes/1.icon.jpg (content/defaultthemes/1.icon.jpg) content/browser/defaultthemes/1.preview.jpg (content/defaultthemes/1.preview.jpg) content/browser/defaultthemes/2.footer.jpg (content/defaultthemes/2.footer.jpg) @@ -183,16 +184,18 @@ browser.jar: * content/browser/viewSourceOverlay.xul (content/viewSourceOverlay.xul) #ifndef XP_MACOSX * content/browser/webrtcIndicator.xul (content/webrtcIndicator.xul) content/browser/webrtcIndicator.js (content/webrtcIndicator.js) #endif #ifdef XP_WIN content/browser/win6BrowserOverlay.xul (content/win6BrowserOverlay.xul) #endif + content/browser/socialmarks.xml (content/socialmarks.xml) + content/browser/socialchat.xml (content/socialchat.xml) # the following files are browser-specific overrides * content/browser/license.html (/toolkit/content/license.html) % override chrome://global/content/license.html chrome://browser/content/license.html #ifdef MOZ_SAFE_BROWSING content/browser/report-phishing-overlay.xul (content/report-phishing-overlay.xul) content/browser/blockedSite.xhtml (content/blockedSite.xhtml) % overlay chrome://browser/content/browser.xul chrome://browser/content/report-phishing-overlay.xul #endif
--- a/browser/base/moz.build +++ b/browser/base/moz.build @@ -11,16 +11,17 @@ MOCHITEST_MANIFESTS += [ ] MOCHITEST_CHROME_MANIFESTS += [ 'content/test/chrome/chrome.ini', ] BROWSER_CHROME_MANIFESTS += [ 'content/test/alerts/browser.ini', + 'content/test/chat/browser.ini', 'content/test/general/browser.ini', 'content/test/newtab/browser.ini', 'content/test/plugins/browser.ini', 'content/test/popupNotifications/browser.ini', 'content/test/referrer/browser.ini', 'content/test/social/browser.ini', 'content/test/tabPrompts/browser.ini', 'content/test/urlbar/browser.ini',
--- a/browser/components/customizableui/CustomizableWidgets.jsm +++ b/browser/components/customizableui/CustomizableWidgets.jsm @@ -544,35 +544,41 @@ const CustomizableWidgets = [ onViewShowing: function(aEvent) { // Populate the subview with whatever menuitems are in the // sidebar menu. We skip menu elements, because the menu panel has no way // of dealing with those right now. let doc = aEvent.target.ownerDocument; let win = doc.defaultView; let menu = doc.getElementById("viewSidebarMenu"); - // First clear any existing menuitems then populate. Add it to the + // First clear any existing menuitems then populate. Social sidebar + // options may not have been added yet, so we do that here. Add it to the // standard menu first, then copy all sidebar options to the panel. + win.SocialSidebar.clearProviderMenus(); + let providerMenuSeps = menu.getElementsByClassName("social-provider-menu"); + if (providerMenuSeps.length > 0) + win.SocialSidebar.populateProviderMenu(providerMenuSeps[0]); + let sidebarItems = doc.getElementById("PanelUI-sidebarItems"); clearSubview(sidebarItems); fillSubviewFromMenuItems([...menu.children], sidebarItems); } }, { id: "social-share-button", // custom build our button so we can attach to the share command type: "custom", onBuild: function(aDocument) { let node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); node.setAttribute("id", this.id); node.classList.add("toolbarbutton-1"); node.classList.add("chromeclass-toolbar-additional"); node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label")); node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext")); node.setAttribute("removable", "true"); - node.setAttribute("observes", "Social:PageShareable"); + node.setAttribute("observes", "Social:PageShareOrMark"); node.setAttribute("command", "Social:SharePage"); let listener = { onWidgetAdded: (aWidgetId) => { if (aWidgetId != this.id) return; Services.obs.notifyObservers(null, "social:" + this.id + "-added", null);
--- a/browser/extensions/pocket/bootstrap.js +++ b/browser/extensions/pocket/bootstrap.js @@ -10,17 +10,17 @@ Cu.import("resource://services-common/ut Cu.import("resource://gre/modules/Preferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource:///modules/RecentWindow.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SocialService", - "resource:///modules/SocialService.jsm"); + "resource://gre/modules/SocialService.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Pocket", "chrome://pocket/content/Pocket.jsm"); XPCOMUtils.defineLazyGetter(this, "gPocketBundle", function() { return Services.strings.createBundle("chrome://pocket/locale/pocket.properties");
--- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -156,16 +156,17 @@ These should match what Safari and other <!ENTITY closeWindow.label "Close Window"> <!ENTITY closeWindow.accesskey "d"> <!ENTITY bookmarksMenu.label "Bookmarks"> <!ENTITY bookmarksMenu.accesskey "B"> <!ENTITY bookmarkThisPageCmd.label "Bookmark This Page"> <!ENTITY editThisBookmarkCmd.label "Edit This Bookmark"> <!ENTITY bookmarkThisPageCmd.commandkey "d"> +<!ENTITY markPageCmd.commandkey "l"> <!-- LOCALIZATION NOTE (findShareServices.label): - Use the unicode ellipsis char, \u2026, - or use "..." if \u2026 doesn't suit traditions in your locale. --> <!ENTITY findShareServices.label "Find more Share services…"> <!ENTITY sharePageCmd.label "Share This Page"> <!ENTITY sharePageCmd.commandkey "S"> <!ENTITY sharePageCmd.accesskey "s"> <!-- LOCALIZATION NOTE (shareLink.accesskey): must be different than the following share access keys --> @@ -769,18 +770,32 @@ you can use these alternative items. Oth <!ENTITY syncSignIn.label "Sign In To &syncBrand.shortName.label;…"> <!ENTITY syncSignIn.accesskey "Y"> <!ENTITY syncSyncNowItem.label "Sync Now"> <!ENTITY syncSyncNowItem.accesskey "S"> <!ENTITY syncReAuthItem.label "Reconnect to &syncBrand.shortName.label;…"> <!ENTITY syncReAuthItem.accesskey "R"> <!ENTITY syncToolbarButton.label "Sync"> +<!ENTITY socialToolbar.title "Social Toolbar Button"> + +<!ENTITY social.ok.label "OK"> +<!ENTITY social.ok.accesskey "O"> + +<!ENTITY social.toggleSidebar.label "Show sidebar"> +<!ENTITY social.toggleSidebar.accesskey "s"> + <!ENTITY social.addons.label "Manage Services…"> +<!ENTITY social.toggleNotifications.label "Show desktop notifications"> +<!ENTITY social.toggleNotifications.accesskey "n"> + +<!ENTITY social.learnMore.label "Learn more…"> +<!ENTITY social.learnMore.accesskey "l"> + <!ENTITY social.directory.label "Activations Directory"> <!ENTITY social.directory.text "You can activate Share services from the directory."> <!ENTITY social.directory.button "Take me there!"> <!ENTITY social.directory.introText "Click on a service to add it to &brandShortName;."> <!ENTITY social.directory.viewmore.text "View More"> <!ENTITY customizeMode.menuAndToolbars.header2 "Additional Tools and Features"> <!ENTITY customizeMode.menuAndToolbars.empty "Want more tools?"> @@ -791,16 +806,25 @@ you can use these alternative items. Oth <!ENTITY customizeMode.lwthemes "Themes"> <!ENTITY customizeMode.lwthemes.myThemes "My Themes"> <!ENTITY customizeMode.lwthemes.recommended "Recommended"> <!ENTITY customizeMode.lwthemes.menuManage "Manage"> <!ENTITY customizeMode.lwthemes.menuManage.accessKey "M"> <!ENTITY customizeMode.lwthemes.menuGetMore "Get More Themes"> <!ENTITY customizeMode.lwthemes.menuGetMore.accessKey "G"> +<!ENTITY social.chatBar.commandkey "c"> +<!ENTITY social.chatBar.label "Focus chats"> +<!ENTITY social.chatBar.accesskey "c"> + +<!ENTITY social.markpageMenu.accesskey "P"> +<!ENTITY social.markpageMenu.label "Save Page To…"> +<!ENTITY social.marklinkMenu.accesskey "L"> +<!ENTITY social.marklinkMenu.label "Save Link To…"> + <!ENTITY getUserMedia.selectCamera.label "Camera to share:"> <!ENTITY getUserMedia.selectCamera.accesskey "C"> <!ENTITY getUserMedia.selectMicrophone.label "Microphone to share:"> <!ENTITY getUserMedia.selectMicrophone.accesskey "M"> <!ENTITY getUserMedia.audioCapture.label "Audio from the tab will be shared."> <!ENTITY getUserMedia.allWindowsShared.message "All visible windows on your screen will be shared."> <!ENTITY trackingProtection.title "Tracking Protection">
new file mode 100644 --- /dev/null +++ b/browser/modules/Chat.jsm @@ -0,0 +1,323 @@ +/* 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/. */ +"use strict"; + +// A module for working with chat windows. + +this.EXPORTED_SYMBOLS = ["Chat", "kDefaultButtonSet"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const kDefaultButtonSet = new Set(["minimize", "swap", "close"]); +const kHiddenDefaultButtons = new Set(["minimize", "close"]); +var gCustomButtons = new Map(); + +// A couple of internal helper function. +function isWindowChromeless(win) { + // XXX - stolen from browser-social.js, but there's no obvious place to + // put this so it can be shared. + + // Is this a popup window that doesn't want chrome shown? + let docElem = win.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"); + return chromeless; +} + +function isWindowGoodForChats(win) { + return !win.closed && + !!win.document.getElementById("pinnedchats") && + !isWindowChromeless(win) && + !PrivateBrowsingUtils.isWindowPrivate(win); +} + +function getChromeWindow(contentWin) { + return contentWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); +} + +/* + * The exported Chat object + */ + +var Chat = { + + /** + * Iterator of <chatbox> elements from this module in all windows. + */ + get chatboxes() { + return function*() { + let winEnum = Services.wm.getEnumerator("navigator:browser"); + while (winEnum.hasMoreElements()) { + let win = winEnum.getNext(); + let chatbar = win.document.getElementById("pinnedchats"); + if (!chatbar) + continue; + + // Make a new array instead of the live NodeList so this iterator can be + // used for closing/deleting. + let chatboxes = [...chatbar.children]; + for (let chatbox of chatboxes) { + yield chatbox; + } + } + + // include standalone chat windows + winEnum = Services.wm.getEnumerator("Social:Chat"); + while (winEnum.hasMoreElements()) { + let win = winEnum.getNext(); + if (win.closed) + continue; + yield win.document.getElementById("chatter"); + } + }(); + }, + + /** + * Open a new chatbox. + * + * @param contentWindow [optional] + * The content window that requested this chat. May be null. + * @param options + * Object that may contain the following properties: + * - origin + * The origin for the chat. This is primarily used as an identifier + * to help identify all chats from the same provider. + * - title + * The title to be used if a new chat window is created. + * - url + * The URL for the that. Should be under the origin. If an existing + * chatbox exists with the same URL, it will be reused and returned. + * - mode [optional] + * May be undefined or 'minimized' + * - focus [optional] + * Indicates if the chatbox should be focused. If undefined the chat + * will be focused if the window is currently handling user input (ie, + * if the chat is being opened as a direct result of user input) + * - remote [optional] + * Indicates if the chatbox browser should use the remote bindings + * to run in the content process when TRUE. + * @param callback + * Function to be invoked once the chat constructed. The chatbox binding + * is passed as the first argument. + * + * @return A chatbox binding. This binding has a number of promises which + * can be used to determine when the chatbox is being created and + * has loaded. Will return null if no chat can be created (Which + * should only happen in edge-cases) + */ + open: function(contentWindow, options, callback) { + let chromeWindow = this.findChromeWindowForChats(contentWindow); + if (!chromeWindow) { + Cu.reportError("Failed to open a chat window - no host window could be found."); + return null; + } + + let chatbar = chromeWindow.document.getElementById("pinnedchats"); + chatbar.hidden = false; + if (options.remote) { + // Double check that current window can handle remote browser elements. + let browser = chromeWindow.gBrowser && chromeWindow.gBrowser.selectedBrowser; + if (!browser || browser.getAttribute("remote") != "true") { + options.remote = false; + } + } + let chatbox = chatbar.openChat(options, callback); + // getAttention is ignored if the target window is already foreground, so + // we can call it unconditionally. + chromeWindow.getAttention(); + // If focus is undefined we want automatic focus handling, and only focus + // if a direct result of user action. + if (!("focus" in options)) { + let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + options.focus = dwu.isHandlingUserInput; + } + if (options.focus) { + chatbar.focus(); + } + return chatbox; + }, + + /** + * Close all chats from the specified origin. + * + * @param origin + * The origin from which all chats should be closed. + */ + closeAll: function(origin) { + for (let chatbox of this.chatboxes) { + if (chatbox.content.getAttribute("origin") != origin) { + continue; + } + chatbox.close(); + } + }, + + /** + * Focus the chatbar associated with a window + * + * @param window + */ + focus: function(win) { + let chatbar = win.document.getElementById("pinnedchats"); + if (chatbar && !chatbar.hidden) { + chatbar.focus(); + } + + }, + + // This is exported as socialchat.xml needs to find a window when a chat + // is re-docked. + findChromeWindowForChats: function(preferredWindow) { + if (preferredWindow) { + preferredWindow = getChromeWindow(preferredWindow); + if (isWindowGoodForChats(preferredWindow)) { + return preferredWindow; + } + } + // no good - we just use the "most recent" browser window which can host + // chats (we used to try and "group" all chats in the same browser window, + // but that didn't work out so well - see bug 835111 + + // Try first the most recent window as getMostRecentWindow works + // even on platforms where getZOrderDOMWindowEnumerator is broken + // (ie. Linux). This will handle most cases, but won't work if the + // foreground window is a popup. + let mostRecent = Services.wm.getMostRecentWindow("navigator:browser"); + if (isWindowGoodForChats(mostRecent)) + return mostRecent; + + let topMost, enumerator; + // *sigh* - getZOrderDOMWindowEnumerator is broken except on Mac and + // Windows. We use BROKEN_WM_Z_ORDER as that is what some other code uses + // and a few bugs recommend searching mxr for this symbol to identify the + // workarounds - we want this code to be hit in such searches. + let os = Services.appinfo.OS; + const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin"; + if (BROKEN_WM_Z_ORDER) { + // this is oldest to newest and no way to change the order. + enumerator = Services.wm.getEnumerator("navigator:browser"); + } else { + // here we explicitly ask for bottom-to-top so we can use the same logic + // where BROKEN_WM_Z_ORDER is true. + enumerator = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false); + } + while (enumerator.hasMoreElements()) { + let win = enumerator.getNext(); + if (!win.closed && isWindowGoodForChats(win)) + topMost = win; + } + return topMost; + }, + + /** + * Adds a button to the collection of custom buttons that can be added to the + * titlebar of a chatbox. + * For the button to be visible, `Chat#loadButtonSet` has to be called with + * the new buttons' ID in the buttonSet argument. + * + * @param {Object} button Button object that may contain the following fields: + * - {String} id Button identifier. + * - {Function} [onBuild] Function that returns a valid DOM node to + * represent the button. + * - {Function} [onCommand] Callback function that is invoked when the DOM + * node is clicked. + */ + registerButton: function(button) { + if (gCustomButtons.has(button.id)) + return; + gCustomButtons.set(button.id, button); + }, + + /** + * Load a set of predefined buttons in a chatbox' titlebar. + * + * @param {XULDOMNode} chatbox Chatbox XUL element. + * @param {Set|String} buttonSet Set of buttons to show in the titlebar. This + * may be a comma-separated string or a predefined + * set object. + */ + loadButtonSet: function(chatbox, buttonSet = kDefaultButtonSet) { + if (!buttonSet) + return; + + // When the buttonSet is coming from an XML attribute, it will be a string. + if (typeof buttonSet == "string") { + buttonSet = buttonSet.split(",").map(button => button.trim()); + } + + // Make sure to keep the current set around. + chatbox.setAttribute("buttonSet", [...buttonSet].join(",")); + + let isUndocked = !chatbox.chatbar; + let document = chatbox.ownerDocument; + let titlebarNode = document.getAnonymousElementByAttribute(chatbox, "class", + "chat-titlebar"); + let buttonsSeen = new Set(); + + for (let buttonId of buttonSet) { + buttonId = buttonId.trim(); + buttonsSeen.add(buttonId); + let nodes, node; + if (kDefaultButtonSet.has(buttonId)) { + node = document.getAnonymousElementByAttribute(chatbox, "anonid", buttonId); + if (!node) + continue; + + node.hidden = isUndocked && kHiddenDefaultButtons.has(buttonId) ? true : false; + } else if (gCustomButtons.has(buttonId)) { + let button = gCustomButtons.get(buttonId); + let buttonClass = "chat-" + buttonId; + // Custom buttons are not defined in the chatbox binding, thus not + // anonymous elements. + nodes = titlebarNode.getElementsByClassName(buttonClass); + node = nodes && nodes.length ? nodes[0] : null; + if (!node) { + // Allow custom buttons to build their own button node. + if (button.onBuild) { + node = button.onBuild(chatbox); + } else { + // We can also build a normal toolbarbutton to insert. + node = document.createElementNS(kNSXUL, "toolbarbutton"); + node.classList.add(buttonClass); + node.classList.add("chat-toolbarbutton"); + } + + if (button.onCommand) { + node.addEventListener("command", e => { + button.onCommand(e, chatbox); + }); + } + titlebarNode.appendChild(node); + } + + // When the chat is undocked and the button wants to be visible then, it + // will be. + node.hidden = isUndocked && !button.visibleWhenUndocked; + } else { + Cu.reportError("Chatbox button '" + buttonId + "' could not be found!\n"); + } + } + + // Hide any button that is part of the default set, but not of the current set. + for (let button of kDefaultButtonSet) { + if (!buttonsSeen.has(button)) + document.getAnonymousElementByAttribute(chatbox, "anonid", button).hidden = true; + } + } +};
--- a/browser/modules/Social.jsm +++ b/browser/modules/Social.jsm @@ -1,15 +1,16 @@ /* 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/. */ "use strict"; -this.EXPORTED_SYMBOLS = ["Social", "OpenGraphBuilder", +this.EXPORTED_SYMBOLS = ["Social", "CreateSocialStatusWidget", + "CreateSocialMarkWidget", "OpenGraphBuilder", "DynamicResizeWatcher", "sizeSocialPanelToContent"]; const Ci = Components.interfaces; const Cc = Components.classes; const Cu = Components.utils; // The minimum sizes for the auto-resize panel code, minimum size necessary to // properly show the error page in the panel. @@ -17,25 +18,65 @@ const PANEL_MIN_HEIGHT = 190; const PANEL_MIN_WIDTH = 330; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SocialService", - "resource:///modules/SocialService.jsm"); + "resource://gre/modules/SocialService.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata", "resource://gre/modules/PageMetadata.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); +function promiseSetAnnotation(aURI, providerList) { + let deferred = Promise.defer(); + + // Delaying to catch issues with asynchronous behavior while waiting + // to implement asynchronous annotations in bug 699844. + Services.tm.mainThread.dispatch(function() { + try { + if (providerList && providerList.length > 0) { + PlacesUtils.annotations.setPageAnnotation( + aURI, "social/mark", JSON.stringify(providerList), 0, + PlacesUtils.annotations.EXPIRE_WITH_HISTORY); + } else { + PlacesUtils.annotations.removePageAnnotation(aURI, "social/mark"); + } + } catch(e) { + Cu.reportError("SocialAnnotation failed: " + e); + } + deferred.resolve(); + }, Ci.nsIThread.DISPATCH_NORMAL); + + return deferred.promise; +} + +function promiseGetAnnotation(aURI) { + let deferred = Promise.defer(); + + // Delaying to catch issues with asynchronous behavior while waiting + // to implement asynchronous annotations in bug 699844. + Services.tm.mainThread.dispatch(function() { + let val = null; + try { + val = PlacesUtils.annotations.getPageAnnotation(aURI, "social/mark"); + } catch (ex) { } + + deferred.resolve(val); + }, Ci.nsIThread.DISPATCH_NORMAL); + + return deferred.promise; +} + this.Social = { initialized: false, lastEventReceived: 0, providers: [], _disabledForSafeMode: false, init: function Social_init() { this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled; @@ -103,16 +144,21 @@ this.Social = { this.providers = providers; Services.obs.notifyObservers(null, "social:providers-changed", null); }, get enabled() { return !this._disabledForSafeMode && this.providers.length > 0; }, + toggleNotifications: function SocialNotifications_toggle() { + let prefValue = Services.prefs.getBoolPref("social.toast-notifications.enabled"); + Services.prefs.setBoolPref("social.toast-notifications.enabled", !prefValue); + }, + _getProviderFromOrigin: function (origin) { for (let p of this.providers) { if (p.origin == origin) { return p; } } return null; }, @@ -129,19 +175,149 @@ this.Social = { SocialService.uninstallProvider(origin, aCallback); }, // Activation functionality activateFromOrigin: function (origin, callback) { // It's OK if the provider has already been activated - we still get called // back with it. SocialService.enableProvider(origin, callback); + }, + + // Page Marking functionality + isURIMarked: function(origin, aURI, aCallback) { + promiseGetAnnotation(aURI).then(function(val) { + if (val) { + let providerList = JSON.parse(val); + val = providerList.indexOf(origin) >= 0; + } + aCallback(!!val); + }).then(null, Cu.reportError); + }, + + markURI: function(origin, aURI, aCallback) { + // update or set our annotation + promiseGetAnnotation(aURI).then(function(val) { + + let providerList = val ? JSON.parse(val) : []; + let marked = providerList.indexOf(origin) >= 0; + if (marked) + return; + providerList.push(origin); + // we allow marking links in a page that may not have been visited yet. + // make sure there is a history entry for the uri, then annotate it. + let place = { + uri: aURI, + visits: [{ + visitDate: Date.now() + 1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK + }] + }; + PlacesUtils.asyncHistory.updatePlaces(place, { + handleError: () => Cu.reportError("couldn't update history for socialmark annotation"), + handleResult: function () {}, + handleCompletion: function () { + promiseSetAnnotation(aURI, providerList).then(function() { + if (aCallback) + schedule(function() { aCallback(true); } ); + }).then(null, Cu.reportError); + } + }); + }).then(null, Cu.reportError); + }, + + unmarkURI: function(origin, aURI, aCallback) { + // this should not be called if this.provider or the port is null + // set our annotation + promiseGetAnnotation(aURI).then(function(val) { + let providerList = val ? JSON.parse(val) : []; + let marked = providerList.indexOf(origin) >= 0; + if (marked) { + // remove the annotation + providerList.splice(providerList.indexOf(origin), 1); + promiseSetAnnotation(aURI, providerList).then(function() { + if (aCallback) + schedule(function() { aCallback(false); } ); + }).then(null, Cu.reportError); + } + }).then(null, Cu.reportError); } }; +function schedule(callback) { + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); +} + +function CreateSocialStatusWidget(aId, aProvider) { + if (!aProvider.statusURL) + return; + let widget = CustomizableUI.getWidget(aId); + // The widget is only null if we've created then destroyed the widget. + // Once we've actually called createWidget the provider will be set to + // PROVIDER_API. + if (widget && widget.provider == CustomizableUI.PROVIDER_API) + return; + + CustomizableUI.createWidget({ + id: aId, + type: "custom", + removable: true, + defaultArea: CustomizableUI.AREA_NAVBAR, + onBuild: function(aDocument) { + let node = aDocument.createElement("toolbarbutton"); + node.id = this.id; + node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional social-status-button badged-button"); + node.style.listStyleImage = "url(" + (aProvider.icon32URL || aProvider.iconURL) + ")"; + node.setAttribute("origin", aProvider.origin); + node.setAttribute("label", aProvider.name); + node.setAttribute("tooltiptext", aProvider.name); + node.setAttribute("oncommand", "SocialStatus.showPopup(this);"); + node.setAttribute("constrain-size", "true"); + + return node; + } + }); +} + +function CreateSocialMarkWidget(aId, aProvider) { + if (!aProvider.markURL) + return; + let widget = CustomizableUI.getWidget(aId); + // The widget is only null if we've created then destroyed the widget. + // Once we've actually called createWidget the provider will be set to + // PROVIDER_API. + if (widget && widget.provider == CustomizableUI.PROVIDER_API) + return; + + CustomizableUI.createWidget({ + id: aId, + type: "custom", + removable: true, + defaultArea: CustomizableUI.AREA_NAVBAR, + onBuild: function(aDocument) { + let node = aDocument.createElement("toolbarbutton"); + node.id = this.id; + node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional social-mark-button"); + node.setAttribute("type", "socialmark"); + node.setAttribute("constrain-size", "true"); + node.style.listStyleImage = "url(" + (aProvider.unmarkedIcon || aProvider.icon32URL || aProvider.iconURL) + ")"; + node.setAttribute("origin", aProvider.origin); + + let window = aDocument.defaultView; + let menuLabel = window.gNavigatorBundle.getFormattedString("social.markpageMenu.label", [aProvider.name]); + node.setAttribute("label", menuLabel); + node.setAttribute("tooltiptext", menuLabel); + node.setAttribute("observes", "Social:PageShareOrMark"); + + return node; + } + }); +} + + function sizeSocialPanelToContent(panel, iframe, requestedSize) { let doc = iframe.contentDocument; if (!doc || !doc.body) { return; } // We need an element to use for sizing our panel. See if the body defines // an id for that element, otherwise use the body itself. let body = doc.body;
--- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -12,16 +12,17 @@ XPCSHELL_TESTS_MANIFESTS += [ EXTRA_JS_MODULES += [ 'AboutHome.jsm', 'AboutNewTab.jsm', 'BrowserUITelemetry.jsm', 'BrowserUsageTelemetry.jsm', 'CaptivePortalWatcher.jsm', 'CastingApps.jsm', + 'Chat.jsm', 'ContentClick.jsm', 'ContentCrashHandlers.jsm', 'ContentLinkHandler.jsm', 'ContentObservers.jsm', 'ContentSearch.jsm', 'ContentWebRTC.jsm', 'DirectoryLinksProvider.jsm', 'E10SUtils.jsm', @@ -37,17 +38,16 @@ EXTRA_JS_MODULES += [ 'ProcessHangMonitor.jsm', 'ReaderParent.jsm', 'RecentWindow.jsm', 'RemotePrompt.jsm', 'Sanitizer.jsm', 'SelfSupportBackend.jsm', 'SitePermissions.jsm', 'Social.jsm', - 'SocialService.jsm', 'TabGroupsMigrator.jsm', 'TransientPrefs.jsm', 'webrtcUI.jsm', ] if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': EXTRA_JS_MODULES += [ 'Windows8WindowFrameColor.jsm',
--- a/browser/modules/test/unit/social/head.js +++ b/browser/modules/test/unit/social/head.js @@ -111,100 +111,17 @@ function do_initialize_social(enabledOnS // expecting 2 providers installed do_wait_observer("social:providers-changed", function() { do_check_eq(Social.providers.length, 2, "2 providers installed"); do_execute_soon(cb); }); } // import and initialize everything - SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; + SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; do_check_eq(enabledOnStartup, SocialService.hasEnabledProviders, "Service has enabled providers"); Social = Cu.import("resource:///modules/Social.jsm", {}).Social; do_check_false(Social.initialized, "Social is not initialized"); Social.init(); do_check_true(Social.initialized, "Social is initialized"); if (!enabledOnStartup) do_execute_soon(cb); } - -function AsyncRunner() { - do_test_pending(); - do_register_cleanup(() => this.destroy()); - - this._callbacks = { - done: do_test_finished, - error: function (err) { - // xpcshell test functions like do_check_eq throw NS_ERROR_ABORT on - // failure. Ignore those so they aren't rethrown here. - if (err !== Cr.NS_ERROR_ABORT) { - if (err.stack) { - err = err + " - See following stack:\n" + err.stack + - "\nUseless do_throw stack"; - } - do_throw(err); - } - }, - consoleError: function (scriptErr) { - // Try to ensure the error is related to the test. - let filename = scriptErr.sourceName || scriptErr.toString() || ""; - if (filename.indexOf("/toolkit/components/social/") >= 0) - do_throw(scriptErr); - }, - }; - this._iteratorQueue = []; - - // This catches errors reported to the console, e.g., via Cu.reportError, but - // not on the runner's stack. - Cc["@mozilla.org/consoleservice;1"]. - getService(Ci.nsIConsoleService). - registerListener(this); -} - -AsyncRunner.prototype = { - - appendIterator: function appendIterator(iter) { - this._iteratorQueue.push(iter); - }, - - next: function next(arg) { - if (!this._iteratorQueue.length) { - this.destroy(); - this._callbacks.done(); - return; - } - - try { - var { done, value: val } = this._iteratorQueue[0].next(arg); - if (done) { - this._iteratorQueue.shift(); - this.next(); - return; - } - } - catch (err) { - this._callbacks.error(err); - } - - // val is an iterator => prepend it to the queue and start on it - // val is otherwise truthy => call next - if (val) { - if (typeof(val) != "boolean") - this._iteratorQueue.unshift(val); - this.next(); - } - }, - - destroy: function destroy() { - Cc["@mozilla.org/consoleservice;1"]. - getService(Ci.nsIConsoleService). - unregisterListener(this); - this.destroy = function alreadyDestroyed() {}; - }, - - observe: function observe(msg) { - if (msg instanceof Ci.nsIScriptError && - !(msg.flags & Ci.nsIScriptError.warningFlag)) - { - this._callbacks.consoleError(msg); - } - }, -};
--- a/browser/modules/test/unit/social/test_social.js +++ b/browser/modules/test/unit/social/test_social.js @@ -14,17 +14,17 @@ function testStartupEnabled() { // wait on startup before continuing do_check_eq(Social.providers.length, 2, "two social providers enabled"); do_check_true(Social.providers[0].enabled, "provider 0 is enabled"); do_check_true(Social.providers[1].enabled, "provider 1 is enabled"); run_next_test(); } function testDisableAfterStartup() { - let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; + let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; SocialService.disableProvider(Social.providers[0].origin, function() { do_wait_observer("social:providers-changed", function() { do_check_eq(Social.enabled, false, "Social is disabled"); do_check_eq(Social.providers.length, 0, "no social providers available"); do_test_finished(); run_next_test(); }); SocialService.disableProvider(Social.providers[0].origin)
--- a/browser/modules/test/unit/social/xpcshell.ini +++ b/browser/modules/test/unit/social/xpcshell.ini @@ -2,12 +2,8 @@ head = head.js tail = firefox-appdir = browser skip-if = toolkit == 'android' || toolkit == 'gonk' support-files = blocklist.xml [test_social.js] [test_socialDisabledStartup.js] -[test_SocialService.js] -[test_SocialServiceMigration21.js] -[test_SocialServiceMigration22.js] -[test_SocialServiceMigration29.js]
--- a/browser/themes/linux/browser.css +++ b/browser/themes/linux/browser.css @@ -1359,17 +1359,16 @@ html|span.ac-emphasize-text-url { } #reader-mode-button:hover:active, #reader-mode-button[readeractive] { -moz-image-region: rect(0, 48px, 16px, 32px); } /* social share panel */ -%include ../shared/social/social.inc.css .social-share-frame { border-top: 1px solid #f8f8f8; width: 756px; height: 150px; } #share-container { @@ -1408,16 +1407,22 @@ html|span.ac-emphasize-text-url { display: none; } .share-provider-button > .toolbarbutton-icon { width: 16px; min-height: 16px; max-height: 16px; } +/* social recommending panel */ + +#social-mark-button { + -moz-image-region: rect(0, 16px, 16px, 0); +} + /* bookmarks menu-button */ #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker { -moz-appearance: none !important; -moz-box-align: center; } #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon { @@ -1802,16 +1807,40 @@ notification.pluginVulnerable > .notific color: #FDF3DE; min-width: 16px; text-shadow: none; background-image: linear-gradient(#B4211B, #8A1915); border-radius: 1px; margin-inline-end: 2px; } +/* social toolbar provider menu */ +#social-statusarea-popup { + margin-top: 0; + margin-left: -12px; + margin-right: -12px; +} + +.social-statusarea-user { + list-style-image:url("chrome://global/skin/icons/information-32.png"); +} + +.social-statusarea-user-portrait { + width: 32px; + height: 32px; + border-radius: 2px; + margin: 10px; +} + +.social-panel > .panel-arrowcontainer > .panel-arrowcontent { + padding: 0; +} + +%include ../shared/social/chat.inc.css + /* Customization mode */ %include ../shared/customizableui/customizeMode.inc.css #main-window[customize-entered] > #tab-view-deck { background-image: url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"), linear-gradient(to bottom, #bcbcbc, #b5b5b5); background-attachment: fixed;
--- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -1987,31 +1987,34 @@ html|span.ac-emphasize-text-url { border-top: 1px solid #f8f8f8; min-width: 756px; height: 150px; /* we resize our panels dynamically, make it look nice */ } #share-container { min-width: 756px; + background-color: white; background-repeat: no-repeat; background-position: center center; } #share-container[loading] { background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png); } #share-container > browser { transition: opacity 150ms ease-in-out; opacity: 1; } #share-container[loading] > browser { opacity: 0; } -#manage-share-providers { +#manage-share-providers, +#social-sidebar-button:hover, +#social-sidebar-button:hover:active { -moz-image-region: rect(18px, 468px, 36px, 450px); } .social-share-toolbar { border-bottom: 1px solid #dedede; padding: 2px; } @@ -2030,16 +2033,22 @@ html|span.ac-emphasize-text-url { } .share-provider-button > .toolbarbutton-icon { width: 16px; min-height: 16px; max-height: 16px; } +/* social recommending panel */ + +#social-mark-button { + -moz-image-region: rect(0, 16px, 16px, 0); +} + /* BOOKMARKING PANEL */ #editBookmarkPanelStarIcon { list-style-image: url("chrome://browser/skin/places/starred48.png"); width: 48px; height: 48px; } #editBookmarkPanelStarIcon[unstarred] { @@ -2950,16 +2959,20 @@ menuitem:hover > hbox > .alltabs-endimag %include ../shared/notification-icons.inc.css .notification-anchor-icon:-moz-focusring { box-shadow: 0 0 2px 1px -moz-mac-focusring inset, 0 0 3px 2px -moz-mac-focusring; } +#social-notification-icon > .toolbarbutton-icon { + height: 16px; +} + /* Translation */ %include ../shared/translation/infobar.inc.css notification[value="translation"] { color: #484848; background-color: #EFEFEF; background-image: none; @@ -3205,18 +3218,39 @@ menulist.translate-infobar-element > .me #developer-toolbar-toolbox-button[error-count]:before { color: #FDF3DE; min-width: 16px; text-shadow: none; background-image: linear-gradient(#B4211B, #8A1915); border-radius: 1px; } -/* Share */ -%include ../shared/social/social.inc.css +/* === end of social toolbar button === */ + +/* === social toolbar provider menu === */ + +.social-statusarea-user { + list-style-image:url("chrome://global/skin/icons/information-32.png"); +} + +.social-statusarea-user-portrait { + width: 32px; + height: 32px; + margin: 4px; + margin-inline-start: 0; +} + +.social-panel > .panel-arrowcontainer > .panel-arrowcontent { + padding: 0; +} + +/* fixup rounded corners for osx panels */ +.social-panel > .social-panel-frame { + border-radius: inherit; +} #social-share-panel { min-height: 100px; min-width: 300px; transition: height .3s ease-in-out, width .3s ease-in-out; } #share-container, @@ -3232,16 +3266,20 @@ menulist.translate-infobar-element > .me border-top-right-radius: inherit; } #social-share-provider-buttons { border-top-left-radius: inherit; border-top-right-radius: inherit; } +/* === end of social toolbar provider menu === */ + +%include ../shared/social/chat.inc.css + /* Customization mode */ %include ../shared/customizableui/customizeMode.inc.css #main-window[customizing] { background-color: rgb(178,178,178); }
--- a/browser/themes/shared/jar.inc.mn +++ b/browser/themes/shared/jar.inc.mn @@ -94,16 +94,17 @@ skin/classic/browser/badge-add-engine.png (../shared/search/badge-add-engine.png) skin/classic/browser/badge-add-engine@2x.png (../shared/search/badge-add-engine@2x.png) skin/classic/browser/search-indicator-badge-add.png (../shared/search/search-indicator-badge-add.png) skin/classic/browser/search-indicator-badge-add@2x.png (../shared/search/search-indicator-badge-add@2x.png) skin/classic/browser/search-history-icon.svg (../shared/search/history-icon.svg) skin/classic/browser/search-indicator-magnifying-glass.svg (../shared/search/search-indicator-magnifying-glass.svg) skin/classic/browser/search-arrow-go.svg (../shared/search/search-arrow-go.svg) skin/classic/browser/gear.svg (../shared/search/gear.svg) + skin/classic/browser/social/chat-icons.svg (../shared/social/chat-icons.svg) skin/classic/browser/social/gear_default.png (../shared/social/gear_default.png) skin/classic/browser/social/gear_clicked.png (../shared/social/gear_clicked.png) skin/classic/browser/tabbrowser/connecting.png (../shared/tabbrowser/connecting.png) skin/classic/browser/tabbrowser/connecting@2x.png (../shared/tabbrowser/connecting@2x.png) skin/classic/browser/tabbrowser/crashed.svg (../shared/tabbrowser/crashed.svg) skin/classic/browser/tabbrowser/pendingpaint.png (../shared/tabbrowser/pendingpaint.png) * skin/classic/browser/tabbrowser/tab-audio.svg (../shared/tabbrowser/tab-audio.svg) skin/classic/browser/tabbrowser/tab-audio-small.svg (../shared/tabbrowser/tab-audio-small.svg)
new file mode 100644 --- /dev/null +++ b/browser/themes/shared/social/chat-icons.svg @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-3 -3 16 16"> + <style> + use:not(:target) { + display: none; + } + use { + fill: #666; + } + use[id$="-hover"] { + fill: #4a4a4a; + } + use[id$="-active"] { + fill: #4a4a4a; + } + use[id$="-disabled"] { + fill: #666; + } + use[id$="-white"] { + fill: #fff; + } + </style> + <defs> + <polygon id="close-shape" points="10,1.717 8.336,0.049 5.024,3.369 1.663,0 0,1.668 3.36,5.037 0.098,8.307 1.762,9.975 5.025,6.705 8.311,10 9.975,8.332 6.688,5.037"/> + <path id="dropdown-shape" fill-rule="evenodd" d="M9,3L4.984,7L1,3H9z"/> + <g id="expand-shape"> + <path fill-rule="evenodd" d="M9.429,7.072v2.143c0,0.531-0.188,0.985-0.566,1.363c-0.377,0.377-0.832,0.565-1.363,0.565H1.929 c-0.531,0-0.986-0.188-1.363-0.565C0.188,10.2,0,9.746,0,9.214V3.643c0-0.531,0.188-0.985,0.566-1.362 c0.377-0.378,0.832-0.566,1.363-0.566h4.714c0.062,0,0.114,0.021,0.154,0.061s0.06,0.092,0.06,0.154v0.428 c0,0.063-0.02,0.114-0.06,0.154S6.705,2.572,6.643,2.572H1.929c-0.295,0-0.547,0.104-0.757,0.314S0.857,3.348,0.857,3.643v5.571 c0,0.295,0.105,0.547,0.315,0.757s0.462,0.314,0.757,0.314H7.5c0.294,0,0.547-0.104,0.757-0.314 c0.209-0.21,0.314-0.462,0.314-0.757V7.072c0-0.062,0.02-0.114,0.061-0.154c0.04-0.04,0.091-0.061,0.154-0.061h0.428 c0.062,0,0.114,0.021,0.154,0.061S9.429,7.009,9.429,7.072z"/> + <path fill-rule="evenodd" d="M7.07,5.82L6.179,4.93C6.127,4.878,6.101,4.818,6.101,4.75s0.026-0.128,0.079-0.18l2.594-2.594L7.648,0.852 C7.549,0.753,7.5,0.636,7.5,0.5s0.049-0.252,0.148-0.351S7.864,0,8,0h3.5c0.136,0,0.252,0.05,0.351,0.149S12,0.365,12,0.5V4 c0,0.136-0.05,0.253-0.149,0.351C11.752,4.451,11.635,4.5,11.5,4.5c-0.136,0-0.253-0.05-0.352-0.149l-1.124-1.125L7.429,5.82 c-0.052,0.052-0.112,0.079-0.18,0.079"/> + </g> + <rect id="minimize-shape" y="7.5" width="10" height="2.2"/> + <path id="exit-shape" fill-rule="evenodd" d="M5.01905144,3.00017279 C5.01277908,3.00005776 5.0064926,3 5.00019251,3 L1.99980749,3 C1.44371665,3 1,3.44762906 1,3.99980749 L1,7.00019251 C1,7.55628335 1.44762906,8 1.99980749,8 L5.00019251,8 C5.00649341,8 5.01277988,7.99994253 5.01905144,7.99982809 L5.01905144,8.5391818 C5.01905144,10.078915 5.37554713,10.2645548 5.81530684,9.9314625 L10.8239665,6.13769619 C11.2653143,5.80340108 11.2637262,5.26455476 10.8239665,4.93146254 L5.81530684,1.13769619 C5.37395904,0.80340108 5.01905144,0.98023404 5.01905144,1.52997693 L5.01905144,3.00017279 Z M-1,1 L4,1 L4,2 L0,2 L0,9 L4,9 L4,10.0100024 L-1,10.0100021 L-1,1 Z" /> + </defs> + <use id="close" xlink:href="#close-shape"/> + <use id="close-active" xlink:href="#close-shape"/> + <use id="close-disabled" xlink:href="#close-shape"/> + <use id="close-hover" xlink:href="#close-shape"/> + <use id="exit-white" xlink:href="#exit-shape"/> + <use id="expand" xlink:href="#expand-shape"/> + <use id="expand-active" xlink:href="#expand-shape"/> + <use id="expand-disabled" xlink:href="#expand-shape"/> + <use id="expand-hover" xlink:href="#expand-shape"/> + <use id="expand-white" xlink:href="#expand-shape"/> + <use id="minimize" xlink:href="#minimize-shape"/> + <use id="minimize-active" xlink:href="#minimize-shape"/> + <use id="minimize-disabled" xlink:href="#minimize-shape"/> + <use id="minimize-hover" xlink:href="#minimize-shape"/> + <use id="minimize-white" xlink:href="#minimize-shape"/> +</svg>
new file mode 100644 --- /dev/null +++ b/browser/themes/shared/social/chat.inc.css @@ -0,0 +1,238 @@ +%if 0 +/* 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/. */ +%endif + +#social-sidebar-header { + padding: 3px; +} + +#manage-share-providers, +#social-sidebar-button { + list-style-image: url("chrome://browser/skin/Toolbar.png"); + -moz-image-region: rect(0, 468px, 18px, 450px); +} + +#social-sidebar-button { + -moz-appearance: none; + border: none; + padding: 0; + margin: 2px; +} +#manage-share-providers > .toolbarbutton-icon, +#social-sidebar-button > .toolbarbutton-icon { + min-height: 18px; + min-width: 18px; +} + +#social-sidebar-button > .toolbarbutton-menu-dropmarker { + display: none; +} + +#social-sidebar-button[loading="true"] { + list-style-image: url("chrome://global/skin/icons/loading.png"); +} + +#social-sidebar-favico { + max-height: 16px; + max-width: 16px; + padding: 0; + margin: 2px; +} + +.chat-status-icon { + max-height: 16px; + max-width: 16px; + padding: 0; +} + +.chat-toolbarbutton { + -moz-appearance: none; + border: none; + padding: 0 3px; + margin: 0; + background: none; +} + +.chat-toolbarbutton > .toolbarbutton-text { + display: none; +} + +.chat-toolbarbutton > .toolbarbutton-icon { + width: 16px; + height: 16px; +} + +.chat-close-button { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#close"); +} + +.chat-close-button:hover { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#close-hover"); +} + +.chat-close-button:hover:active { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#close-active"); +} + +.chat-minimize-button { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize"); +} + +.chat-minimize-button:hover { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-hover"); +} + +.chat-minimize-button:hover:active { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-active"); +} + +.chat-swap-button { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand"); + transform: rotate(180deg); +} + +.chat-swap-button:hover { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand-hover"); +} + +.chat-swap-button:hover:active { + list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand-active"); +} + +chatbar > chatbox > .chat-titlebar > .chat-swap-button { + transform: none; +} + +.chat-title { + color: #666; + text-shadow: none; + cursor: inherit; +} + +.chat-titlebar { + height: 26px; + min-height: 26px; + width: 100%; + margin: 0; + padding: 5px 4px; + border: 1px solid #ebebeb; + border-bottom: 0; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + cursor: pointer; + background-color: #ebebeb; +} + +.chat-titlebar[selected] { + background-color: #f0f0f0; +} + +.chat-titlebar > .notification-anchor-icon { + margin-left: 2px; + margin-right: 2px; +} + +.chat-titlebar[minimized="true"] { + border-bottom: none; +} + +.chat-titlebar[activity] { + background-image: radial-gradient(ellipse closest-side at center, rgb(255,255,255), transparent); + background-repeat: no-repeat; + background-size: 100% 20px; + background-position: 0 -10px; +} + +.chat-frame { + padding: 0; + margin: 0; + overflow: hidden; +} + +.chatbar-button { + list-style-image: url("chrome://browser/skin/social/services-16.png"); + margin: 0; + padding: 2px; + height: 21px; + width: 21px; + border: 1px solid #ccc; + border-bottom: none; + background-color: #d9d9d9; + background-image: linear-gradient(rgba(255,255,255,.43), transparent); + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +@media (min-resolution: 2dppx) { + .chatbar-button { + list-style-image: url("chrome://browser/skin/social/services-16@2x.png"); + } +} + +.chatbar-button:hover, +.chatbar-button[open="true"] { + background-color: #f0f0f0; +} + +.chatbar-button[activity]:not([open]) { + background-image: radial-gradient(circle farthest-corner at center 2px, rgb(254,254,255) 3%, rgba(210,235,255,0.9) 12%, rgba(148,205,253,0.6) 30%, rgba(148,205,253,0.2) 70%); +} + +.chatbar-button > .toolbarbutton-icon { + width: 16px; +} + +.chatbar-button > menupopup > .menuitem-iconic > .menu-iconic-left > .menu-iconic-icon { + width: auto; + height: auto; + max-height: 16px; + max-width: 16px; +} + +.chatbar-button[open="true"] { + box-shadow: inset 0 2px 5px rgba(0,0,0,0.6), 0 1px rgba(255,255,255,0.2); +} + +.chatbar-button > .toolbarbutton-text, +.chatbar-button > .toolbarbutton-menu-dropmarker { + display: none; +} + +.chatbar-button > menupopup > menuitem[activity] { + font-weight: bold; +} + +.chatbar-innerbox { + background: transparent; + overflow: hidden; +} + +chatbar { + margin-inline-end: 20px; +} + +chatbox { + margin-inline-start: 4px; + background-color: transparent; +} + +chatbar > chatbox { + /* Apply the same border-radius as the .chat-titlebar to make the box-shadow + go round nicely. */ + border-top-left-radius: 4px; + border-top-right-radius: 4px; + box-shadow: 0 0 5px rgba(0,0,0,.3); + /* Offset the chatbox the same amount as the box-shadows' spread, to make it + visible. */ + margin-inline-end: 5px; +} + +window > chatbox { + margin-inline-start: 0px; + margin: 0px; + border: none; + padding: 0px; + border-radius: 4px; +}
deleted file mode 100644 --- a/browser/themes/shared/social/social.inc.css +++ /dev/null @@ -1,23 +0,0 @@ -%if 0 -/* 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/. */ -%endif - -#manage-share-providers { - list-style-image: url("chrome://browser/skin/Toolbar.png"); - -moz-image-region: rect(0, 468px, 18px, 450px); -} - -#manage-share-providers > .toolbarbutton-icon { - min-height: 18px; - min-width: 18px; -} - -.social-panel > .panel-arrowcontainer > .panel-arrowcontent { - padding: 0; -} -/* fixup corners for share panel */ -.social-panel > .social-panel-frame { - border-radius: inherit; -}
--- a/browser/themes/windows/browser-aero.css +++ b/browser/themes/windows/browser-aero.css @@ -49,17 +49,19 @@ @media (-moz-os-version: windows-vista), (-moz-os-version: windows-win7) { .sidebar-header:not(:-moz-lwtheme), #sidebar-header:not(:-moz-lwtheme) { background-color: #EEF3FA; } .sidebar-splitter, - #appcontent ~ .sidebar-splitter { + #appcontent ~ .sidebar-splitter, + .chatbar-button, + chatbar > chatbox { border-color: #A9B7C9; } #navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar):not(:-moz-lwtheme), #browser-bottombox:not(:-moz-lwtheme), .browserContainer > findbar { background-color: @customToolbarColor@; }
--- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -1799,21 +1799,16 @@ html|span.ac-emphasize-text-url { } #reader-mode-button:hover:active, #reader-mode-button[readeractive] { -moz-image-region: rect(0, 48px, 16px, 32px); } /* social share panel */ -%include ../shared/social/social.inc.css - -.social-panel-frame { - border-radius: inherit; -} .social-share-frame { min-width: 756px; height: 150px; } #share-container { min-width: 756px; background-color: white; @@ -1850,16 +1845,22 @@ html|span.ac-emphasize-text-url { display: none; } .share-provider-button > .toolbarbutton-icon { width: 16px; min-height: 16px; max-height: 16px; } + +/* fixup corners for share panel */ +.social-panel > .social-panel-frame { + border-radius: inherit; +} + #social-share-panel { min-height: 100px; min-width: 766px; } #share-container, .social-share-frame { border-top-left-radius: 0; @@ -1873,16 +1874,22 @@ html|span.ac-emphasize-text-url { border-top-right-radius: inherit; } #social-share-provider-buttons { border-top-left-radius: inherit; border-top-right-radius: inherit; } +/* social recommending panel */ + +#social-mark-button { + -moz-image-region: rect(0, 16px, 16px, 0); +} + /* bookmarks menu-button */ #nav-bar #bookmarks-menu-button[cui-areatype="toolbar"]:not([overflowedItem=true]) > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon { padding-top: var(--toolbarbutton-vertical-inner-padding); padding-bottom: var(--toolbarbutton-vertical-inner-padding); } #BMB_bookmarksPopup[side="top"], @@ -2452,16 +2459,63 @@ notification.pluginVulnerable > .notific color: #FDF3DE; min-width: 16px; text-shadow: none; background-image: linear-gradient(#B4211B, #8A1915); border-radius: 1px; margin-inline-end: 5px; } +/* social toolbar provider menu */ +.social-statusarea-popup { + margin-top: 0; + margin-left: -12px; + margin-right: -12px; +} + +.social-statusarea-user { + -moz-appearance: none; + border-bottom: 1px solid rgb(221,221,221); + background-color: -moz-Dialog; + position: relative; + cursor: pointer; + list-style-image:url("chrome://global/skin/icons/information-32.png"); +} + +.social-statusarea-user-portrait { + width: 32px; + height: 32px; + border-radius: 2px; + margin: 10px; +} + +.social-statusarea-loggedInStatus { + -moz-appearance: none; + background: transparent; + border: none; + color: -moz-nativehyperlinktext; + min-width: 0; + margin: 0 6px; + list-style-image: none; +} + +.social-statusarea-user[_moz-menuactive] > vbox > .social-statusarea-loggedInStatus { + text-decoration: underline; +} + +.social-panel > .panel-arrowcontainer > .panel-arrowcontent { + padding: 0; +} + +.social-panel-frame { + border-radius: inherit; +} + +%include ../shared/social/chat.inc.css + /* Customization mode */ %include ../shared/customizableui/customizeMode.inc.css /** * This next rule is a hack to disable subpixel anti-aliasing on all * labels during the customize mode transition. Subpixel anti-aliasing * on Windows with Direct2D layers acceleration is particularly slow to
--- a/testing/mochitest/browser-test.js +++ b/testing/mochitest/browser-test.js @@ -609,17 +609,24 @@ Tester.prototype = { // This will prevent false positives for tests that were the last // to touch the sidebar. They will thus not be blamed for leaking // a document. let sidebar = document.getElementById("sidebar"); sidebar.setAttribute("src", "data:text/html;charset=utf-8,"); sidebar.docShell.createAboutBlankContentViewer(null); sidebar.setAttribute("src", "about:blank"); + // Do the same for the social sidebar. + let socialSidebar = document.getElementById("social-sidebar-browser"); + socialSidebar.setAttribute("src", "data:text/html;charset=utf-8,"); + socialSidebar.docShell.createAboutBlankContentViewer(null); + socialSidebar.setAttribute("src", "about:blank"); + SelfSupportBackend.uninit(); + SocialFlyout.unload(); SocialShare.uninit(); } // Destroy BackgroundPageThumbs resources. let {BackgroundPageThumbs} = Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm", {}); BackgroundPageThumbs._destroy();
--- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -70,16 +70,19 @@ if CONFIG['MOZ_BUILD_APP'] != 'mobile/an DIRS += ['narrate', 'viewsource']; if CONFIG['NS_PRINTING']: DIRS += ['printing'] if CONFIG['MOZ_CRASHREPORTER']: DIRS += ['crashes'] +if CONFIG['MOZ_SOCIAL']: + DIRS += ['social'] + if CONFIG['BUILD_CTYPES']: DIRS += ['ctypes'] if CONFIG['MOZ_FEEDS']: DIRS += ['feeds'] if CONFIG['MOZ_XUL']: DIRS += ['autocomplete', 'satchel']
new file mode 100644 --- /dev/null +++ b/toolkit/components/social/MozSocialAPI.jsm @@ -0,0 +1,298 @@ +/* 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/. */ + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SocialService", "resource://gre/modules/SocialService.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Social", "resource:///modules/Social.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Chat", "resource:///modules/Chat.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); + +this.EXPORTED_SYMBOLS = [ + "MozSocialAPI", "openChatWindow", "findChromeWindowForChats", "closeAllChatWindows", + "hookWindowCloseForPanelClose" +]; + +this.MozSocialAPI = { + _enabled: false, + _everEnabled: false, + set enabled(val) { + let enable = !!val; + if (enable == this._enabled) { + return; + } + this._enabled = enable; + + if (enable) { + Services.obs.addObserver(injectController, "document-element-inserted", false); + + if (!this._everEnabled) { + this._everEnabled = true; + Services.telemetry.getHistogramById("SOCIAL_ENABLED_ON_SESSION").add(true); + } + + } else { + Services.obs.removeObserver(injectController, "document-element-inserted"); + } + } +}; + +// Called on document-element-inserted, checks that the API should be injected, +// and then calls attachToWindow as appropriate +function injectController(doc, topic, data) { + try { + let window = doc.defaultView; + if (!window || PrivateBrowsingUtils.isContentWindowPrivate(window)) + return; + + // Do not attempt to load the API into about: error pages + if (doc.documentURIObject.scheme == "about") { + return; + } + + let containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + // limit injecting into social panels or same-origin browser tabs if + // social.debug.injectIntoTabs is enabled + let allowTabs = false; + try { + allowTabs = containingBrowser.contentWindow == window && + Services.prefs.getBoolPref("social.debug.injectIntoTabs"); + } catch(e) {} + + let origin = containingBrowser.getAttribute("origin"); + if (!allowTabs && !origin) { + return; + } + + // we always handle window.close on social content, even if they are not + // "enabled". + hookWindowCloseForPanelClose(window); + + SocialService.getProvider(doc.nodePrincipal.origin, function(provider) { + if (provider && provider.enabled) { + attachToWindow(provider, window); + } + }); + } catch(e) { + Cu.reportError("MozSocialAPI injectController: unable to attachToWindow for " + doc.location + ": " + e); + } +} + +// Loads mozSocial support functions associated with provider into targetWindow +function attachToWindow(provider, targetWindow) { + // If the loaded document isn't from the provider's origin (or a protocol + // that inherits the principal), don't attach the mozSocial API. + let targetDocURI = targetWindow.document.documentURIObject; + if (!provider.isSameOrigin(targetDocURI)) { + let msg = "MozSocialAPI: not attaching mozSocial API for " + provider.origin + + " to " + targetDocURI.spec + " since origins differ." + Services.console.logStringMessage(msg); + return; + } + + let mozSocialObj = { + openChatWindow: { + enumerable: true, + configurable: true, + writable: true, + value: function(toURL, callback) { + let url = targetWindow.document.documentURIObject.resolve(toURL); + let dwu = getChromeWindow(targetWindow) + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + openChatWindow(targetWindow, provider, url, chatWin => { + if (chatWin && dwu.isHandlingUserInput) + chatWin.focus(); + if (callback) + callback(!!chatWin); + }); + } + }, + openPanel: { + enumerable: true, + configurable: true, + writable: true, + value: function(toURL, offset, callback) { + let chromeWindow = getChromeWindow(targetWindow); + if (!chromeWindow.SocialFlyout) + return; + let url = targetWindow.document.documentURIObject.resolve(toURL); + if (!provider.isSameOrigin(url)) + return; + chromeWindow.SocialFlyout.open(url, offset, callback); + } + }, + closePanel: { + enumerable: true, + configurable: true, + writable: true, + value: function(toURL, offset, callback) { + let chromeWindow = getChromeWindow(targetWindow); + if (!chromeWindow.SocialFlyout || !chromeWindow.SocialFlyout.panel) + return; + chromeWindow.SocialFlyout.panel.hidePopup(); + } + }, + // allow a provider to share to other providers through the browser + share: { + enumerable: true, + configurable: true, + writable: true, + value: function(data) { + let chromeWindow = getChromeWindow(targetWindow); + if (!chromeWindow.SocialShare || chromeWindow.SocialShare.shareButton.hidden) + throw new Error("Share is unavailable"); + // ensure user action initates the share + let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + if (!dwu.isHandlingUserInput) + throw new Error("Attempt to share without user input"); + + // limit to a few params we want to support for now + let dataOut = {}; + for (let sub of ["url", "title", "description", "source"]) { + dataOut[sub] = data[sub]; + } + if (data.image) + dataOut.previews = [data.image]; + + chromeWindow.SocialShare.sharePage(null, dataOut); + } + }, + getAttention: { + enumerable: true, + configurable: true, + writable: true, + value: function() { + getChromeWindow(targetWindow).getAttention(); + } + }, + isVisible: { + enumerable: true, + configurable: true, + get: function() { + return targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell).isActive; + } + } + }; + + let contentObj = Cu.createObjectIn(targetWindow); + Object.defineProperties(contentObj, mozSocialObj); + Cu.makeObjectPropsNormal(contentObj); + + targetWindow.navigator.wrappedJSObject.__defineGetter__("mozSocial", function() { + // We do this in a getter, so that we create these objects + // only on demand (this is a potential concern, since + // otherwise we might add one per iframe, and keep them + // alive for as long as the window is alive). + delete targetWindow.navigator.wrappedJSObject.mozSocial; + return targetWindow.navigator.wrappedJSObject.mozSocial = contentObj; + }); +} + +function hookWindowCloseForPanelClose(targetWindow) { + let _mozSocialDOMWindowClose; + + if ("messageManager" in targetWindow) { + let _mozSocialSwapped; + let mm = targetWindow.messageManager; + mm.sendAsyncMessage("Social:HookWindowCloseForPanelClose"); + mm.addMessageListener("Social:DOMWindowClose", _mozSocialDOMWindowClose = function() { + targetWindow.removeEventListener("SwapDocShells", _mozSocialSwapped); + closePanel(targetWindow); + }); + + targetWindow.addEventListener("SwapDocShells", _mozSocialSwapped = function(ev) { + targetWindow.removeEventListener("SwapDocShells", _mozSocialSwapped); + + targetWindow = ev.detail; + targetWindow.messageManager.addMessageListener("Social:DOMWindowClose", _mozSocialDOMWindowClose); + }); + return; + } + + // We allow window.close() to close the panel, so add an event handler for + // this, then cancel the event (so the window itself doesn't die) and + // close the panel instead. + // However, this is typically affected by the dom.allow_scripts_to_close_windows + // preference, but we can avoid that check by setting a flag on the window. + let dwu = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + dwu.allowScriptsToClose(); + + targetWindow.addEventListener("DOMWindowClose", _mozSocialDOMWindowClose = function(evt) { + let elt = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + closePanel(elt); + // preventDefault stops the default window.close() function being called, + // which doesn't actually close anything but causes things to get into + // a bad state (an internal 'closed' flag is set and debug builds start + // asserting as the window is used.). + // None of the windows we inject this API into are suitable for this + // default close behaviour, so even if we took no action above, we avoid + // the default close from doing anything. + evt.preventDefault(); + }, true); +} + +function closePanel(elt) { + while (elt) { + if (elt.localName == "panel") { + elt.hidePopup(); + break; + } else if (elt.localName == "chatbox") { + elt.close(); + break; + } + elt = elt.parentNode; + } +} + +function schedule(callback) { + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); +} + +function getChromeWindow(contentWin) { + return contentWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); +} + +this.openChatWindow = + function openChatWindow(contentWindow, provider, url, callback, mode) { + let fullURI = provider.resolveUri(url); + if (!provider.isSameOrigin(fullURI)) { + Cu.reportError("Failed to open a social chat window - the requested URL is not the same origin as the provider."); + return; + } + + let chatbox = Chat.open(contentWindow, { + origin: provider.origin, + title: provider.name, + url: fullURI.spec, + mode: mode + }); + if (callback) { + chatbox.promiseChatLoaded.then(() => { + callback(chatbox); + }); + } +} + +this.closeAllChatWindows = function closeAllChatWindows(provider) { + return Chat.closeAll(provider.origin); +}
rename from browser/modules/SocialService.jsm rename to toolkit/components/social/SocialService.jsm --- a/browser/modules/SocialService.jsm +++ b/toolkit/components/social/SocialService.jsm @@ -11,16 +11,18 @@ Cu.import("resource://gre/modules/XPCOMU Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/PlacesUtils.jsm"); const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; const ADDON_TYPE_SERVICE = "service"; const ID_SUFFIX = "@services.mozilla.org"; const STRING_TYPE_NAME = "type.%ID%.name"; +XPCOMUtils.defineLazyModuleGetter(this, "MozSocialAPI", "resource://gre/modules/MozSocialAPI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "closeAllChatWindows", "resource://gre/modules/MozSocialAPI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "etld", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"); /** * The SocialService is the public API to social providers - it tracks which @@ -134,16 +136,17 @@ var SocialServiceInternal = { XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () { initService(); let providers = {}; for (let manifest of this.manifests) { try { if (ActiveProviders.has(manifest.origin)) { // enable the api when a provider is enabled + MozSocialAPI.enabled = true; let provider = new SocialProvider(manifest); providers[provider.origin] = provider; } } catch (err) { Cu.reportError("SocialService: failed to load provider: " + manifest.origin + ", exception: " + err); } } @@ -396,16 +399,17 @@ this.SocialService = { }, // Adds a provider given a manifest, and returns the added provider. addProvider: function addProvider(manifest, onDone) { if (SocialServiceInternal.providers[manifest.origin]) throw new Error("SocialService.addProvider: provider with this origin already exists"); // enable the api when a provider is enabled + MozSocialAPI.enabled = true; let provider = new SocialP