Merge f-t to m-c, a=merge

This commit is contained in:
Phil Ringnalda 2015-03-21 12:47:01 -07:00
commit 9ef50f3565
30 changed files with 514 additions and 320 deletions

View File

@ -1881,5 +1881,6 @@ pref("reader.parse-on-load.enabled", false);
// Disable ReadingList browser UI by default.
pref("browser.readinglist.enabled", false);
pref("browser.readinglist.sidebarEverOpened", false);
// Enable the readinglist engine by default.
pref("readinglist.scheduler.enabled", true);

View File

@ -178,7 +178,7 @@ let ReadingListUI = {
});
target.insertBefore(menuitem, insertPoint);
});
}, {sort: "addedOn", descending: true});
if (!hasItems) {
let menuitem = document.createElement("menuitem");
@ -242,12 +242,20 @@ let ReadingListUI = {
uri = null;
}
let msg = {topic: "UpdateActiveItem", url: null};
if (!uri) {
this.toolbarButton.setAttribute("hidden", true);
if (this.isSidebarOpen)
document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
return;
}
let isInList = yield ReadingList.containsURL(uri);
let isInList = yield ReadingList.hasItemForURL(uri);
if (this.isSidebarOpen) {
if (isInList)
msg.url = typeof uri == "string" ? uri : uri.spec;
document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
}
this.setToolbarButtonState(isInList);
}),
@ -284,7 +292,7 @@ let ReadingListUI = {
if (!uri)
return;
let item = yield ReadingList.getItemForURL(uri);
let item = yield ReadingList.itemForURL(uri);
if (item) {
yield item.delete();
} else {
@ -315,8 +323,15 @@ let ReadingListUI = {
* @param {ReadingListItem} item - Item added.
*/
onItemAdded(item) {
if (!Services.prefs.getBoolPref("browser.readinglist.sidebarEverOpened")) {
SidebarUI.show("readingListSidebar");
}
if (this.isItemForCurrentBrowser(item)) {
this.setToolbarButtonState(true);
if (this.isSidebarOpen) {
let msg = {topic: "UpdateActiveItem", url: item.url};
document.getElementById("sidebar").contentWindow.postMessage(msg, "*");
}
}
},

View File

@ -915,7 +915,6 @@ chatbox {
chatbox[large="true"] {
width: 300px;
heigth: 272px;
}
chatbox[minimized="true"] {

View File

@ -168,7 +168,8 @@ let gGrid = {
' class="newtab-control newtab-control-pin"/>' +
'<input type="button" title="' + newTabString("block") + '"' +
' class="newtab-control newtab-control-block"/>' +
'<span class="newtab-sponsored">' + newTabString("sponsored.button") + '</span>';
'<span class="newtab-sponsored">' + newTabString("sponsored.button") + '</span>' +
'<span class="newtab-suggested"/>';
this._siteFragment = document.createDocumentFragment();
this._siteFragment.appendChild(site);
@ -189,7 +190,8 @@ let gGrid = {
// Save the cell's computed height/width including margin and border
if (this._cellMargin === undefined) {
let refCell = document.querySelector(".newtab-cell");
this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) * 2;
this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop) +
parseFloat(getComputedStyle(refCell).marginBottom);
this._cellHeight = refCell.offsetHeight + this._cellMargin;
this._cellWidth = refCell.offsetWidth + this._cellMargin;
}

View File

@ -159,7 +159,7 @@ input[type=button] {
.newtab-cell {
display: -moz-box;
height: 180px;
margin: 20px 10px;
margin: 20px 10px 85px;
width: 290px;
}
@ -193,20 +193,45 @@ input[type=button] {
/* TITLES */
.newtab-sponsored,
.newtab-title {
bottom: -26px;
.newtab-title,
.newtab-suggested {
overflow: hidden;
position: absolute;
right: 0;
text-align: center;
}
.newtab-sponsored,
.newtab-title {
bottom: -26px;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 13px;
}
.newtab-suggested {
border: 1px solid #dcdcdc;
border-radius: 2px;
cursor: pointer;
font-size: 12px;
height: 17px;
line-height: 17px;
margin-bottom: -1px;
padding: 2px 8px;
display: none;
margin-left: auto;
margin-right: auto;
left: 0;
top: 215px;
}
.newtab-suggested-bounds {
max-height: 51px; /* 51 / 17 = 3 lines maximum */
}
.newtab-title {
font-size: 13px;
left: 0;
padding-top: 14px;
text-overflow: ellipsis;
}
.newtab-sponsored {
@ -237,6 +262,10 @@ input[type=button] {
display: block;
}
.newtab-site[type=related] .newtab-suggested {
display: table;
}
.sponsored-explain,
.sponsored-explain a {
color: white;
@ -462,7 +491,7 @@ input[type=button] {
.newtab-customize-panel-item,
.newtab-search-panel-engine,
#newtab-search-manage {
padding: 4px 24px;
padding: 10px 10px 10px 25px;
}
.newtab-customize-panel-item:not(:last-child),
@ -484,6 +513,10 @@ input[type=button] {
margin: 0;
}
.newtab-customize-panel-item:not([selected]) {
color: #919191;
}
.newtab-customize-panel-item[selected],
.newtab-search-panel-engine[selected] {
background: url("chrome://global/skin/menu/shared-menu-check.png") center left 4px no-repeat transparent;

View File

@ -37,13 +37,13 @@
<xul:panel id="newtab-customize-panel" orient="vertical" type="arrow"
noautohide="true" hidden="true">
<xul:hbox id="newtab-customize-enhanced" class="newtab-customize-panel-item">
<xul:label>&newtab.customize.enhanced;</xul:label>
<xul:label>&newtab.customize.suggested;</xul:label>
</xul:hbox>
<xul:hbox id="newtab-customize-classic" class="newtab-customize-panel-item">
<xul:label>&newtab.customize.classic;</xul:label>
<xul:label>&newtab.customize.topsites;</xul:label>
</xul:hbox>
<xul:hbox id="newtab-customize-blank" class="newtab-customize-panel-item">
<xul:label>&newtab.customize.blank;</xul:label>
<xul:label>&newtab.customize.blank2;</xul:label>
</xul:hbox>
</xul:panel>

View File

@ -131,6 +131,12 @@ Site.prototype = {
this._querySelector(".newtab-title").textContent = title;
this.node.setAttribute("type", this.link.type);
if (this.link.targetedSite) {
let targetedSite = `<strong> ${this.link.targetedSite} </strong>`;
this._querySelector(".newtab-suggested").innerHTML =
`<div class='newtab-suggested-bounds'> ${newTabString("suggested.button", [targetedSite])} </div>`;
}
if (this.isPinned())
this._updateAttributes(true);
// Capture the page if the thumbnail is missing, which will cause page.js
@ -177,6 +183,15 @@ Site.prototype = {
}
},
_ignoreHoverEvents: function(element) {
element.addEventListener("mouseover", () => {
this.cell.node.setAttribute("ignorehover", "true");
});
element.addEventListener("mouseout", () => {
this.cell.node.removeAttribute("ignorehover");
});
},
/**
* Adds event handlers for the site and its buttons.
*/
@ -186,14 +201,12 @@ Site.prototype = {
this._node.addEventListener("dragend", this, false);
this._node.addEventListener("mouseover", this, false);
// Specially treat the sponsored icon to prevent regular hover effects
// Specially treat the sponsored icon & suggested explanation
// text to prevent regular hover effects
let sponsored = this._querySelector(".newtab-sponsored");
sponsored.addEventListener("mouseover", () => {
this.cell.node.setAttribute("ignorehover", "true");
});
sponsored.addEventListener("mouseout", () => {
this.cell.node.removeAttribute("ignorehover");
});
let suggested = this._querySelector(".newtab-suggested");
this._ignoreHoverEvents(sponsored);
this._ignoreHoverEvents(suggested);
},
/**
@ -268,6 +281,12 @@ Site.prototype = {
}
// Only handle primary clicks for the remaining targets
else if (button == 0) {
if (target.parentElement.classList.contains("newtab-suggested") ||
target.classList.contains("newtab-suggested")) {
// Suggested explanation text should do nothing when clicked and
// the link in the suggested explanation should act as default.
return;
}
aEvent.preventDefault();
if (target.classList.contains("newtab-control-block")) {
this.block();

View File

@ -60,8 +60,8 @@ function runTests() {
yield addNewTabPageTab();
checkGrid("0,1,2,3,4,5,6,7p,8p");
yield simulateDrop(2, 8);
checkGrid("0,1,3,4,5,6,7p,8p,2p");
yield simulateDrop(2, 5);
checkGrid("0,1,3,4,5,2p,6,7p,8p");
// make sure that pinned sites are re-positioned correctly
yield setLinks("0,1,2,3,4,5,6,7,8");

View File

@ -35,8 +35,8 @@ function runTests() {
// force the grid to be small enough that a pinned cell could be pushed out
Services.prefs.setIntPref(PREF_NEWTAB_COLUMNS, 3);
yield simulateExternalDrop(7);
checkGrid("0,1,2,3,4,5,7p,99p,8p");
yield simulateExternalDrop(5);
checkGrid("0,1,2,3,4,99p,5,7p,8p");
// drag a new site beneath a pinned cell and make sure the pinned cell is
// not moved
@ -46,8 +46,8 @@ function runTests() {
yield addNewTabPageTab();
checkGrid("0,1,2,3,4,5,6,7,8p");
yield simulateExternalDrop(7);
checkGrid("0,1,2,3,4,5,6,99p,8p");
yield simulateExternalDrop(5);
checkGrid("0,1,2,3,4,99p,5,6,8p");
// drag a new site onto a block of pinned sites and make sure they're shifted
// around accordingly

View File

@ -40,21 +40,21 @@ function runTests() {
yield addNewTabPageTab();
yield customizeNewTabPage("classic");
let {type, enhanced, title} = getData(0);
is(type, "organic", "directory link is organic");
isnot(enhanced, "", "directory link has enhanced image");
is(title, "title");
isnot(type, "enhanced", "history link is not enhanced");
is(enhanced, "", "history link has no enhanced image");
is(title, "site#-1");
is(getData(1), null, "history link pushed out by directory link");
is(getData(1), null, "there is only one link and it's a history link");
// Test with enhanced = true
yield addNewTabPageTab();
yield customizeNewTabPage("enhanced");
({type, enhanced, title} = getData(0));
is(type, "organic", "directory link is still organic");
isnot(enhanced, "", "directory link still has enhanced image");
is(type, "organic", "directory link is organic");
isnot(enhanced, "", "directory link has enhanced image");
is(title, "title");
is(getData(1), null, "history link still pushed out by directory link");
is(getData(1), null, "history link pushed out by directory link");
// Test with a pinned link
setPinnedLinks("-1");

View File

@ -26,7 +26,7 @@ var gContentPane = {
row.removeAttribute("hidden");
}
setEventListener("font.language.group", "blur",
setEventListener("font.language.group", "change",
gContentPane._rebuildFonts);
setEventListener("popupPolicyButton", "command",
gContentPane.showPopupExceptions);

View File

@ -33,8 +33,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "SQLiteStore",
let log = Log.repository.getLogger("readinglist.api");
// Names of basic properties on ReadingListItem.
const ITEM_BASIC_PROPERTY_NAMES = `
// Each ReadingListItem has a _record property, an object containing the raw
// data from the server and local store. These are the names of the properties
// in that object.
const ITEM_RECORD_PROPERTIES = `
guid
lastModified
url
@ -71,7 +73,7 @@ const ITEM_BASIC_PROPERTY_NAMES = `
* control the items that the method acts on.
*
* Each options object is a simple object with properties whose names are drawn
* from ITEM_BASIC_PROPERTY_NAMES. For an item to match an options object, the
* from ITEM_RECORD_PROPERTIES. For an item to match an options object, the
* properties of the item must match all the properties in the object. For
* example, an object { guid: "123" } matches any item whose GUID is 123. An
* object { guid: "123", title: "foo" } matches any item whose GUID is 123 *and*
@ -89,7 +91,7 @@ const ITEM_BASIC_PROPERTY_NAMES = `
* options object { guid: ["123", "456"] } matches any item whose GUID is either
* 123 *or* 456.
*
* In addition to properties with names from ITEM_BASIC_PROPERTY_NAMES, options
* In addition to properties with names from ITEM_RECORD_PROPERTIES, options
* objects can also have the following special properties:
*
* * sort: The name of a property to sort on.
@ -109,14 +111,14 @@ const ITEM_BASIC_PROPERTY_NAMES = `
*/
function ReadingListImpl(store) {
this._store = store;
this._itemsByURL = new Map();
this._itemsByNormalizedURL = new Map();
this._iterators = new Set();
this._listeners = new Set();
}
ReadingListImpl.prototype = {
ItemBasicPropertyNames: ITEM_BASIC_PROPERTY_NAMES,
ItemRecordProperties: ITEM_RECORD_PROPERTIES,
/**
* Yields the number of items in the list.
@ -137,20 +139,20 @@ ReadingListImpl.prototype = {
* @returns {Promise} Promise that is fulfilled with a boolean indicating
* whether the URL is in the list or not.
*/
containsURL: Task.async(function* (url) {
hasItemForURL: Task.async(function* (url) {
url = normalizeURI(url).spec;
// This is used on every tab switch and page load of the current tab, so we
// want it to be quick and avoid a DB query whenever possible.
// First check if any cached items have a direct match.
if (this._itemsByURL.has(url)) {
if (this._itemsByNormalizedURL.has(url)) {
return true;
}
// Then check if any cached items may have a different resolved URL
// that matches.
for (let itemWeakRef of this._itemsByURL.values()) {
for (let itemWeakRef of this._itemsByNormalizedURL.values()) {
let item = itemWeakRef.get();
if (item && item.resolvedURL == url) {
return true;
@ -177,10 +179,10 @@ ReadingListImpl.prototype = {
*/
forEachItem: Task.async(function* (callback, ...optsList) {
let promiseChain = Promise.resolve();
yield this._store.forEachItem(obj => {
yield this._store.forEachItem(record => {
promiseChain = promiseChain.then(() => {
return new Promise((resolve, reject) => {
let promise = callback(this._itemFromObject(obj));
let promise = callback(this._itemFromRecord(record));
if (promise instanceof Promise) {
return promise.then(resolve, reject);
}
@ -210,23 +212,26 @@ ReadingListImpl.prototype = {
* Adds an item to the list that isn't already present.
*
* The given object represents a new item, and the properties of the object
* are those in ITEM_BASIC_PROPERTY_NAMES. It may have as few or as many
* are those in ITEM_RECORD_PROPERTIES. It may have as few or as many
* properties that you want to set, but it must have a `url` property.
*
* It's an error to call this with an object whose `url` or `guid` properties
* are the same as those of items that are already present in the list. The
* returned promise is rejected in that case.
*
* @param obj A simple object representing an item.
* @param record A simple object representing an item.
* @return Promise<ReadingListItem> Resolved with the new item when the list
* is updated. Rejected with an Error on error.
*/
addItem: Task.async(function* (obj) {
obj = stripNonItemProperties(obj);
normalizeReadingListProperties(obj);
yield this._store.addItem(obj);
addItem: Task.async(function* (record) {
record = normalizeRecord(record);
record.addedOn = Date.now();
if (Services.prefs.prefHasUserValue("services.sync.client.name")) {
record.addedBy = Services.prefs.getCharPref("services.sync.client.name");
}
yield this._store.addItem(record);
this._invalidateIterators();
let item = this._itemFromObject(obj);
let item = this._itemFromRecord(record);
this._callListeners("onItemAdded", item);
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.broadcastAsyncMessage("Reader:Added", item);
@ -249,7 +254,7 @@ ReadingListImpl.prototype = {
*/
updateItem: Task.async(function* (item) {
this._ensureItemBelongsToList(item);
yield this._store.updateItem(item._properties);
yield this._store.updateItem(item._record);
this._invalidateIterators();
this._callListeners("onItemUpdated", item);
}),
@ -268,26 +273,36 @@ ReadingListImpl.prototype = {
this._ensureItemBelongsToList(item);
yield this._store.deleteItemByURL(item.url);
item.list = null;
this._itemsByURL.delete(item.url);
this._itemsByNormalizedURL.delete(item.url);
this._invalidateIterators();
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.broadcastAsyncMessage("Reader:Removed", item);
this._callListeners("onItemDeleted", item);
}),
/**
* Finds the first item that matches the given options.
*
* @param optsList See Options Objects.
* @return The first matching item, or null if there are no matching items.
*/
item: Task.async(function* (...optsList) {
return (yield this.iterator(...optsList).items(1))[0] || null;
}),
/**
* Find any item that matches a given URL - either the item's URL, or its
* resolved URL.
*
* @param {String/nsIURI} uri - URI to match against. This will be normalized.
* @return The first matching item, or null if there are no matching items.
*/
getItemForURL: Task.async(function* (uri) {
itemForURL: Task.async(function* (uri) {
let url = normalizeURI(uri).spec;
let [item] = yield this.iterator({url: url}, {resolvedURL: url}).items(1);
return item;
return (yield this.item({ url: url }, { resolvedURL: url }));
}),
/**
/**
* Add to the ReadingList the page that is loaded in a given browser.
*
* @param {<xul:browser>} browser - Browser element for the document,
@ -297,7 +312,7 @@ ReadingListImpl.prototype = {
*/
addItemFromBrowser: Task.async(function* (browser, url) {
let metadata = yield getMetadataFromBrowser(browser);
let itemData = {
let record = {
url: url,
title: metadata.title,
resolvedURL: metadata.url,
@ -305,11 +320,10 @@ ReadingListImpl.prototype = {
};
if (metadata.previews.length > 0) {
itemData.preview = metadata.previews[0];
record.preview = metadata.previews[0];
}
let item = yield ReadingList.addItem(itemData);
return item;
return (yield this.addItem(record));
}),
/**
@ -340,21 +354,21 @@ ReadingListImpl.prototype = {
*/
destroy: Task.async(function* () {
yield this._store.destroy();
for (let itemWeakRef of this._itemsByURL.values()) {
for (let itemWeakRef of this._itemsByNormalizedURL.values()) {
let item = itemWeakRef.get();
if (item) {
item.list = null;
}
}
this._itemsByURL.clear();
this._itemsByNormalizedURL.clear();
}),
// The list's backing store.
_store: null,
// A Map mapping URL strings to nsIWeakReferences that refer to
// A Map mapping *normalized* URL strings to nsIWeakReferences that refer to
// ReadingListItems.
_itemsByURL: null,
_itemsByNormalizedURL: null,
// A Set containing nsIWeakReferences that refer to valid iterators produced
// by the list.
@ -364,22 +378,22 @@ ReadingListImpl.prototype = {
_listeners: null,
/**
* Returns the ReadingListItem represented by the given simple object. If
* Returns the ReadingListItem represented by the given record object. If
* the item doesn't exist yet, it's created first.
*
* @param obj A simple object with item properties.
* @param record A simple object with *normalized* item record properties.
* @return The ReadingListItem.
*/
_itemFromObject(obj) {
let itemWeakRef = this._itemsByURL.get(obj.url);
_itemFromRecord(record) {
let itemWeakRef = this._itemsByNormalizedURL.get(record.url);
let item = itemWeakRef ? itemWeakRef.get() : null;
if (item) {
item.setProperties(obj, false);
item._record = record;
}
else {
item = new ReadingListItem(obj);
item = new ReadingListItem(record);
item.list = this;
this._itemsByURL.set(obj.url, Cu.getWeakReference(item));
this._itemsByNormalizedURL.set(record.url, Cu.getWeakReference(item));
}
return item;
},
@ -425,18 +439,6 @@ ReadingListImpl.prototype = {
},
};
/*
* normalize the properties of a "regular" object that reflects a ReadingListItem
*/
function normalizeReadingListProperties(obj) {
if (obj.url) {
obj.url = normalizeURI(obj.url).spec;
}
if (obj.resolvedURL) {
obj.resolvedURL = normalizeURI(obj.resolvedURL).spec;
}
}
let _unserializable = () => {}; // See comments in the ReadingListItem ctor.
@ -446,27 +448,30 @@ let _unserializable = () => {}; // See comments in the ReadingListItem ctor.
* Each item belongs to a list, and it's an error to use an item with a
* ReadingList that the item doesn't belong to.
*
* @param props The properties of the item, as few or many as you want.
* @param record A simple object with the properties of the item, as few or many
* as you want. This will be normalized.
*/
function ReadingListItem(props={}) {
this._properties = {};
function ReadingListItem(record={}) {
this._record = record;
// |this._unserializable| works around a problem when sending one of these
// items via a message manager. If |this.list| is set, the item can't be
// transferred directly, so .toJSON is implicitly called and the object
// returned via that is sent. However, once the item is deleted and |this.list|
// is null, the item *can* be directly serialized - so the message handler
// sees the "raw" object - ie, it sees "_properties" etc.
// sees the "raw" object - ie, it sees "_record" etc.
// We work around this problem by *always* having an unserializable property
// on the object - this way the implicit .toJSON call is always made, even
// when |this.list| is null.
this._unserializable = _unserializable;
this.setProperties(props, false);
}
ReadingListItem.prototype = {
// Be careful when caching properties. If you cache a property that depends
// on a mutable _record property, then you need to recache your property after
// _record is set.
/**
* Item's unique ID.
* @type string
@ -480,27 +485,11 @@ ReadingListItem.prototype = {
/**
* The item's server-side GUID. This is set by the remote server and therefore is not
* guarenteed to be set for local items.
* guaranteed to be set for local items.
* @type string
*/
get guid() {
return this._properties.guid || undefined;
},
set guid(val) {
this._properties.guid = val;
},
/**
* The date the item was last modified.
* @type Date
*/
get lastModified() {
return this._properties.lastModified ?
new Date(this._properties.lastModified) :
undefined;
},
set lastModified(val) {
this._properties.lastModified = val.valueOf();
return this._record.guid || undefined;
},
/**
@ -508,10 +497,7 @@ ReadingListItem.prototype = {
* @type string
*/
get url() {
return this._properties.url;
},
set url(val) {
this._properties.url = normalizeURI(val).spec;
return this._record.url;
},
/**
@ -519,24 +505,12 @@ ReadingListItem.prototype = {
* @type nsIURI
*/
get uri() {
return this._properties.url ?
Services.io.newURI(this._properties.url, "", null) :
undefined;
},
set uri(val) {
this.url = normalizeURI(val).spec;
},
/**
* Returns the domain (a string) of the item's URL. If the URL doesn't have a
* domain, then the URL itself (also a string) is returned.
*/
get domain() {
try {
return this.uri.host;
if (!this._uri) {
this._uri = this._record.url ?
Services.io.newURI(this._record.url, "", null) :
undefined;
}
catch (err) {}
return this.url;
return this._uri;
},
/**
@ -544,23 +518,24 @@ ReadingListItem.prototype = {
* @type string
*/
get resolvedURL() {
return this._properties.resolvedURL;
return this._record.resolvedURL;
},
set resolvedURL(val) {
this._properties.resolvedURL = normalizeURI(val).spec;
this._updateRecord({ resolvedURL: val });
},
/**
* The item's resolved URL as an nsIURI.
* The item's resolved URL as an nsIURI. The setter takes an nsIURI or a
* string spec.
* @type nsIURI
*/
get resolvedURI() {
return this._properties.resolvedURL ?
Services.io.newURI(this._properties.resolvedURL, "", null) :
return this._record.resolvedURL ?
Services.io.newURI(this._record.resolvedURL, "", null) :
undefined;
},
set resolvedURI(val) {
this.resolvedURL = val.spec;
this._updateRecord({ resolvedURL: val });
},
/**
@ -568,10 +543,10 @@ ReadingListItem.prototype = {
* @type string
*/
get title() {
return this._properties.title;
return this._record.title;
},
set title(val) {
this._properties.title = val;
this._updateRecord({ title: val });
},
/**
@ -579,10 +554,10 @@ ReadingListItem.prototype = {
* @type string
*/
get resolvedTitle() {
return this._properties.resolvedTitle;
return this._record.resolvedTitle;
},
set resolvedTitle(val) {
this._properties.resolvedTitle = val;
this._updateRecord({ resolvedTitle: val });
},
/**
@ -590,10 +565,10 @@ ReadingListItem.prototype = {
* @type string
*/
get excerpt() {
return this._properties.excerpt;
return this._record.excerpt;
},
set excerpt(val) {
this._properties.excerpt = val;
this._updateRecord({ excerpt: val });
},
/**
@ -601,10 +576,10 @@ ReadingListItem.prototype = {
* @type integer
*/
get status() {
return this._properties.status;
return this._record.status;
},
set status(val) {
this._properties.status = val;
this._updateRecord({ status: val });
},
/**
@ -612,10 +587,10 @@ ReadingListItem.prototype = {
* @type boolean
*/
get favorite() {
return !!this._properties.favorite;
return !!this._record.favorite;
},
set favorite(val) {
this._properties.favorite = !!val;
this._updateRecord({ favorite: !!val });
},
/**
@ -623,10 +598,10 @@ ReadingListItem.prototype = {
* @type boolean
*/
get isArticle() {
return !!this._properties.isArticle;
return !!this._record.isArticle;
},
set isArticle(val) {
this._properties.isArticle = !!val;
this._updateRecord({ isArticle: !!val });
},
/**
@ -634,10 +609,10 @@ ReadingListItem.prototype = {
* @type integer
*/
get wordCount() {
return this._properties.wordCount;
return this._record.wordCount;
},
set wordCount(val) {
this._properties.wordCount = val;
this._updateRecord({ wordCount: val });
},
/**
@ -645,10 +620,10 @@ ReadingListItem.prototype = {
* @type boolean
*/
get unread() {
return !!this._properties.unread;
return !!this._record.unread;
},
set unread(val) {
this._properties.unread = !!val;
this._updateRecord({ unread: !!val });
},
/**
@ -656,12 +631,12 @@ ReadingListItem.prototype = {
* @type Date
*/
get addedOn() {
return this._properties.addedOn ?
new Date(this._properties.addedOn) :
return this._record.addedOn ?
new Date(this._record.addedOn) :
undefined;
},
set addedOn(val) {
this._properties.addedOn = val.valueOf();
this._updateRecord({ addedOn: val.valueOf() });
},
/**
@ -669,12 +644,12 @@ ReadingListItem.prototype = {
* @type Date
*/
get storedOn() {
return this._properties.storedOn ?
new Date(this._properties.storedOn) :
return this._record.storedOn ?
new Date(this._record.storedOn) :
undefined;
},
set storedOn(val) {
this._properties.storedOn = val.valueOf();
this._updateRecord({ storedOn: val.valueOf() });
},
/**
@ -682,10 +657,10 @@ ReadingListItem.prototype = {
* @type string
*/
get markedReadBy() {
return this._properties.markedReadBy;
return this._record.markedReadBy;
},
set markedReadBy(val) {
this._properties.markedReadBy = val;
this._updateRecord({ markedReadBy: val });
},
/**
@ -693,12 +668,12 @@ ReadingListItem.prototype = {
* @type Date
*/
get markedReadOn() {
return this._properties.markedReadOn ?
new Date(this._properties.markedReadOn) :
return this._record.markedReadOn ?
new Date(this._record.markedReadOn) :
undefined;
},
set markedReadOn(val) {
this._properties.markedReadOn = val.valueOf();
this._updateRecord({ markedReadOn: val.valueOf() });
},
/**
@ -706,10 +681,10 @@ ReadingListItem.prototype = {
* @param integer
*/
get readPosition() {
return this._properties.readPosition;
return this._record.readPosition;
},
set readPosition(val) {
this._properties.readPosition = val;
this._updateRecord({ readPosition: val });
},
/**
@ -717,28 +692,9 @@ ReadingListItem.prototype = {
* @type string
*/
get preview() {
return this._properties.preview;
return this._record.preview;
},
/**
* Sets the given properties of the item, optionally calling list.updateItem().
*
* @param props A simple object containing the properties to set.
* @param update If true, updateItem() is called for this item.
* @return Promise<null> If update is true, resolved when the update
* completes; otherwise resolved immediately.
*/
setProperties: Task.async(function* (props, update=true) {
for (let name in props) {
this._properties[name] = props[name];
}
// make sure everything is normalized.
normalizeReadingListProperties(this._properties);
if (update) {
yield this.list.updateItem(this);
}
}),
/**
* Deletes the item from its list.
*
@ -751,7 +707,38 @@ ReadingListItem.prototype = {
}),
toJSON() {
return this._properties;
return this._record;
},
/**
* Do not use this at all unless you know what you're doing. Use the public
* getters and setters, above, instead.
*
* A simple object that contains the item's normalized data in the same format
* that the local store and server use. Records passed in by the consumer are
* not normalized, but everywhere else, records are always normalized unless
* otherwise stated. The setter normalizes the passed-in value, so it will
* throw an error if the value is not a valid record.
*/
get _record() {
return this.__record;
},
set _record(val) {
this.__record = normalizeRecord(val);
},
/**
* Updates the item's record. This calls the _record setter, so it will throw
* an error if the partial record is not valid.
*
* @param partialRecord An object containing any of the record properties.
*/
_updateRecord(partialRecord) {
let record = this._record;
for (let prop in partialRecord) {
record[prop] = partialRecord[prop];
}
this._record = record;
},
_ensureBelongsToList() {
@ -854,6 +841,36 @@ ReadingListItemIterator.prototype = {
},
};
/**
* Normalizes the properties of a record object, which represents a
* ReadingListItem. Throws an error if the record contains properties that
* aren't in ITEM_RECORD_PROPERTIES.
*
* @param record A non-normalized record object.
* @return The new normalized record.
*/
function normalizeRecord(nonNormalizedRecord) {
let record = {};
for (let prop in nonNormalizedRecord) {
if (!ITEM_RECORD_PROPERTIES.includes(prop)) {
throw new Error("Unrecognized item property: " + prop);
}
switch (prop) {
case "url":
case "resolvedURL":
if (nonNormalizedRecord[prop]) {
record[prop] = normalizeURI(nonNormalizedRecord[prop]).spec;
}
break;
default:
record[prop] = nonNormalizedRecord[prop];
break;
}
}
return record;
}
/**
* Normalize a URI, stripping away extraneous parts we don't want to store
* or compare against.
@ -872,16 +889,6 @@ function normalizeURI(uri) {
return uri;
};
function stripNonItemProperties(item) {
let obj = {};
for (let name of ITEM_BASIC_PROPERTY_NAMES) {
if (name in item) {
obj[name] = item[name];
}
}
return obj;
}
function hash(str) {
let hasher = Cc["@mozilla.org/security/hash;1"].
createInstance(Ci.nsICryptoHash);

View File

@ -62,7 +62,7 @@ this.SQLiteStore.prototype = {
*/
forEachItem: Task.async(function* (callback, ...optsList) {
let [sql, args] = sqlFromOptions(optsList);
let colNames = ReadingList.ItemBasicPropertyNames;
let colNames = ReadingList.ItemRecordProperties;
let conn = yield this._connectionPromise;
yield conn.executeCached(`
SELECT ${colNames} FROM items ${sql};
@ -71,7 +71,7 @@ this.SQLiteStore.prototype = {
/**
* Adds an item to the store that isn't already present. See
* ReadingList.prototype.addItems.
* ReadingList.prototype.addItem.
*
* @param items A simple object representing an item.
* @return Promise<null> Resolved when the store is updated. Rejected with an
@ -219,14 +219,14 @@ this.SQLiteStore.prototype = {
/**
* Returns a simple object whose properties are the
* ReadingList.ItemBasicPropertyNames properties lifted from the given row.
* ReadingList.ItemRecordProperties lifted from the given row.
*
* @param row A mozIStorageRow.
* @return The item.
*/
function itemFromRow(row) {
let item = {};
for (let name of ReadingList.ItemBasicPropertyNames) {
for (let name of ReadingList.ItemRecordProperties) {
item[name] = row.getResultByName(name);
}
return item;

View File

@ -61,9 +61,13 @@ let RLSidebar = {
this.list.addEventListener("mousemove", event => this.onListMouseMove(event));
this.list.addEventListener("keydown", event => this.onListKeyDown(event), true);
window.addEventListener("message", event => this.onMessage(event));
this.listPromise = this.ensureListItems();
ReadingList.addListener(this);
Services.prefs.setBoolPref("browser.readinglist.sidebarEverOpened", true);
let initEvent = new CustomEvent("Initialized", {bubbles: true});
document.documentElement.dispatchEvent(initEvent);
},
@ -84,12 +88,17 @@ let RLSidebar = {
*
* @param {ReadinglistItem} item - Item that was added.
*/
onItemAdded(item) {
onItemAdded(item, append = false) {
log.trace(`onItemAdded: ${item}`);
let itemNode = document.importNode(this.itemTemplate.content, true).firstElementChild;
this.updateItem(item, itemNode);
this.list.appendChild(itemNode);
// XXX Inserting at the top by default is a temp hack that will stop
// working once we start including items received from sync.
if (append)
this.list.appendChild(itemNode);
else
this.list.insertBefore(itemNode, this.list.firstChild);
this.itemNodesById.set(item.id, itemNode);
this.itemsById.set(item.id, item);
@ -138,7 +147,14 @@ let RLSidebar = {
itemNode.setAttribute("title", `${item.title}\n${item.url}`);
itemNode.querySelector(".item-title").textContent = item.title;
itemNode.querySelector(".item-domain").textContent = item.domain;
let domain = item.uri.spec;
try {
domain = item.uri.host;
}
catch (err) {}
itemNode.querySelector(".item-domain").textContent = domain;
let thumb = itemNode.querySelector(".item-thumb-container");
if (item.preview) {
thumb.style.backgroundImage = "url(" + item.preview + ")";
@ -154,11 +170,11 @@ let RLSidebar = {
yield ReadingList.forEachItem(item => {
// TODO: Should be batch inserting via DocumentFragment
try {
this.onItemAdded(item);
this.onItemAdded(item, true);
} catch (e) {
log.warn("Error adding item", e);
}
});
}, {sort: "addedOn", descending: true});
this.emptyListInfo.hidden = (this.numItems > 0);
}),
@ -186,14 +202,8 @@ let RLSidebar = {
log.debug(`Setting activeItem: ${node ? node.id : null}`);
if (node) {
if (!node.classList.contains("selected")) {
this.selectedItem = node;
}
if (node.classList.contains("active")) {
return;
}
if (node && node.classList.contains("active")) {
return;
}
let prevItem = document.querySelector("#list > .item.active");
@ -416,6 +426,26 @@ let RLSidebar = {
}
}
},
/**
* Handle a message, typically sent from browser-readinglist.js
* @param {Event} event - Triggering event.
*/
onMessage(event) {
let msg = event.data;
if (msg.topic != "UpdateActiveItem") {
return;
}
if (!msg.url) {
this.activeItem = null;
} else {
ReadingList.itemForURL(msg.url).then(item => {
this.activeItem = this.itemNodesById.get(item.id);
});
}
}
};

View File

@ -84,7 +84,13 @@ SidebarUtils.prototype = {
"Node should have correct title attribute");
this.Assert.equal(node.querySelector(".item-title").textContent, item.title,
"Node's title element's text should match item title");
this.Assert.equal(node.querySelector(".item-domain").textContent, item.domain,
let domain = item.uri.spec;
try {
domain = item.uri.host;
}
catch (err) {}
this.Assert.equal(node.querySelector(".item-domain").textContent, domain,
"Node's domain element's text should match item title");
},

View File

@ -32,14 +32,12 @@ add_task(function* prepare() {
gItems = [];
for (let i = 0; i < 3; i++) {
gItems.push({
list: gList,
guid: `guid${i}`,
url: `http://example.com/${i}`,
resolvedURL: `http://example.com/resolved/${i}`,
title: `title ${i}`,
excerpt: `excerpt ${i}`,
unread: 0,
addedOn: Date.now(),
lastModified: Date.now(),
favorite: 0,
isArticle: 1,
@ -63,14 +61,11 @@ add_task(function* item_properties() {
Assert.ok(item.uri);
Assert.ok(item.uri instanceof Ci.nsIURI);
Assert.equal(item.uri.spec, item.url);
Assert.equal(item.uri.spec, item._record.url);
Assert.ok(item.resolvedURI);
Assert.ok(item.resolvedURI instanceof Ci.nsIURI);
Assert.equal(item.resolvedURI.spec, item.resolvedURL);
Assert.ok(item.lastModified);
Assert.ok(item.lastModified instanceof Cu.getGlobalForObject(ReadingList).Date);
Assert.equal(item.resolvedURI.spec, item._record.resolvedURL);
Assert.ok(item.addedOn);
Assert.ok(item.addedOn instanceof Cu.getGlobalForObject(ReadingList).Date);
@ -82,8 +77,7 @@ add_task(function* item_properties() {
Assert.ok(typeof(item.isArticle) == "boolean");
Assert.ok(typeof(item.unread) == "boolean");
Assert.equal(item.domain, "example.com");
Assert.equal(item.id, hash(item.url));
Assert.equal(item.id, hash(item._record.url));
});
add_task(function* constraints() {
@ -121,18 +115,6 @@ add_task(function* constraints() {
}
checkError(err);
// update an item with an existing url
let rlitem = yield gList.getItemForURL(gItems[0].url);
rlitem.guid = gItems[1].guid;
err = null;
try {
yield gList.updateItem(rlitem);
}
catch (e) {
err = e;
}
checkError(err);
// add a new item with an existing resolvedURL
item = kindOfClone(gItems[0]);
item.resolvedURL = gItems[0].resolvedURL;
@ -145,18 +127,32 @@ add_task(function* constraints() {
}
checkError(err);
// update an item with an existing resolvedURL
rlitem = yield gList.getItemForURL(gItems[0].url);
rlitem.url = gItems[1].url;
// add a new item with no url
item = kindOfClone(gItems[0]);
delete item.url;
err = null;
try {
yield gList.updateItem(rlitem);
yield gList.addItem(item);
}
catch (e) {
err = e;
}
checkError(err);
// add an item with a bogus property
item = kindOfClone(gItems[0]);
item.bogus = "gnarly";
err = null;
try {
yield gList.addItem(item);
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("Unrecognized item property:") >= 0);
// add a new item with no guid, which is allowed
item = kindOfClone(gItems[0]);
delete item.guid;
@ -183,24 +179,13 @@ add_task(function* constraints() {
}
Assert.ok(!err, err ? err.message : undefined);
// Delete both items since other tests assume the store contains only gItems.
// Delete the two previous items since other tests assume the store contains
// only gItems.
yield gList.deleteItem(rlitem1);
yield gList.deleteItem(rlitem2);
let items = [];
yield gList.forEachItem(i => items.push(i), { url: [rlitem1.url, rlitem2.url] });
yield gList.forEachItem(i => items.push(i), { url: [rlitem1.uri.spec, rlitem2.uri.spec] });
Assert.equal(items.length, 0);
// add a new item with no url
item = kindOfClone(gItems[0]);
delete item.url;
err = null;
try {
yield gList.addItem(item);
}
catch (e) {
err = e;
}
checkError(err);
});
add_task(function* count() {
@ -506,6 +491,22 @@ add_task(function* iterator_forEach_promise() {
checkItems(items, gItems);
});
add_task(function* item() {
let item = yield gList.item({ guid: gItems[0].guid });
checkItems([item], [gItems[0]]);
item = yield gList.item({ guid: gItems[1].guid });
checkItems([item], [gItems[1]]);
});
add_task(function* itemForURL() {
let item = yield gList.itemForURL(gItems[0].url);
checkItems([item], [gItems[0]]);
item = yield gList.itemForURL(gItems[1].url);
checkItems([item], [gItems[1]]);
});
add_task(function* updateItem() {
// get an item
let items = [];
@ -531,7 +532,7 @@ add_task(function* updateItem() {
Assert.equal(item.title, newTitle);
});
add_task(function* item_setProperties() {
add_task(function* item_setRecord() {
// get an item
let iter = gList.iterator({
sort: "guid",
@ -539,12 +540,12 @@ add_task(function* item_setProperties() {
let item = (yield iter.items(1))[0];
Assert.ok(item);
// item.setProperties(update=false). After fetching the item again, its title
// should be the old title.
// Set item._record without an updateItem. After fetching the item again, its
// title should be the old title.
let oldTitle = item.title;
let newTitle = "item_setProperties title 1";
let newTitle = "item_setRecord title 1";
Assert.notEqual(oldTitle, newTitle);
item.setProperties({ title: newTitle }, false);
item._record.title = newTitle;
Assert.equal(item.title, newTitle);
iter = gList.iterator({
sort: "guid",
@ -553,10 +554,11 @@ add_task(function* item_setProperties() {
Assert.ok(item === sameItem);
Assert.equal(sameItem.title, oldTitle);
// item.setProperties(update=true). After fetching the item again, its title
// should be the new title.
newTitle = "item_setProperties title 2";
item.setProperties({ title: newTitle }, true);
// Set item._record followed by an updateItem. After fetching the item again,
// its title should be the new title.
newTitle = "item_setRecord title 2";
item._record.title = newTitle;
yield gList.updateItem(item);
Assert.equal(item.title, newTitle);
iter = gList.iterator({
sort: "guid",
@ -565,11 +567,11 @@ add_task(function* item_setProperties() {
Assert.ok(item === sameItem);
Assert.equal(sameItem.title, newTitle);
// Set item.title directly. After fetching the item again, its title should
// be the new title.
newTitle = "item_setProperties title 3";
// Set item.title directly and call updateItem. After fetching the item
// again, its title should be the new title.
newTitle = "item_setRecord title 3";
item.title = newTitle;
gList.updateItem(item);
yield gList.updateItem(item);
Assert.equal(item.title, newTitle);
iter = gList.iterator({
sort: "guid",
@ -577,6 +579,18 @@ add_task(function* item_setProperties() {
sameItem = (yield iter.items(1))[0];
Assert.ok(item === sameItem);
Assert.equal(sameItem.title, newTitle);
// Setting _record to an object with a bogus property should throw.
let err = null;
try {
item._record = { bogus: "gnarly" };
}
catch (e) {
err = e;
}
Assert.ok(err);
Assert.ok(err.message);
Assert.ok(err.message.indexOf("Unrecognized item property:") >= 0);
});
add_task(function* listeners() {
@ -602,7 +616,7 @@ add_task(function* listeners() {
};
gList.addListener(listener);
items[0].title = "listeners new title";
gList.updateItem(items[0]);
yield gList.updateItem(items[0]);
let listenerItem = yield listenerPromise;
Assert.ok(listenerItem);
Assert.ok(listenerItem === items[0]);
@ -666,11 +680,10 @@ function checkItems(actualItems, expectedItems) {
for (let i = 0; i < expectedItems.length; i++) {
for (let prop in expectedItems[i]) {
if (prop != "list") {
Assert.ok(prop in actualItems[i]._properties, prop);
Assert.equal(actualItems[i]._properties[prop], expectedItems[i][prop]);
Assert.ok(prop in actualItems[i]._record, prop);
Assert.equal(actualItems[i]._record[prop], expectedItems[i][prop]);
}
}
Assert.equal(actualItems[i].list, expectedItems[i].list);
}
}

View File

@ -5,9 +5,9 @@
<!-- These strings are used in the about:newtab page -->
<!ENTITY newtab.pageTitle "New Tab">
<!ENTITY newtab.customize.title "Customize your New Tab page">
<!ENTITY newtab.customize.enhanced "Enhanced">
<!ENTITY newtab.customize.classic "Classic">
<!ENTITY newtab.customize.blank "Blank">
<!ENTITY newtab.customize.suggested "Show suggested and your top sites">
<!ENTITY newtab.customize.topsites "Show your top sites">
<!ENTITY newtab.customize.blank2 "Show blank page">
<!ENTITY newtab.customize.what "What is this page?">
<!ENTITY newtab.intro.header "What is this page?">
<!ENTITY newtab.undo.removedLabel "Thumbnail removed.">

View File

@ -9,6 +9,11 @@ newtab.block=Remove this site
# and enhanced tiles on the same line as the tile's title, so prefer short
# strings to avoid overlap. This string should be uppercase.
newtab.sponsored.button=SPONSORED
# LOCALIZATION NOTE(newtab.suggested.button): %1$S will be replaced inline by
# one of the user's top 100 sites that triggered this suggested tile.
# This text appears for suggested tiles under the tile's title, so prefer short
# strings to avoid truncating important text.
newtab.suggested.button=Suggested for %1$S visitors
# LOCALIZATION NOTE(newtab.sponsored.explain): %1$S will be replaced inline by
# the (X) block icon. %2$S will be replaced by an active link using string
# newtab.learn.link as text.

View File

@ -563,10 +563,18 @@ let DirectoryLinksProvider = {
// from url to relatedLink. Thus, each link has an equal chance of being chosen at
// random from flattenedLinks if it appears only once.
let possibleLinks = new Map();
let targetedSites = new Map();
this._topSitesWithRelatedLinks.forEach(topSiteWithRelatedLink => {
let relatedLinksMap = this._relatedLinks.get(topSiteWithRelatedLink);
relatedLinksMap.forEach((relatedLink, url) => {
possibleLinks.set(url, relatedLink);
// Keep a map of URL to targeted sites. We later use this to show the user
// what site they visited to trigger this suggestion.
if (!targetedSites.get(url)) {
targetedSites.set(url, []);
}
targetedSites.get(url).push(topSiteWithRelatedLink);
})
});
let flattenedLinks = [...possibleLinks.values()];
@ -578,9 +586,16 @@ let DirectoryLinksProvider = {
// Show the new directory tile.
this._callObservers("onLinkChanged", {
url: chosenRelatedLink.url,
title: chosenRelatedLink.title,
frecency: RELATED_FRECENCY,
lastVisitDate: chosenRelatedLink.lastVisitDate,
type: "related",
// Choose the first site a user has visited as the target. In the future,
// this should be the site with the highest frecency. However, we currently
// store frecency by URL not by site.
targetedSite: targetedSites.get(chosenRelatedLink.url).length ?
targetedSites.get(chosenRelatedLink.url)[0] : null
});
return chosenRelatedLink;
},

View File

@ -60,7 +60,7 @@ let ReaderParent = {
break;
}
case "Reader:ListStatusRequest":
ReadingList.containsURL(message.data.url).then(inList => {
ReadingList.hasItemForURL(message.data.url).then(inList => {
let mm = message.target.messageManager
// Make sure the target browser is still alive before trying to send data back.
if (mm) {
@ -72,7 +72,7 @@ let ReaderParent = {
case "Reader:RemoveFromList":
// We need to get the "real" item to delete it.
ReadingList.getItemForURL(message.data.url).then(item => {
ReadingList.itemForURL(message.data.url).then(item => {
ReadingList.deleteItem(item)
});
break;

View File

@ -122,7 +122,7 @@
transition: opacity 100ms ease-out;
}
.newtab-site:hover .newtab-thumbnail.enhanced-content {
.newtab-cell:not([ignorehover]) .newtab-site:hover .newtab-thumbnail.enhanced-content {
opacity: 0;
}
@ -137,10 +137,15 @@
/* TITLES */
#newtab-intro-what,
.newtab-sponsored,
.newtab-title {
.newtab-title,
.newtab-suggested {
color: #5c5c5c;
}
.newtab-suggested {
background-color: white;
}
.newtab-site:hover .newtab-title {
color: #222;
}

View File

@ -51,7 +51,7 @@ body {
box-shadow: 0px 1px 2px rgba(0,0,0,.35);
margin: 5px;
background-color: #fff;
background-size: contain;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-image: url("chrome://branding/content/silhouette-40.svg");

View File

@ -166,7 +166,8 @@ enum class DeviceResetReason
REMOVED,
RESET,
DRIVER_ERROR,
INVALID_CALL
INVALID_CALL,
OUT_OF_MEMORY
};
class gfxPlatform {

View File

@ -1168,6 +1168,10 @@ gfxWindowsPlatform::DidRenderingDeviceReset(DeviceResetReason* aResetReason)
break;
case DXGI_ERROR_INVALID_CALL:
*aResetReason = DeviceResetReason::INVALID_CALL;
break;
case E_OUTOFMEMORY:
*aResetReason = DeviceResetReason::OUT_OF_MEMORY;
break;
default:
MOZ_ASSERT(false);
}

View File

@ -63,39 +63,7 @@ add_test(function test_passwords_list() {
let username = logins_list.querySelector(".username");
do_check_eq(username.textContent, LOGIN_FIELDS.username);
let login_item = browser.contentDocument.querySelector("#logins-list > .login-item");
browser.addEventListener("PasswordsDetailsLoad", function() {
browser.removeEventListener("PasswordsDetailsLoad", this, false);
Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
}, false);
// Expand item details.
login_item.click();
});
add_test(function test_passwords_details() {
let login_details = browser.contentDocument.getElementById("login-details");
let hostname = login_details.querySelector(".hostname");
do_check_eq(hostname.textContent, LOGIN_FIELDS.hostname);
let username = login_details.querySelector(".username");
do_check_eq(username.textContent, LOGIN_FIELDS.username);
// Check that details page opens link to host.
BrowserApp.deck.addEventListener("TabOpen", (tabevent) => {
// Wait for tab to finish loading.
let browser_target = tabevent.target;
browser_target.addEventListener("load", () => {
browser_target.removeEventListener("load", this, true);
do_check_eq(BrowserApp.selectedTab.browser.currentURI.spec, LOGIN_FIELDS.hostname);
Services.tm.mainThread.dispatch(run_next_test, Ci.nsIThread.DISPATCH_NORMAL);
}, true);
BrowserApp.deck.removeEventListener("TabOpen", this, false);
}, false);
browser.contentDocument.getElementById("details-header").click();
run_next_test();
});
run_next_test();

View File

@ -17,6 +17,9 @@ XPCOMUtils.defineLazyGetter(window, "gChromeWin", function()
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow));
XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
"resource://gre/modules/Prompt.jsm");
let debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "AboutPasswords");
let gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutPasswords.properties");
@ -125,9 +128,33 @@ let Passwords = {
loginItem.setAttribute("loginID", login.guid);
loginItem.className = "login-item list-item";
loginItem.addEventListener("click", () => {
this._showDetails(loginItem);
history.pushState({ id: login.guid }, document.title);
let prompt = new Prompt({
window: window,
});
let menuItems = [
{ label: gStringBundle.GetStringFromName("passwordsMenu.copyPassword") },
{ label: gStringBundle.GetStringFromName("passwordsMenu.copyUsername") },
{ label: gStringBundle.GetStringFromName("passwordsMenu.details") } ];
prompt.setSingleChoiceItems(menuItems);
prompt.show((data) => {
// Switch on indices of buttons, as they were added when creating login item.
switch (data.button) {
case 0:
copyStringAndToast(login.password, gStringBundle.GetStringFromName("passwordsDetails.passwordCopied"));
break;
case 1:
copyStringAndToast(login.username, gStringBundle.GetStringFromName("passwordsDetails.usernameCopied"));
break;
case 2:
this._showDetails(loginItem);
history.pushState({ id: login.guid }, document.title);
break;
}
});
}, true);
// Create item icon.

View File

@ -2,6 +2,10 @@
# 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/.
passwordsMenu.copyPassword=Copy password
passwordsMenu.copyUsername=Copy username
passwordsMenu.details=Details
passwordsDetails.age=Age: %S days
passwordsDetails.copyFailed=Copy failed

View File

@ -214,7 +214,7 @@
"expires_in_version": "never",
"kind": "enumerated",
"n_values": 10,
"description": "GPU Device Reset Reason (ok, hung, removed, reset, internal error, invalid call)"
"description": "GPU Device Reset Reason (ok, hung, removed, reset, internal error, invalid call, out of memory)"
},
"FORGET_SKIPPABLE_MAX": {
"expires_in_version": "never",

View File

@ -935,7 +935,12 @@ let Links = {
_getMergedProviderLinks: function Links__getMergedProviderLinks() {
// Build a list containing a copy of each provider's sortedLinks list.
let linkLists = [];
for (let links of this._providers.values()) {
for (let provider of this._providers.keys()) {
if (!AllPages.enhanced && provider != PlacesProvider) {
// Only show history tiles if we're not in 'enhanced' mode.
continue;
}
let links = this._providers.get(provider);
if (links && links.sortedLinks) {
linkLists.push(links.sortedLinks.slice());
}
@ -1248,11 +1253,19 @@ this.NewTabUtils = {
},
getProviderLinks: function(aProvider) {
return Links._providers.get(aProvider).sortedLinks;
let cache = Links._providers.get(aProvider);
if (cache && cache.sortedLinks) {
return cache.sortedLinks;
}
return [];
},
isTopSiteGivenProvider: function(aSite, aProvider) {
return Links._providers.get(aProvider).siteMap.has(aSite);
let cache = Links._providers.get(aProvider);
if (cache && cache.siteMap) {
return cache.siteMap.has(aSite);
}
return false;
},
isTopPlacesSite: function(aSite) {

View File

@ -7,11 +7,38 @@ const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/NewTabUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
function run_test() {
Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, true);
run_next_test();
}
add_task(function validCacheMidPopulation() {
let expectedLinks = makeLinks(0, 3, 1);
let provider = new TestProvider(done => done(expectedLinks));
provider.maxNumLinks = expectedLinks.length;
NewTabUtils.initWithoutProviders();
NewTabUtils.links.addProvider(provider);
let promise = new Promise(resolve => NewTabUtils.links.populateCache(resolve));
// isTopSiteGivenProvider() and getProviderLinks() should still return results
// even when cache is empty or being populated.
do_check_false(NewTabUtils.isTopSiteGivenProvider("example1.com", provider));
do_check_links(NewTabUtils.getProviderLinks(provider), []);
yield promise;
// Once the cache is populated, we get the expected results
do_check_true(NewTabUtils.isTopSiteGivenProvider("example1.com", provider));
do_check_links(NewTabUtils.getProviderLinks(provider), expectedLinks);
NewTabUtils.links.removeProvider(provider);
});
add_task(function notifyLinkDelete() {
let expectedLinks = makeLinks(0, 3, 1);