bug 891219 new social bookmarks, part 2, new implementation, r=markh

This commit is contained in:
Shane Caraveo 2013-09-06 10:56:01 -07:00
parent 967bf9736a
commit fbbdb70eaf
19 changed files with 1009 additions and 169 deletions

View File

@ -45,6 +45,10 @@
label="&saveLinkCmd.label;" label="&saveLinkCmd.label;"
accesskey="&saveLinkCmd.accesskey;" accesskey="&saveLinkCmd.accesskey;"
oncommand="gContextMenu.saveLink();"/> oncommand="gContextMenu.saveLink();"/>
<menu id="context-marklinkMenu" label="&social.marklinkMenu.label;"
accesskey="&social.marklink.accesskey;">
<menupopup/>
</menu>
<menuitem id="context-copyemail" <menuitem id="context-copyemail"
label="&copyEmailCmd.label;" label="&copyEmailCmd.label;"
accesskey="&copyEmailCmd.accesskey;" accesskey="&copyEmailCmd.accesskey;"
@ -247,6 +251,10 @@
label="&savePageCmd.label;" label="&savePageCmd.label;"
accesskey="&savePageCmd.accesskey2;" accesskey="&savePageCmd.accesskey2;"
oncommand="gContextMenu.savePageAs();"/> oncommand="gContextMenu.savePageAs();"/>
<menu id="context-markpageMenu" label="&social.markpageMenu.label;"
accesskey="&social.markpage.accesskey;">
<menupopup/>
</menu>
<menuseparator id="context-sep-viewbgimage"/> <menuseparator id="context-sep-viewbgimage"/>
<menuitem id="context-viewbgimage" <menuitem id="context-viewbgimage"
label="&viewBGImageCmd.label;" label="&viewBGImageCmd.label;"

View File

@ -6,6 +6,7 @@
let SocialUI, let SocialUI,
SocialChatBar, SocialChatBar,
SocialFlyout, SocialFlyout,
SocialMarks,
SocialShare, SocialShare,
SocialMenu, SocialMenu,
SocialToolbar, SocialToolbar,
@ -26,6 +27,18 @@ XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() {
return tmp.OpenGraphBuilder; 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;
});
SocialUI = { SocialUI = {
// Called on delayed startup to initialize the UI // Called on delayed startup to initialize the UI
init: function SocialUI_init() { init: function SocialUI_init() {
@ -44,6 +57,10 @@ SocialUI = {
Services.prefs.addObserver("social.toast-notifications.enabled", this, false); Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true); gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true);
window.addEventListener("aftercustomization", function() {
if (SocialUI.enabled)
SocialMarks.populateContextMenu(SocialMarks);
}, false);
if (!Social.initialized) { if (!Social.initialized) {
Social.init(); Social.init();
@ -82,15 +99,19 @@ SocialUI = {
try { try {
switch (topic) { switch (topic) {
case "social:provider-installed": case "social:provider-installed":
SocialMarks.setPosition(data);
SocialStatus.setPosition(data); SocialStatus.setPosition(data);
break; break;
case "social:provider-uninstalled": case "social:provider-uninstalled":
SocialMarks.removePosition(data);
SocialStatus.removePosition(data); SocialStatus.removePosition(data);
break; break;
case "social:provider-enabled": case "social:provider-enabled":
SocialMarks.populateToolbarPalette();
SocialStatus.populateToolbarPalette(); SocialStatus.populateToolbarPalette();
break; break;
case "social:provider-disabled": case "social:provider-disabled":
SocialMarks.removeProvider(data);
SocialStatus.removeProvider(data); SocialStatus.removeProvider(data);
break; break;
case "social:provider-reload": case "social:provider-reload":
@ -115,6 +136,7 @@ SocialUI = {
SocialSidebar.update(); SocialSidebar.update();
SocialToolbar.update(); SocialToolbar.update();
SocialStatus.populateToolbarPalette(); SocialStatus.populateToolbarPalette();
SocialMarks.populateToolbarPalette();
SocialMenu.populate(); SocialMenu.populate();
break; break;
case "social:providers-changed": case "social:providers-changed":
@ -124,6 +146,7 @@ SocialUI = {
SocialToolbar.populateProviderMenus(); SocialToolbar.populateProviderMenus();
SocialShare.populateProviderMenu(); SocialShare.populateProviderMenu();
SocialStatus.populateToolbarPalette(); SocialStatus.populateToolbarPalette();
SocialMarks.populateToolbarPalette();
break; break;
// Provider-specific notifications // Provider-specific notifications
@ -137,6 +160,7 @@ SocialUI = {
case "social:profile-changed": case "social:profile-changed":
if (this._matchesCurrentProvider(data)) { if (this._matchesCurrentProvider(data)) {
SocialToolbar.updateProvider(); SocialToolbar.updateProvider();
SocialMarks.update();
SocialChatBar.update(); SocialChatBar.update();
} }
break; break;
@ -363,6 +387,14 @@ SocialUI = {
return !!Social.provider; return !!Social.provider;
}, },
// called on tab/urlbar/location changes and after customization. Update
// anything that is tab specific.
updateState: function() {
if (!this.enabled)
return;
SocialMarks.update();
SocialShare.update();
}
} }
SocialChatBar = { SocialChatBar = {
@ -403,63 +435,6 @@ SocialChatBar = {
} }
} }
function sizeSocialPanelToContent(panel, iframe) {
// FIXME: bug 764787: Maybe we can use nsIDOMWindowUtils.getRootBounds() here?
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;
let bodyId = body.getAttribute("contentid");
if (bodyId) {
body = doc.getElementById(bodyId) || doc.body;
}
// offsetHeight/Width don't include margins, so account for that.
let cs = doc.defaultView.getComputedStyle(body);
let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom);
let height = Math.max(computedHeight, PANEL_MIN_HEIGHT);
let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight);
let width = Math.max(computedWidth, PANEL_MIN_WIDTH);
iframe.style.width = width + "px";
iframe.style.height = height + "px";
// since we do not use panel.sizeTo, we need to adjust the arrow ourselves
if (panel.state == "open")
panel.adjustArrowPosition();
}
function DynamicResizeWatcher() {
this._mutationObserver = null;
}
DynamicResizeWatcher.prototype = {
start: function DynamicResizeWatcher_start(panel, iframe) {
this.stop(); // just in case...
let doc = iframe.contentDocument;
this._mutationObserver = new iframe.contentWindow.MutationObserver(function(mutations) {
sizeSocialPanelToContent(panel, iframe);
});
// Observe anything that causes the size to change.
let config = {attributes: true, characterData: true, childList: true, subtree: true};
this._mutationObserver.observe(doc, config);
// and since this may be setup after the load event has fired we do an
// initial resize now.
sizeSocialPanelToContent(panel, iframe);
},
stop: function DynamicResizeWatcher_stop() {
if (this._mutationObserver) {
try {
this._mutationObserver.disconnect();
} catch (ex) {
// may get "TypeError: can't access dead object" which seems strange,
// but doesn't seem to indicate a real problem, so ignore it...
}
this._mutationObserver = null;
}
}
}
SocialFlyout = { SocialFlyout = {
get panel() { get panel() {
return document.getElementById("social-flyout-panel"); return document.getElementById("social-flyout-panel");
@ -765,7 +740,7 @@ SocialShare = {
} }
this.currentShare = pageData; this.currentShare = pageData;
let shareEndpoint = this._generateShareEndpointURL(provider.shareURL, pageData); let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData);
this._dynamicResizer = new DynamicResizeWatcher(); this._dynamicResizer = new DynamicResizeWatcher();
// if we've already loaded this provider/page share endpoint, we don't want // if we've already loaded this provider/page share endpoint, we don't want
@ -810,44 +785,6 @@ SocialShare = {
document.getAnonymousElementByAttribute(this.shareButton, "class", "toolbarbutton-icon"); document.getAnonymousElementByAttribute(this.shareButton, "class", "toolbarbutton-icon");
this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
Social.setErrorListener(iframe, this.setErrorMessage.bind(this)); Social.setErrorListener(iframe, this.setErrorMessage.bind(this));
},
_generateShareEndpointURL: function(shareURL, pageData) {
// support for existing share endpoints by supporting their querystring
// arguments. parse the query string template and do replacements where
// necessary the query names may be different than ours, so we could see
// u=%{url} or url=%{url}
let [shareEndpoint, queryString] = shareURL.split("?");
let query = {};
if (queryString) {
queryString.split('&').forEach(function (val) {
let [name, value] = val.split('=');
let p = /%\{(.+)\}/.exec(value);
if (!p) {
// preserve non-template query vars
query[name] = value;
} else if (pageData[p[1]]) {
query[name] = pageData[p[1]];
} else if (p[1] == "body") {
// build a body for emailers
let body = "";
if (pageData.title)
body += pageData.title + "\n\n";
if (pageData.description)
body += pageData.description + "\n\n";
if (pageData.text)
body += pageData.text + "\n\n";
body += pageData.url;
query["body"] = body;
}
});
}
var str = [];
for (let p in query)
str.push(p + "=" + encodeURIComponent(query[p]));
if (str.length)
shareEndpoint = shareEndpoint + "?" + str.join("&");
return shareEndpoint;
} }
}; };
@ -1450,7 +1387,7 @@ ToolbarHelper.prototype = {
for (let provider of Social.providers) { for (let provider of Social.providers) {
let id = this.idFromOrgin(provider.origin); let id = this.idFromOrgin(provider.origin);
if (this._getExistingButton(id)) if (this._getExistingButton(id))
return; continue;
let button = this._createButton(provider); let button = this._createButton(provider);
if (button && persistedById.hasOwnProperty(id)) { if (button && persistedById.hasOwnProperty(id)) {
let parent = persistedById[id]; let parent = persistedById[id];
@ -1684,4 +1621,130 @@ SocialStatus = {
}; };
/**
* SocialMarks
*
* Handles updates to toolbox and signals all buttons to update when necessary.
*/
SocialMarks = {
update: function() {
// signal each button to update itself
let currentButtons = document.querySelectorAll('toolbarbutton[type="socialmark"]');
for (let elt of currentButtons)
elt.update();
},
getProviders: function() {
// only rely on providers that the user has placed in the UI somewhere. This
// also means that populateToolbarPalette must be called prior to using this
// method, otherwise you get a big fat zero. For our use case with context
// menu's, this is ok.
let tbh = this._toolbarHelper;
return [p for (p of Social.providers) if (p.markURL &&
document.getElementById(tbh.idFromOrgin(p.origin)))];
},
populateContextMenu: function() {
// only show a selection if enabled and there is more than one
let providers = this.getProviders();
// remove all previous entries by class
let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))];
[m.parentNode.removeChild(m) for (m of menus)];
let contextMenus = [
{
type: "link",
id: "context-marklinkMenu",
label: "social.marklink.label"
},
{
type: "page",
id: "context-markpageMenu",
label: "social.markpage.label"
}
];
for (let cfg of contextMenus) {
this._populateContextPopup(cfg, providers);
}
},
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();
},
setPosition: function(origin) {
// this is called during install, before the provider is enabled so we have
// to use the manifest rather than the provider instance as we do elsewhere.
let manifest = Social.getManifestByOrigin(origin);
if (!manifest.markURL)
return;
let tbh = this._toolbarHelper;
tbh.setPersistentPosition(tbh.idFromOrgin(origin));
},
removePosition: function(origin) {
let tbh = this._toolbarHelper;
tbh.removePersistence(tbh.idFromOrgin(origin));
},
removeProvider: function(origin) {
this._toolbarHelper.removeProviderButton(origin);
},
get _toolbarHelper() {
delete this._toolbarHelper;
this._toolbarHelper = new ToolbarHelper("social-mark-button", this._createButton.bind(this));
return this._toolbarHelper;
},
_createButton: function(provider) {
if (!provider.markURL)
return null;
let palette = document.getElementById("navigator-toolbox").palette;
let button = document.createElement("toolbarbutton");
button.setAttribute("type", "socialmark");
button.setAttribute("class", "toolbarbutton-1 social-mark-button");
button.style.listStyleImage = "url(" + provider.iconURL + ")";
button.setAttribute("origin", provider.origin);
button.setAttribute("id", this._toolbarHelper.idFromOrgin(provider.origin));
palette.appendChild(button);
return button
},
markLink: function(aOrigin, aUrl) {
// find the button for this provider, and open it
let id = this._toolbarHelper.idFromOrgin(aOrigin);
document.getElementById(id).markLink(aUrl);
}
};
})(); })();

View File

@ -656,6 +656,14 @@ toolbarbutton[type="badged"] {
-moz-binding: url("chrome://browser/content/urlbarBindings.xml#toolbarbutton-badged"); -moz-binding: url("chrome://browser/content/urlbarBindings.xml#toolbarbutton-badged");
} }
toolbarbutton[type="socialmark"] {
-moz-binding: url("chrome://browser/content/socialmarks.xml#toolbarbutton-marks");
}
toolbarbutton[type="socialmark"] > .toolbarbutton-icon {
width: 16px;
height: 16px;
}
/* Note the chatbox 'width' values are duplicated in socialchat.xml */ /* Note the chatbox 'width' values are duplicated in socialchat.xml */
chatbox { chatbox {
-moz-binding: url("chrome://browser/content/socialchat.xml#chatbox"); -moz-binding: url("chrome://browser/content/socialchat.xml#chatbox");

View File

@ -3445,7 +3445,7 @@ function BrowserToolboxCustomizeDone(aToolboxChanged) {
URLBarSetURI(); URLBarSetURI();
XULBrowserWindow.asyncUpdateUI(); XULBrowserWindow.asyncUpdateUI();
BookmarkingUI.updateStarState(); BookmarkingUI.updateStarState();
SocialShare.update(); SocialUI.updateState();
} }
TabsInTitlebar.allowedBy("customizing-toolbars", true); TabsInTitlebar.allowedBy("customizing-toolbars", true);
@ -3913,9 +3913,7 @@ var XULBrowserWindow = {
// Update starring UI // Update starring UI
BookmarkingUI.updateStarState(); BookmarkingUI.updateStarState();
if (SocialUI.enabled) { SocialUI.updateState();
SocialShare.update();
}
} }
// Show or hide browser chrome based on the whitelist // Show or hide browser chrome based on the whitelist

View File

@ -309,6 +309,24 @@ nsContextMenu.prototype = {
this.showItem("context-bidi-page-direction-toggle", this.showItem("context-bidi-page-direction-toggle",
!this.onTextInput && top.gBidiUI); !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");
[m.hidden = !enablePageMarkItems for (m of linkmenus)];
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");
[m.hidden = !enableLinkMarkItems for (m of linkmenus)];
// SocialShare // SocialShare
let shareButton = SocialShare.shareButton; let shareButton = SocialShare.shareButton;
let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial; let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
@ -1571,6 +1589,10 @@ nsContextMenu.prototype = {
}, window.top); }, window.top);
} }
}, },
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);
},
shareLink: function CM_shareLink() { shareLink: function CM_shareLink() {
SocialShare.sharePage(null, { url: this.linkURI.spec }); SocialShare.sharePage(null, { url: this.linkURI.spec });
}, },

View File

@ -0,0 +1,236 @@
<?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 disabled="true">
<xul:panel anonid="panel" hidden="true" type="arrow" class="social-panel">
<xul:iframe type="content" flex="1" tooltip="aHTMLTooltip"
class="social-panel-frame" context="contentAreaContextMenu"
xbl:inherits="src,origin"/>
</xul: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"/>
</content>
<implementation implements="nsIDOMEventListener, nsIObserver">
<field name="panel" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "panel");
</field>
<field name="content" readonly="true">
this.panel.firstChild;
</field>
<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>
this._isMarked = val;
let provider = this.provider;
// we cannot size the image when we apply it via listStyleImage, so
// use the toolbar image
if (val)
this.setAttribute("image", provider.unmarkedIcon || provider.iconURL);
else
this.setAttribute("image", provider.markedIcon || provider.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.setAttribute("src", "about:blank");
// do we have a savable page loaded?
let aURI = gBrowser.currentURI;
this.disabled = !aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https'));
if (this.disabled) {
this.isMarked = false;
} else {
Social.isURIMarked(provider.origin, aURI, (isMarked) => {
this.isMarked = isMarked;
});
}
this.setAttribute("label", provider.name);
this.setAttribute("tooltiptext", provider.name);
this.setAttribute("origin", provider.origin);
this.panel.hidePopup();
this.panel.hidden = true;
this.pageData = null;
]]></body>
</method>
<method name="loadPanel">
<parameter name="pageData"/>
<body><![CDATA[
let provider = this.provider;
this.panel.hidden = false;
let URLTemplate = provider.markURL;
this.pageData = pageData || OpenGraphBuilder.getData(gBrowser);
let endpoint = OpenGraphBuilder.generateEndpointURL(URLTemplate, this.pageData);
// setup listeners
this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
if (event.target != this.contentDocument)
return;
this._loading = false;
this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true);
// add our resizer after the dom is ready
let DynamicResizeWatcher = Cu.import("resource:///modules/Social.jsm", {}).DynamicResizeWatcher;
this._dynamicResizer = new DynamicResizeWatcher();
this._dynamicResizer.start(this.panel, this.content);
// send the opengraph data
let evt = this.contentDocument.createEvent("CustomEvent");
evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(this.pageData));
this.contentDocument.documentElement.dispatchEvent(evt);
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;
let uri = Services.io.newURI(this.pageData.url, null, null);
if (this.isMarked) {
Social.markURI(provider.origin, uri);
} else {
Social.unmarkURI(provider.origin, uri, () => {
this.update();
});
}
}.bind(this);
contentWindow.addEventListener("socialMarkUpdate", markUpdate);
contentWindow.addEventListener("unload", function unload() {
contentWindow.removeEventListener("unload", unload);
contentWindow.removeEventListener("socialMarkUpdate", markUpdate);
});
}, true);
this._loading = true;
this.setAttribute("src", endpoint);
]]></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.getAttribute("src");
if (!src || src == "about:blank") {
this.loadPanel();
this.isMarked = true;
}
if (openPanel)
this.panel.openPopup(this, "bottomcenter topright", 0, 0, false, false);
]]></body>
</method>
<method name="markLink">
<parameter name="aUrl"/>
<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.setAttribute("src", "about:blank");
this.loadPanel({ url: aUrl });
this.isMarked = true;
this.panel.openPopup(this, "bottomcenter topright", 0, 0, false, false);
this.panel.addEventListener("popuphidden", function _hidden() {
this.panel.removeEventListener("popuphidden", _hidden);
this.update();
}.bind(this), false);
]]></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>
</implementation>
<handlers>
<handler event="popupshown"><![CDATA[
// because the panel may be preloaded, we need to size the panel when
// showing as well as after load
let sizeSocialPanelToContent = Cu.import("resource:///modules/Social.jsm", {}).sizeSocialPanelToContent;
if (!this._loading && this.contentDocument.readyState == "complete") {
this.dispatchPanelEvent("socialFrameShow");
sizeSocialPanelToContent(this.panel, this.content);
} else {
this.content.addEventListener("load", function panelBrowserOnload(e) {
this.content.removeEventListener("load", panelBrowserOnload, true);
this.dispatchPanelEvent("socialFrameShow");
sizeSocialPanelToContent(this.panel, this.content);
}.bind(this), true);
}
]]></handler>
<handler event="popuphidden"><![CDATA[
this.dispatchPanelEvent("socialFrameHide");
]]></handler>
<handler event="command"><![CDATA[
this.markCurrentPage();
]]></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 uri = DOMLinkHandler.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);
]]></handler>
</handlers>
</binding>
</bindings>

View File

@ -22,17 +22,20 @@ MOCHITEST_BROWSER_FILES = \
browser_social_multiprovider.js \ browser_social_multiprovider.js \
browser_social_multiworker.js \ browser_social_multiworker.js \
browser_social_errorPage.js \ browser_social_errorPage.js \
browser_social_marks.js \
browser_social_status.js \ browser_social_status.js \
browser_social_window.js \ browser_social_window.js \
social_activate.html \ social_activate.html \
social_activate_iframe.html \ social_activate_iframe.html \
browser_share.js \ browser_share.js \
social_panel.html \ social_panel.html \
social_mark_image.png \ social_mark.html \
social_sidebar.html \ social_sidebar.html \
social_chat.html \ social_chat.html \
social_flyout.html \ social_flyout.html \
social_window.html \ social_window.html \
social_worker.js \ social_worker.js \
share.html \ share.html \
checked.jpg \
unchecked.jpg \
$(NULL) $(NULL)

View File

@ -0,0 +1,340 @@
/* 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/. */
let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
let manifest = { // builtin provider
name: "provider example.com",
origin: "https://example.com",
sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "https://example.com/browser/browser/base/content/test/moz.png"
};
let manifest2 = { // used for testing install
name: "provider test1",
origin: "https://test1.example.com",
workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js",
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/moz.png",
version: 1
};
let 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/moz.png",
version: 1
};
function makeMarkProvider(origin) {
return { // used for testing install
name: "mark provider " + origin,
origin: "https://" + origin + ".example.com",
workerURL: "https://" + origin + ".example.com/browser/browser/base/content/test/social/social_worker.js",
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/moz.png",
version: 1
}
}
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();
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
let toolbar = document.getElementById("nav-bar");
let currentsetAtStart = toolbar.currentSet;
runSocialTestWithProvider(manifest, function () {
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.allowMultipleWorkers");
Services.prefs.clearUserPref("social.whitelist");
// This post-test test ensures that a new window maintains the same
// toolbar button set as when we started. That means our insert/removal of
// persistent id's is working correctly
is(currentsetAtStart, toolbar.currentSet, "toolbar currentset unchanged");
openWindowAndWaitForInit(function(w1) {
checkSocialUI(w1);
// Sometimes the new window adds other buttons to currentSet that are
// outside the scope of what we're checking. So we verify that all
// buttons from startup are in currentSet for a new window, and that the
// provider buttons are properly removed. (e.g on try, window-controls
// was not present in currentsetAtStart, but present on the second
// window)
let tb1 = w1.document.getElementById("nav-bar");
let startupSet = Set(toolbar.currentSet.split(','));
let newSet = Set(tb1.currentSet.split(','));
let intersect = Set([x for (x of startupSet) if (newSet.has(x))]);
let difference = Set([x for (x of newSet) if (!startupSet.has(x))]);
is(startupSet.size, intersect.size, "new window toolbar same as old");
// verify that our provider buttons are not in difference
let id = SocialMarks._toolbarHelper.idFromOrgin(manifest2.origin);
ok(!difference.has(id), "mark button not persisted at end");
w1.close();
finish();
});
});
});
}
var tests = {
testNoButtonOnInstall: function(next) {
// we expect the addon install dialog to appear, we need to accept the
// install from the dialog.
info("Waiting for install dialog");
let panel = document.getElementById("servicesInstall-notification");
PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
info("servicesInstall-notification panel opened");
panel.button.click();
})
let id = "social-mark-button-" + manifest3.origin;
let toolbar = document.getElementById("nav-bar");
let currentset = toolbar.getAttribute("currentset").split(',');
ok(currentset.indexOf(id) < 0, "button is not part of currentset at start");
let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html"
addTab(activationURL, function(tab) {
let doc = tab.linkedBrowser.contentDocument;
Social.installProvider(doc, manifest3, function(addonManifest) {
// enable the provider so we know the button would have appeared
SocialService.addBuiltinProvider(manifest3.origin, function(provider) {
ok(provider, "provider is installed");
currentset = toolbar.getAttribute("currentset").split(',');
ok(currentset.indexOf(id) < 0, "button was not added to currentset");
Social.uninstallProvider(manifest3.origin, function() {
gBrowser.removeTab(tab);
next();
});
});
});
});
},
testButtonOnInstall: function(next) {
// we expect the addon install dialog to appear, we need to accept the
// install from the dialog.
info("Waiting for install dialog");
let panel = document.getElementById("servicesInstall-notification");
PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
info("servicesInstall-notification panel opened");
panel.button.click();
})
let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
addTab(activationURL, function(tab) {
let doc = tab.linkedBrowser.contentDocument;
Social.installProvider(doc, manifest2, function(addonManifest) {
// at this point, we should have a button id in the currentset for our provider
let id = "social-mark-button-" + manifest2.origin;
let toolbar = document.getElementById("nav-bar");
waitForCondition(function() {
let currentset = toolbar.getAttribute("currentset").split(',');
return currentset.indexOf(id) >= 0;
},
function() {
// no longer need the tab
gBrowser.removeTab(tab);
next();
}, "mark button added to currentset");
});
});
},
testButtonOnEnable: function(next) {
// enable the provider now
SocialService.addBuiltinProvider(manifest2.origin, function(provider) {
ok(provider, "provider is installed");
let id = "social-mark-button-" + manifest2.origin;
waitForCondition(function() { return document.getElementById(id) },
function() {
checkSocialUI(window);
next();
}, "button exists after enabling social");
});
},
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 = "social-mark-button-" + provider.origin;
let btn = document.getElementById(id)
ok(btn, "got a mark button");
let port = provider.getWorkerPort();
ok(port, "got a port");
// 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"
addTab(activationURL, function(tab) {
ok(!btn.disabled, "button is enabled");
port.onmessage = function (e) {
let topic = e.data.topic;
switch (topic) {
case "test-init-done":
ok(true, "test-init-done received");
ok(provider.profile.userName, "profile was set by test worker");
// 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
waitForCondition(function() btn.isMarked, function() {
EventUtils.synthesizeMouseAtCenter(btn, {});
}, "button is marked");
break;
case "got-social-panel-visibility":
ok(true, "got the panel message " + e.data.result);
if (e.data.result == "shown") {
// unmark the page via the button in the page
let doc = btn.contentDocument;
let unmarkBtn = doc.getElementById("unmark");
ok(unmarkBtn, "got the panel unmark button");
EventUtils.sendMouseEvent({type: "click"}, unmarkBtn, btn.contentWindow);
} else {
// page should no longer be marked
port.close();
waitForCondition(function() !btn.isMarked, function() {
// after closing the addons tab, verify provider is still installed
gBrowser.tabContainer.addEventListener("TabClose", function onTabClose() {
gBrowser.tabContainer.removeEventListener("TabClose", onTabClose);
executeSoon(function () {
ok(btn.disabled, "button is disabled");
next();
});
});
gBrowser.removeTab(tab);
}, "button unmarked");
}
break;
}
};
port.postMessage({topic: "test-init"});
});
},
testButtonOnDisable: function(next) {
// enable the provider now
let provider = Social._getProviderFromOrigin(manifest2.origin);
ok(provider, "provider is installed");
SocialService.removeProvider(manifest2.origin, function() {
let id = "social-mark-button-" + manifest2.origin;
waitForCondition(function() { return !document.getElementById(id) },
function() {
checkSocialUI(window);
next();
}, "button does not exist after disabling the provider");
});
},
testButtonOnUninstall: function(next) {
Social.uninstallProvider(manifest2.origin, function() {
// test that the button is no longer persisted
let id = "social-mark-button-" + manifest2.origin;
let toolbar = document.getElementById("nav-bar");
let currentset = toolbar.getAttribute("currentset").split(',');
is(currentset.indexOf(id), -1, "button no longer in currentset");
next();
});
},
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");
PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
info("servicesInstall-notification panel opened");
panel.button.click();
})
let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html"
let id = "social-mark-button-" + manifest.origin;
let toolbar = document.getElementById("nav-bar");
addTab(activationURL, function(tab) {
let doc = tab.linkedBrowser.contentDocument;
Social.installProvider(doc, manifest, function(addonManifest) {
waitForCondition(function() {
let currentset = toolbar.getAttribute("currentset").split(',');
return currentset.indexOf(id) >= 0;
},
function() {
// enable the provider so we know the button would have appeared
SocialService.addBuiltinProvider(manifest.origin, function(provider) {
waitForCondition(function() { return document.getElementById(id) },
function() {
gBrowser.removeTab(tab);
installed.push(manifest.origin);
// checkSocialUI will properly check where the menus are located
checkSocialUI(window);
executeSoon(function() {
addProviders(callback);
});
}, "button exists after enabling social");
});
}, "mark button added to currentset");
});
});
}
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();
});
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

@ -247,6 +247,42 @@ function checkSocialUI(win) {
if (provider) { if (provider) {
for (let id of ["menu_socialSidebar", "menu_socialAmbientMenu"]) for (let id of ["menu_socialSidebar", "menu_socialAmbientMenu"])
_is(document.getElementById(id).getAttribute("label"), Social.provider.name, "element has the provider name"); _is(document.getElementById(id).getAttribute("label"), Social.provider.name, "element has the provider name");
let contextMenus = [
{
type: "link",
id: "context-marklinkMenu",
label: "social.marklink.label"
},
{
type: "page",
id: "context-markpageMenu",
label: "social.markpage.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. // and for good measure, check all the social commands.

View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<link id="siteicon" rel="icon" href="./icon.png"/>
<title>Demo Mark Window</title>
<script type="text/javascript">
window.addEventListener("socialFrameShow", function(e) {
var port = navigator.mozSocial.getWorker().port;
port.postMessage({topic: "status-panel-visibility", result: "shown"});
}, false);
window.addEventListener("socialFrameHide", function(e) {
var port = navigator.mozSocial.getWorker().port;
port.postMessage({topic: "status-panel-visibility", result: "hidden"});
}, false);
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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 779 B

View File

@ -121,6 +121,7 @@ browser.jar:
#ifdef XP_WIN #ifdef XP_WIN
content/browser/win6BrowserOverlay.xul (content/win6BrowserOverlay.xul) content/browser/win6BrowserOverlay.xul (content/win6BrowserOverlay.xul)
#endif #endif
content/browser/socialmarks.xml (content/socialmarks.xml)
content/browser/socialchat.xml (content/socialchat.xml) content/browser/socialchat.xml (content/socialchat.xml)
# the following files are browser-specific overrides # the following files are browser-specific overrides
* content/browser/license.html (/toolkit/content/license.html) * content/browser/license.html (/toolkit/content/license.html)

View File

@ -645,6 +645,11 @@ just addresses the organization to follow, e.g. "This site is run by " -->
<!ENTITY social.chatBar.label "Focus chats"> <!ENTITY social.chatBar.label "Focus chats">
<!ENTITY social.chatBar.accesskey "c"> <!ENTITY social.chatBar.accesskey "c">
<!ENTITY social.markpage.accesskey "P">
<!ENTITY social.markpageMenu.label "Save Page To…">
<!ENTITY social.marklink.accesskey "L">
<!ENTITY social.marklinkMenu.label "Save Link To…">
<!ENTITY getUserMedia.selectCamera.label "Camera to share:"> <!ENTITY getUserMedia.selectCamera.label "Camera to share:">
<!ENTITY getUserMedia.selectCamera.accesskey "C"> <!ENTITY getUserMedia.selectCamera.accesskey "C">
<!ENTITY getUserMedia.selectMicrophone.label "Microphone to share:"> <!ENTITY getUserMedia.selectMicrophone.label "Microphone to share:">

View File

@ -429,6 +429,11 @@ social.turnOff.accesskey=T
social.turnOn.label=Turn on %S social.turnOn.label=Turn on %S
social.turnOn.accesskey=T social.turnOn.accesskey=T
# LOCALIZATION NOTE (social.markpage.label): %S is the name of the social provider
social.markpage.label=Save Page to %S
# LOCALIZATION NOTE (social.marklink.label): %S is the name of the social provider
social.marklink.label=Save Link to %S
# LOCALIZATION NOTE (social.error.message): %1$S is brandShortName (e.g. Firefox), %2$S is the name of the social provider # LOCALIZATION NOTE (social.error.message): %1$S is brandShortName (e.g. Firefox), %2$S is the name of the social provider
social.error.message=%1$S is unable to connect with %2$S right now. social.error.message=%1$S is unable to connect with %2$S right now.
social.error.tryAgain.label=Try Again social.error.tryAgain.label=Try Again

View File

@ -4,12 +4,16 @@
"use strict"; "use strict";
this.EXPORTED_SYMBOLS = ["Social", "OpenGraphBuilder"]; this.EXPORTED_SYMBOLS = ["Social", "OpenGraphBuilder", "DynamicResizeWatcher", "sizeSocialPanelToContent"];
const Ci = Components.interfaces; const Ci = Components.interfaces;
const Cc = Components.classes; const Cc = Components.classes;
const Cu = Components.utils; const Cu = Components.utils;
// The minimum sizes for the auto-resize panel code.
const PANEL_MIN_HEIGHT = 100;
const PANEL_MIN_WIDTH = 330;
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -303,45 +307,25 @@ this.Social = {
}, },
// Page Marking functionality // Page Marking functionality
_getMarkablePageUrl: function Social_getMarkablePageUrl(aURI) { isURIMarked: function(origin, aURI, aCallback) {
let uri = aURI.clone();
try {
// Setting userPass on about:config throws.
uri.userPass = "";
} catch (e) {}
return uri.spec;
},
isURIMarked: function(aURI, aCallback) {
promiseGetAnnotation(aURI).then(function(val) { promiseGetAnnotation(aURI).then(function(val) {
if (val) { if (val) {
let providerList = JSON.parse(val); let providerList = JSON.parse(val);
val = providerList.indexOf(this.provider.origin) >= 0; val = providerList.indexOf(origin) >= 0;
} }
aCallback(!!val); aCallback(!!val);
}.bind(this)); }).then(null, Cu.reportError);
}, },
markURI: function(aURI, aCallback) { markURI: function(origin, aURI, aCallback) {
// this should not be called if this.provider or the port is null
if (!this.provider) {
Cu.reportError("Can't mark a page when no provider is current");
return;
}
let port = this.provider.getWorkerPort();
if (!port) {
Cu.reportError("Can't mark page as no provider port is available");
return;
}
// update or set our annotation // update or set our annotation
promiseGetAnnotation(aURI).then(function(val) { promiseGetAnnotation(aURI).then(function(val) {
let providerList = val ? JSON.parse(val) : []; let providerList = val ? JSON.parse(val) : [];
let marked = providerList.indexOf(this.provider.origin) >= 0; let marked = providerList.indexOf(origin) >= 0;
if (marked) if (marked)
return; return;
providerList.push(this.provider.origin); providerList.push(origin);
// we allow marking links in a page that may not have been visited yet. // 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. // make sure there is a history entry for the uri, then annotate it.
let place = { let place = {
@ -355,52 +339,30 @@ this.Social = {
handleError: function () Cu.reportError("couldn't update history for socialmark annotation"), handleError: function () Cu.reportError("couldn't update history for socialmark annotation"),
handleResult: function () {}, handleResult: function () {},
handleCompletion: function () { handleCompletion: function () {
promiseSetAnnotation(aURI, providerList).then(); promiseSetAnnotation(aURI, providerList).then(function() {
// post to the provider if (aCallback)
let url = this._getMarkablePageUrl(aURI); schedule(function() { aCallback(true); } );
port.postMessage({ }).then(null, Cu.reportError);
topic: "social.page-mark", }
data: { url: url, 'marked': true }
});
port.close();
if (aCallback)
schedule(function() { aCallback(true); } );
}.bind(this)
}); });
}.bind(this)); }).then(null, Cu.reportError);
}, },
unmarkURI: function(aURI, aCallback) {
// this should not be called if this.provider or the port is null
if (!this.provider) {
Cu.reportError("Can't mark a page when no provider is current");
return;
}
let port = this.provider.getWorkerPort();
if (!port) {
Cu.reportError("Can't mark page as no provider port is available");
return;
}
unmarkURI: function(origin, aURI, aCallback) {
// this should not be called if this.provider or the port is null
// set our annotation // set our annotation
promiseGetAnnotation(aURI).then(function(val) { promiseGetAnnotation(aURI).then(function(val) {
let providerList = val ? JSON.parse(val) : []; let providerList = val ? JSON.parse(val) : [];
let marked = providerList.indexOf(this.provider.origin) >= 0; let marked = providerList.indexOf(origin) >= 0;
if (marked) { if (marked) {
// remove the annotation // remove the annotation
providerList.splice(providerList.indexOf(this.provider.origin), 1); providerList.splice(providerList.indexOf(origin), 1);
promiseSetAnnotation(aURI, providerList).then(); promiseSetAnnotation(aURI, providerList).then(function() {
if (aCallback)
schedule(function() { aCallback(false); } );
}).then(null, Cu.reportError);
} }
// post to the provider regardless }).then(null, Cu.reportError);
let url = this._getMarkablePageUrl(aURI);
port.postMessage({
topic: "social.page-mark",
data: { url: url, 'marked': false }
});
port.close();
if (aCallback)
schedule(function() { aCallback(false); } );
}.bind(this));
}, },
setErrorListener: function(iframe, errorHandler) { setErrorListener: function(iframe, errorHandler) {
@ -481,7 +443,108 @@ SocialErrorListener.prototype = {
}; };
function sizeSocialPanelToContent(panel, iframe) {
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;
let bodyId = body.getAttribute("contentid");
if (bodyId) {
body = doc.getElementById(bodyId) || doc.body;
}
// offsetHeight/Width don't include margins, so account for that.
let cs = doc.defaultView.getComputedStyle(body);
let width = PANEL_MIN_WIDTH;
let height = PANEL_MIN_HEIGHT;
// if the panel is preloaded prior to being shown, cs will be null. in that
// case use the minimum size for the panel until it is shown.
if (cs) {
let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom);
height = Math.max(computedHeight, height);
let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight);
width = Math.max(computedWidth, width);
}
iframe.style.width = width + "px";
iframe.style.height = height + "px";
// since we do not use panel.sizeTo, we need to adjust the arrow ourselves
if (panel.state == "open")
panel.adjustArrowPosition();
}
function DynamicResizeWatcher() {
this._mutationObserver = null;
}
DynamicResizeWatcher.prototype = {
start: function DynamicResizeWatcher_start(panel, iframe) {
this.stop(); // just in case...
let doc = iframe.contentDocument;
this._mutationObserver = new iframe.contentWindow.MutationObserver(function(mutations) {
sizeSocialPanelToContent(panel, iframe);
});
// Observe anything that causes the size to change.
let config = {attributes: true, characterData: true, childList: true, subtree: true};
this._mutationObserver.observe(doc, config);
// and since this may be setup after the load event has fired we do an
// initial resize now.
sizeSocialPanelToContent(panel, iframe);
},
stop: function DynamicResizeWatcher_stop() {
if (this._mutationObserver) {
try {
this._mutationObserver.disconnect();
} catch (ex) {
// may get "TypeError: can't access dead object" which seems strange,
// but doesn't seem to indicate a real problem, so ignore it...
}
this._mutationObserver = null;
}
}
}
this.OpenGraphBuilder = { this.OpenGraphBuilder = {
generateEndpointURL: function(URLTemplate, pageData) {
// support for existing oexchange style endpoints by supporting their
// querystring arguments. parse the query string template and do
// replacements where necessary the query names may be different than ours,
// so we could see u=%{url} or url=%{url}
let [endpointURL, queryString] = URLTemplate.split("?");
let query = {};
if (queryString) {
queryString.split('&').forEach(function (val) {
let [name, value] = val.split('=');
let p = /%\{(.+)\}/.exec(value);
if (!p) {
// preserve non-template query vars
query[name] = value;
} else if (pageData[p[1]]) {
query[name] = pageData[p[1]];
} else if (p[1] == "body") {
// build a body for emailers
let body = "";
if (pageData.title)
body += pageData.title + "\n\n";
if (pageData.description)
body += pageData.description + "\n\n";
if (pageData.text)
body += pageData.text + "\n\n";
body += pageData.url;
query["body"] = body;
}
});
}
var str = [];
for (let p in query)
str.push(p + "=" + encodeURIComponent(query[p]));
if (str.length)
endpointURL = endpointURL + "?" + str.join("&");
return endpointURL;
},
getData: function(browser) { getData: function(browser) {
let res = { let res = {
url: this._validateURL(browser, browser.currentURI.spec), url: this._validateURL(browser, browser.currentURI.spec),

View File

@ -230,10 +230,10 @@ function attachToWindow(provider, targetWindow) {
.QueryInterface(Ci.nsIDocShell) .QueryInterface(Ci.nsIDocShell)
.chromeEventHandler; .chromeEventHandler;
while (elt) { while (elt) {
if (elt.nodeName == "panel") { if (elt.localName == "panel") {
elt.hidePopup(); elt.hidePopup();
break; break;
} else if (elt.nodeName == "chatbox") { } else if (elt.localName == "chatbox") {
elt.close(); elt.close();
break; break;
} }

View File

@ -500,7 +500,7 @@ this.SocialService = {
}, },
_manifestFromData: function(type, data, principal) { _manifestFromData: function(type, data, principal) {
let sameOriginRequired = ['workerURL', 'sidebarURL', 'shareURL', 'statusURL']; let sameOriginRequired = ['workerURL', 'sidebarURL', 'shareURL', 'statusURL', 'markURL'];
if (type == 'directory') { if (type == 'directory') {
// directory provided manifests must have origin in manifest, use that // directory provided manifests must have origin in manifest, use that
@ -728,6 +728,9 @@ function SocialProvider(input) {
this.sidebarURL = input.sidebarURL; this.sidebarURL = input.sidebarURL;
this.shareURL = input.shareURL; this.shareURL = input.shareURL;
this.statusURL = input.statusURL; this.statusURL = input.statusURL;
this.markURL = input.markURL;
this.markedIcon = input.markedIcon;
this.unmarkedIcon = input.unmarkedIcon;
this.origin = input.origin; this.origin = input.origin;
let originUri = Services.io.newURI(input.origin, null, null); let originUri = Services.io.newURI(input.origin, null, null);
this.principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri); this.principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri);