Update On Thu Oct 24 20:51:06 CEST 2024

This commit is contained in:
github-action[bot] 2024-10-24 20:51:07 +02:00
parent e5dbd50651
commit cc861e1a90
620 changed files with 16257 additions and 10130 deletions

8
Cargo.lock generated
View file

@ -743,6 +743,7 @@ dependencies = [
"rkv",
"rust_cascade",
"sha2",
"static_prefs",
"storage_variant",
"tempfile",
"thin-vec",
@ -1103,6 +1104,7 @@ dependencies = [
"libloading",
"lmdb-rkv-sys",
"log",
"minidump-analyzer",
"mozbuild",
"mozilla-central-workspace-hack",
"objc",
@ -3870,15 +3872,12 @@ dependencies = [
"anyhow",
"async-trait",
"breakpad-symbols",
"clap",
"env_logger",
"futures-executor",
"futures-util",
"lazy_static",
"log",
"minidump",
"minidump-unwind",
"mozilla-central-workspace-hack",
"serde_json",
"windows-sys",
]
@ -4069,7 +4068,6 @@ dependencies = [
"futures",
"futures-channel",
"futures-core",
"futures-executor",
"futures-sink",
"futures-util",
"getrandom",
@ -5433,7 +5431,7 @@ dependencies = [
[[package]]
name = "selectors"
version = "0.25.0"
version = "0.26.0"
dependencies = [
"bitflags 2.6.0",
"cssparser",

View file

@ -17,7 +17,6 @@ members = [
"testing/geckodriver",
"toolkit/components/uniffi-bindgen-gecko-js",
"toolkit/crashreporter/client/app",
"toolkit/crashreporter/minidump-analyzer",
"toolkit/crashreporter/mozwer-rust",
"toolkit/crashreporter/rust_minidump_writer_linux",
"toolkit/library/gtest/rust",
@ -41,6 +40,7 @@ exclude = [
"media/mp4parse-rust/mp4parse",
"media/mp4parse-rust/mp4parse_capi",
"xpcom/rust/gkrust_utils",
"toolkit/crashreporter/minidump-analyzer",
"tools/lint/test/files/clippy",
"tools/fuzzing/rust",
"dom/base/rust",

View file

@ -240,6 +240,7 @@ export class PromptParent extends JSWindowActorParent {
},
bag
);
dialog.promptID = promptID;
this.registerDialog(dialog, promptID);
await closedPromise;
} finally {

View file

@ -13,9 +13,6 @@
#if defined(MOZ_ASAN) || defined(MOZ_TSAN) || defined(FUZZING)
/llvm-symbolizer
#endif
#if defined(MOZ_CRASHREPORTER)
/minidump-analyzer
#endif
/nmhproxy
/pingsender
/pk12util

View file

@ -402,6 +402,9 @@ pref("browser.urlbar.autoFill.adaptiveHistory.enabled", false);
// autofill.
pref("browser.urlbar.autoFill.adaptiveHistory.minCharsThreshold", 0);
// Set default NER threshold value of 0.5
pref("browser.urlbar.nerThreshold", "0.5");
// Whether to warm up network connections for autofill or search results.
pref("browser.urlbar.speculativeConnect.enabled", true);
@ -1483,7 +1486,7 @@ pref("browser.bookmarks.editDialog.maxRecentFolders", 7);
// On windows these levels are:
// See - security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
// SetSecurityLevelForContentProcess() for what the different settings mean.
#if defined(NIGHTLY_BUILD)
#if defined(EARLY_BETA_OR_EARLIER)
pref("security.sandbox.content.level", 8);
#else
pref("security.sandbox.content.level", 7);
@ -2651,6 +2654,7 @@ pref("browser.toolbars.bookmarks.showOtherBookmarks", true);
// Felt Privacy pref to control simplified private browsing UI
pref("browser.privatebrowsing.felt-privacy-v1", false);
pref("security.certerrors.felt-privacy-v1", false);
// Prefs to control the Firefox Account toolbar menu.
// This pref will surface existing Firefox Account information

View file

@ -898,6 +898,7 @@ var gIdentityHandler = {
warnTextOnInsecure
) {
icon_label = gNavigatorBundle.getString("identity.notSecure.label");
tooltip = gNavigatorBundle.getString("identity.notSecure.tooltip");
this._identityBox.classList.add("notSecureText");
}
} else if (this._isMixedActiveContentBlocked) {

View file

@ -96,10 +96,7 @@
<hbox class="titlebar-spacer" type="post-tabs"/>
<hbox id="private-browsing-indicator-with-label">
<image class="private-browsing-indicator-icon"/>
<label data-l10n-id="private-browsing-indicator-label"></label>
</hbox>
#include private-browsing-indicator.inc.xhtml
<toolbarbutton id="content-analysis-indicator"
class="toolbarbutton-1 content-analysis-indicator-icon"/>
@ -490,6 +487,8 @@
data-l10n-id="appmenu-menu-button-closed2"/>
</toolbaritem>
<hbox class="titlebar-spacer" type="post-tabs"/>
#include private-browsing-indicator.inc.xhtml
#include titlebar-items.inc.xhtml
</toolbar>

View file

@ -0,0 +1,8 @@
# 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/.
<hbox class="private-browsing-indicator-with-label">
<image data-l10n-id="private-browsing-indicator-tooltip" class="private-browsing-indicator-icon"/>
<label data-l10n-id="private-browsing-indicator-label" class="private-browsing-indicator-label"></label>
</hbox>

View file

@ -10,68 +10,76 @@
// dialog, using that to add an exception, and finally successfully visiting
// the site, including showing the right identity box and control center icons.
add_task(async function () {
await BrowserTestUtils.openNewForegroundTab(gBrowser);
await loadBadCertPage("https://expired.example.com");
for (let feltPrivacyEnabled of [true, false]) {
await SpecialPowers.pushPrefEnv({
set: [["security.certerrors.felt-privacy-v1", feltPrivacyEnabled]],
});
let { gIdentityHandler } = gBrowser.ownerGlobal;
let promisePanelOpen = BrowserTestUtils.waitForEvent(
gBrowser.ownerGlobal,
"popupshown",
true,
event => event.target == gIdentityHandler._identityPopup
);
gIdentityHandler._identityIconBox.click();
await promisePanelOpen;
await BrowserTestUtils.openNewForegroundTab(gBrowser);
await loadBadCertPage("https://expired.example.com", feltPrivacyEnabled);
let promiseViewShown = BrowserTestUtils.waitForEvent(
gIdentityHandler._identityPopup,
"ViewShown"
);
document.getElementById("identity-popup-security-button").click();
await promiseViewShown;
let { gIdentityHandler } = gBrowser.ownerGlobal;
let promisePanelOpen = BrowserTestUtils.waitForEvent(
gBrowser.ownerGlobal,
"popupshown",
true,
event => event.target == gIdentityHandler._identityPopup
);
gIdentityHandler._identityIconBox.click();
await promisePanelOpen;
is_element_visible(
document.getElementById("identity-icon"),
"Should see identity icon"
);
let identityIconImage = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-icon"))
.getPropertyValue("list-style-image");
let securityViewBG = gBrowser.ownerGlobal
.getComputedStyle(
document
.getElementById("identity-popup-securityView")
.getElementsByClassName("identity-popup-security-connection")[0]
)
.getPropertyValue("list-style-image");
let securityContentBG = gBrowser.ownerGlobal
.getComputedStyle(
document
.getElementById("identity-popup-mainView")
.getElementsByClassName("identity-popup-security-connection")[0]
)
.getPropertyValue("list-style-image");
is(
identityIconImage,
'url("chrome://global/skin/icons/security-warning.svg")',
"Using expected icon image in the identity block"
);
is(
securityViewBG,
'url("chrome://global/skin/icons/security-warning.svg")',
"Using expected icon image in the Control Center main view"
);
is(
securityContentBG,
'url("chrome://global/skin/icons/security-warning.svg")',
"Using expected icon image in the Control Center subview"
);
let promiseViewShown = BrowserTestUtils.waitForEvent(
gIdentityHandler._identityPopup,
"ViewShown"
);
document.getElementById("identity-popup-security-button").click();
await promiseViewShown;
gIdentityHandler._identityPopup.hidePopup();
is_element_visible(
document.getElementById("identity-icon"),
"Should see identity icon"
);
let identityIconImage = gBrowser.ownerGlobal
.getComputedStyle(document.getElementById("identity-icon"))
.getPropertyValue("list-style-image");
let securityViewBG = gBrowser.ownerGlobal
.getComputedStyle(
document
.getElementById("identity-popup-securityView")
.getElementsByClassName("identity-popup-security-connection")[0]
)
.getPropertyValue("list-style-image");
let securityContentBG = gBrowser.ownerGlobal
.getComputedStyle(
document
.getElementById("identity-popup-mainView")
.getElementsByClassName("identity-popup-security-connection")[0]
)
.getPropertyValue("list-style-image");
is(
identityIconImage,
'url("chrome://global/skin/icons/security-warning.svg")',
"Using expected icon image in the identity block"
);
is(
securityViewBG,
'url("chrome://global/skin/icons/security-warning.svg")',
"Using expected icon image in the Control Center main view"
);
is(
securityContentBG,
'url("chrome://global/skin/icons/security-warning.svg")',
"Using expected icon image in the Control Center subview"
);
let certOverrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
certOverrideService.clearValidityOverride("expired.example.com", -1, {});
BrowserTestUtils.removeTab(gBrowser.selectedTab);
gIdentityHandler._identityPopup.hidePopup();
let certOverrideService = Cc[
"@mozilla.org/security/certoverride;1"
].getService(Ci.nsICertOverrideService);
certOverrideService.clearValidityOverride("expired.example.com", -1, {});
BrowserTestUtils.removeTab(gBrowser.selectedTab);
await SpecialPowers.popPrefEnv();
}
});

View file

@ -1,59 +1,89 @@
function remote(task) {
return SpecialPowers.spawn(gBrowser.selectedBrowser, [], task);
}
add_task(async function () {
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
for (let feltPrivacyEnabled of [false, true]) {
await SpecialPowers.pushPrefEnv({
set: [["security.certerrors.felt-privacy-v1", feltPrivacyEnabled]],
});
let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
BrowserTestUtils.startLoadingURIString(
gBrowser,
"https://nocert.example.com/"
);
await promise;
gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
await remote(() => {
// Confirm that we are displaying the contributed error page, not the default
let uri = content.document.documentURI;
Assert.ok(
uri.startsWith("about:certerror"),
"Broken page should go to about:certerror, not about:neterror"
let promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
BrowserTestUtils.startLoadingURIString(
gBrowser,
"https://nocert.example.com/"
);
});
await promise;
await remote(() => {
let div = content.document.getElementById("badCertAdvancedPanel");
// Confirm that the expert section is collapsed
Assert.ok(div, "Advanced content div should exist");
Assert.equal(
div.ownerGlobal.getComputedStyle(div).display,
"none",
"Advanced content should not be visible by default"
);
});
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
// Confirm that we are displaying the contributed error page, not the default
let uri = content.document.documentURI;
Assert.ok(
uri.startsWith("about:certerror"),
"Broken page should go to about:certerror, not about:neterror"
);
});
// Tweak the expert mode pref
Services.prefs.setBoolPref("browser.xul.error_pages.expert_bad_cert", true);
if (feltPrivacyEnabled) {
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
let netErrorCard =
content.document.querySelector("net-error-card").wrappedJSObject;
await netErrorCard.getUpdateComplete();
Assert.ok(
!netErrorCard.advancedShowing,
"Advanced showing attribute should be true"
);
Assert.ok(!netErrorCard.advancedContainer);
});
} else {
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
let div = content.document.getElementById("badCertAdvancedPanel");
// Confirm that the expert section is collapsed
Assert.ok(div, "Advanced content div should exist");
Assert.equal(
div.ownerGlobal.getComputedStyle(div).display,
"none",
"Advanced content should not be visible by default"
);
});
}
promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
gBrowser.reload();
await promise;
// Tweak the expert mode pref
Services.prefs.setBoolPref("browser.xul.error_pages.expert_bad_cert", true);
await remote(() => {
let div = content.document.getElementById("badCertAdvancedPanel");
Assert.ok(div, "Advanced content div should exist");
Assert.equal(
div.ownerGlobal.getComputedStyle(div).display,
"block",
"Advanced content should be visible by default"
);
});
promise = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
gBrowser.reload();
await promise;
// Clean up
gBrowser.removeCurrentTab();
if (
Services.prefs.prefHasUserValue("browser.xul.error_pages.expert_bad_cert")
) {
Services.prefs.clearUserPref("browser.xul.error_pages.expert_bad_cert");
if (feltPrivacyEnabled) {
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
let netErrorCard =
content.document.querySelector("net-error-card").wrappedJSObject;
await netErrorCard.getUpdateComplete();
Assert.ok(
netErrorCard.advancedShowing,
"Advanced showing attribute should be true"
);
Assert.ok(ContentTaskUtils.isVisible(netErrorCard.advancedContainer));
});
} else {
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
let div = content.document.getElementById("badCertAdvancedPanel");
Assert.ok(div, "Advanced content div should exist");
Assert.equal(
div.ownerGlobal.getComputedStyle(div).display,
"block",
"Advanced content should be visible by default"
);
});
}
// Clean up
gBrowser.removeCurrentTab();
if (
Services.prefs.prefHasUserValue("browser.xul.error_pages.expert_bad_cert")
) {
Services.prefs.clearUserPref("browser.xul.error_pages.expert_bad_cert");
}
await SpecialPowers.popPrefEnv();
}
});

View file

@ -289,13 +289,31 @@ function promiseOnBookmarkItemAdded(aExpectedURI) {
});
}
async function loadBadCertPage(url) {
async function loadBadCertPage(url, feltPrivacy = false) {
let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
await loaded;
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
content.document.getElementById("exceptionDialogButton").click();
});
await SpecialPowers.spawn(
gBrowser.selectedBrowser,
[feltPrivacy],
async isFeltPrivacy => {
if (isFeltPrivacy) {
let netErrorCard =
content.document.querySelector("net-error-card").wrappedJSObject;
await netErrorCard.getUpdateComplete();
netErrorCard.advancedButton.click();
await ContentTaskUtils.waitForCondition(() => {
return (
netErrorCard.exceptionButton &&
!netErrorCard.exceptionButton.disabled
);
}, "Waiting for exception button");
netErrorCard.exceptionButton.click();
} else {
content.document.getElementById("exceptionDialogButton").click();
}
}
);
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
}

View file

@ -122,6 +122,9 @@ if (AppConstants.MOZ_BACKGROUNDTASKS) {
// referencing the listed file in a way that the test can't detect, or a
// bug number to remove or use the file if it is indeed currently unreferenced.
var allowlist = [
// See bug 1926381 for why this is hard to fix.
{ file: "resource://app/modules/urlbar/private/MLSuggest.sys.mjs" },
// security/manager/pki/resources/content/device_manager.js
{ file: "chrome://pippki/content/load_device.xhtml" },

View file

@ -137,6 +137,7 @@ export const SingleSelect = ({
className={`select-item ${type}`}
title={value}
onKeyDown={e => handleKeyDown(e)}
style={icon?.width ? { minWidth: icon.width } : {}}
>
{flair ? (
<Localized text={valOrObj(flair.text)}>

View file

@ -1635,7 +1635,10 @@ const SingleSelect = ({
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
className: `select-item ${type}`,
title: value,
onKeyDown: e => handleKeyDown(e)
onKeyDown: e => handleKeyDown(e),
style: icon?.width ? {
minWidth: icon.width
} : {}
}, flair ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
text: valOrObj(flair.text)
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {

View file

@ -54,116 +54,58 @@ XPCOMUtils.defineLazyPreferenceGetter(
* A class that groups browsing contexts by their top-level one.
* This is necessary because if there may be a subframe that
* is showing a "DLP request busy" dialog when another subframe
* (other the outer frame) wants to show one. This class makes it
* convenient to find if another frame with the same top browsing
* context is currently showing a dialog, and also to find if there
* are any pending dialogs to show when one closes.
* (other than the outer frame) wants to show one. This is also needed
* because there may be multiple requests active for a given top-level
* or subframe.
*
* This class makes it convenient to find if another frame with
* the same top browsing context is currently showing a dialog, and
* also to find if there are any pending dialogs to show when one closes.
*/
class MapByTopBrowsingContext {
/**
* A map from top-level browsing context to
* a map from browsing context to a list of entries
*
* @type {Map<BrowsingContext, Map<BrowsingContext, Array<object>>>}
*/
#map;
constructor() {
this.#map = new Map();
}
/**
* Gets any existing data associated with the browsing context
* Gets a specific request associated with the browsing context
*
* @param {BrowsingContext} aBrowsingContext the browsing context to search for
* @param {string} aRequestToken the request token to search for
* @returns {object | undefined} the existing data, or `undefined` if there is none
*/
getEntry(aBrowsingContext) {
getAndRemoveEntry(aBrowsingContext, aRequestToken) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return undefined;
}
return topEntry.get(aBrowsingContext);
}
/**
* Returns whether the browsing context has any data associated with it
*
* @param {BrowsingContext} aBrowsingContext the browsing context to search for
* @returns {boolean} Whether the browsing context has any associated data
*/
hasEntry(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return false;
}
return topEntry.has(aBrowsingContext);
}
/**
* Whether the tab containing the browsing context has a dialog
* currently showing
*
* @param {BrowsingContext} aBrowsingContext the browsing context to search for
* @returns {boolean} whether the tab has a dialog currently showing
*/
hasEntryDisplayingNotification(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return false;
}
for (const otherEntry in topEntry.values()) {
if (otherEntry.notification?.dialogBrowsingContext) {
return true;
}
}
return false;
}
/**
* Gets another browsing context in the same tab that has pending "DLP busy" dialog
* info to show, if any.
*
* @param {BrowsingContext} aBrowsingContext the browsing context to search for
* @returns {BrowsingContext} Another browsing context in the same tab that has pending "DLP busy" dialog info, or `undefined` if there aren't any.
*/
getBrowsingContextWithPendingNotification(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
const browsingContextEntries = topEntry.get(aBrowsingContext);
if (!browsingContextEntries) {
return undefined;
}
if (aBrowsingContext.top.isDiscarded) {
// The top-level tab has already been closed, so remove
// the top-level entry and return there are no pending dialogs.
this.#map.delete(aBrowsingContext.top);
return undefined;
}
for (const otherContext in topEntry.keys()) {
if (
topEntry.get(otherContext).notification?.dialogBrowsingContextArgs &&
otherContext !== aBrowsingContext
) {
return otherContext;
for (let i = 0; i < browsingContextEntries.length; i++) {
if (browsingContextEntries[i].request.requestToken === aRequestToken) {
// Remove and return this entry
return browsingContextEntries.splice(i, 1)[0];
}
}
return undefined;
}
/**
* Deletes the entry for the browsing context, if any
*
* @param {BrowsingContext} aBrowsingContext the browsing context to delete
* @returns {boolean} Whether an entry was deleted or not
*/
deleteEntry(aBrowsingContext) {
const topEntry = this.#map.get(aBrowsingContext.top);
if (!topEntry) {
return false;
}
const toReturn = topEntry.delete(aBrowsingContext);
if (!topEntry.size || aBrowsingContext.top.isDiscarded) {
// Either the inner Map is now empty, or the whole tab
// has been closed. Either way, remove the top-level entry.
this.#map.delete(aBrowsingContext.top);
}
return toReturn;
}
/**
* Sets the associated data for the browsing context
* Adds or replaces the associated entry for the browsing context
*
* @param {BrowsingContext} aBrowsingContext the browsing context to set the data for
* @param {object} aValue the data to associated with the browsing context
* @returns {MapByTopBrowsingContext} this
*/
setEntry(aBrowsingContext, aValue) {
addOrReplaceEntry(aBrowsingContext, aValue) {
if (!aValue.request) {
console.error(
"MapByTopBrowsingContext.setEntry() called with a value without a request!"
@ -174,15 +116,37 @@ class MapByTopBrowsingContext {
topEntry = new Map();
this.#map.set(aBrowsingContext.top, topEntry);
}
topEntry.set(aBrowsingContext, aValue);
let existingEntries = topEntry.get(aBrowsingContext);
if (existingEntries) {
for (let i = 0; i < existingEntries.length; ++i) {
let existingEntry = existingEntries[i];
if (
existingEntry.request.requestToken === aValue.request.requestToken
) {
existingEntries[i] = aValue;
return this;
}
}
existingEntries.push(aValue);
} else {
topEntry.set(aBrowsingContext, [aValue]);
}
return this;
}
/**
* Gets all requests across all browsing contexts
*
* @returns {Array<object>} all the requests
*/
getAllRequests() {
let requests = [];
this.#map.forEach(topEntry => {
for (let entry of topEntry.values()) {
requests.push(entry.request);
this.#map.forEach(topBrowsingContext => {
for (const entries of topBrowsingContext.values()) {
for (const entry of entries) {
requests.push(entry.request);
}
}
});
return requests;
@ -202,10 +166,15 @@ export const ContentAnalysis = {
_RESULT_NOTIFICATION_FAST_TIMEOUT_MS: 60 * 1000, // 1 min
PROMPTID_PREFIX: "ContentAnalysisDialog-",
isInitialized: false,
dlpBusyViewsByTopBrowsingContext: new MapByTopBrowsingContext(),
/**
* @type {Map<string, {browsingContext: BrowsingContext, resourceNameOrOperationType: object}>}
*/
requestTokenToRequestInfo: new Map(),
/**
@ -336,34 +305,32 @@ export const ContentAnalysis = {
// Start timer that, when it expires,
// presents a "slow CA check" message.
// Note that there should only be one DLP request
// at a time per browsingContext (since we block the UI and
// the content process waits synchronously for the result).
if (this.dlpBusyViewsByTopBrowsingContext.hasEntry(browsingContext)) {
throw new Error(
"Got dlp-request-made message for a browsingContext that already has a busy view!"
);
}
let resourceNameOrOperationType =
this._getResourceNameOrOperationTypeFromRequest(request, false);
this.requestTokenToRequestInfo.set(request.requestToken, {
browsingContext,
resourceNameOrOperationType,
});
this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, {
timer: lazy.setTimeout(() => {
this.dlpBusyViewsByTopBrowsingContext.setEntry(browsingContext, {
notification: this._showSlowCAMessage(
analysisType,
request,
resourceNameOrOperationType,
browsingContext
),
request,
});
}, slowTimeoutMs),
request,
});
this.dlpBusyViewsByTopBrowsingContext.addOrReplaceEntry(
browsingContext,
{
timer: lazy.setTimeout(() => {
this.dlpBusyViewsByTopBrowsingContext.addOrReplaceEntry(
browsingContext,
{
notification: this._showSlowCAMessage(
analysisType,
request,
resourceNameOrOperationType,
browsingContext
),
request,
}
);
}, slowTimeoutMs),
request,
}
);
}
break;
case "dlp-response": {
@ -385,15 +352,12 @@ export const ContentAnalysis = {
return;
}
this.requestTokenToRequestInfo.delete(request.requestToken);
let dlpBusyView = this.dlpBusyViewsByTopBrowsingContext.getEntry(
windowAndResourceNameOrOperationType.browsingContext
);
if (dlpBusyView) {
this._disconnectFromView(dlpBusyView);
this.dlpBusyViewsByTopBrowsingContext.deleteEntry(
windowAndResourceNameOrOperationType.browsingContext
let dlpBusyView =
this.dlpBusyViewsByTopBrowsingContext.getAndRemoveEntry(
windowAndResourceNameOrOperationType.browsingContext,
request.requestToken
);
}
this._disconnectFromView(dlpBusyView);
const responseResult =
request?.action ?? Ci.nsIContentAnalysisResponse.eUnspecified;
// Don't show dialog if this is a cached response
@ -406,9 +370,6 @@ export const ContentAnalysis = {
request.cancelError
);
}
this._showAnotherPendingDialog(
windowAndResourceNameOrOperationType.browsingContext
);
break;
}
}
@ -426,25 +387,6 @@ export const ContentAnalysis = {
panelUI.showSubView("content-analysis-panel", element);
},
_showAnotherPendingDialog(aBrowsingContext) {
const otherBrowsingContext =
this.dlpBusyViewsByTopBrowsingContext.getBrowsingContextWithPendingNotification(
aBrowsingContext
);
if (otherBrowsingContext) {
const args =
this.dlpBusyViewsByTopBrowsingContext.getEntry(otherBrowsingContext);
this.dlpBusyViewsByTopBrowsingContext.setEntry(otherBrowsingContext, {
notification: this._showSlowCABlockingMessage(
otherBrowsingContext,
args.requestToken,
args.resourceNameOrOperationType
),
request: args.request,
});
}
},
_disconnectFromView(caView) {
if (!caView) {
return;
@ -463,9 +405,13 @@ export const ContentAnalysis = {
let win = browser?.ownerGlobal;
if (win) {
let dialogBox = win.gBrowser.getTabDialogBox(browser);
// Don't close any content-modal dialogs, because we could be doing
// content analysis on something like a prompt() call.
dialogBox.getTabDialogManager().abortDialogs();
// Just close the dialog associated with this CA request.
dialogBox.getTabDialogManager().abortDialogs(dialog => {
return (
dialog.promptID ==
this.PROMPTID_PREFIX + caView.request.requestToken
);
});
}
} else {
console.error(
@ -597,22 +543,6 @@ export const ContentAnalysis = {
);
}
if (
this.dlpBusyViewsByTopBrowsingContext.hasEntryDisplayingNotification(
aBrowsingContext
)
) {
// This tab already has a frame displaying a "DLP in progress" message, so we can't
// show another one right now. Record the arguments we will need to show another
// "DLP in progress" message when the existing message goes away.
return {
requestToken: aRequest.requestToken,
dialogBrowsingContextArgs: {
resourceNameOrOperationType: aResourceNameOrOperationType,
},
};
}
return this._showSlowCABlockingMessage(
aBrowsingContext,
aRequest.requestToken,
@ -690,6 +620,9 @@ export const ContentAnalysis = {
aResourceNameOrOperationType
) {
let bodyMessage = this._getSlowDialogMessage(aResourceNameOrOperationType);
// Note that TabDialogManager maintains a list of displaying dialogs, and so
// we can pop up multiple of these and the first one will keep displaying until
// it is closed, at which point the next one will display, etc.
let promise = Services.prompt.asyncConfirmEx(
aBrowsingContext,
Ci.nsIPromptService.MODAL_TYPE_TAB,
@ -703,7 +636,8 @@ export const ContentAnalysis = {
null,
null,
null,
false
false,
{ promptID: this.PROMPTID_PREFIX + aRequestToken }
);
promise
.catch(() => {
@ -718,11 +652,11 @@ export const ContentAnalysis = {
if (this.requestTokenToRequestInfo.delete(aRequestToken)) {
lazy.gContentAnalysis.cancelContentAnalysisRequest(aRequestToken);
let dlpBusyView =
this.dlpBusyViewsByTopBrowsingContext.getEntry(aBrowsingContext);
if (dlpBusyView) {
this._disconnectFromView(dlpBusyView);
this.dlpBusyViewsByTopBrowsingContext.deleteEntry(aBrowsingContext);
}
this.dlpBusyViewsByTopBrowsingContext.getAndRemoveEntry(
aBrowsingContext,
aRequestToken
);
this._disconnectFromView(dlpBusyView);
}
});
return {
@ -889,6 +823,8 @@ export const ContentAnalysis = {
"Got unexpected cancel response with eUserInitiated"
);
return null;
case Ci.nsIContentAnalysisResponse.eOtherRequestInGroupCancelled:
return null;
case Ci.nsIContentAnalysisResponse.eNoAgent:
messageId = "contentanalysis-no-agent-connected-message-content";
break;

View file

@ -116,6 +116,9 @@ export function CustomizeMode(aWindow) {
container.lastChild
);
}
this._attachEventListeners();
// There are two palettes - there's the palette that can be overlayed with
// toolbar items in browser.xhtml. This is invisible, and never seen by the
// user. Then there's the visible palette, which gets populated and displayed
@ -1682,6 +1685,83 @@ CustomizeMode.prototype = {
wrapper.replaceWith(wrapper.content);
},
_attachEventListeners() {
let container = this.$("customization-container");
container.addEventListener("command", event => {
switch (event.target.id) {
case "customization-titlebar-visibility-checkbox":
// NB: because command fires after click, by the time we've fired, the checkbox binding
// will already have switched the button's state, so this is correct:
this.toggleTitlebar(event.target.checked);
break;
case "customization-uidensity-menuitem-compact":
case "customization-uidensity-menuitem-normal":
case "customization-uidensity-menuitem-touch":
this.setUIDensity(event.target.mode);
break;
case "customization-uidensity-autotouchmode-checkbox":
this.updateAutoTouchMode(event.target.checked);
break;
case "whimsy-button":
this.togglePong(event.target.checked);
break;
case "customization-touchbar-button":
this.customizeTouchBar();
break;
case "customization-undo-reset-button":
this.undoReset();
break;
case "customization-reset-button":
this.reset();
break;
case "customization-done-button":
this.exit();
break;
}
});
container.addEventListener("popupshowing", event => {
switch (event.target.id) {
case "customization-toolbar-menu":
this.window.ToolbarContextMenu.onViewToolbarsPopupShowing(event);
break;
case "customization-uidensity-menu":
this.onUIDensityMenuShowing();
break;
}
});
let updateDensity = event => {
switch (event.target.id) {
case "customization-uidensity-menuitem-compact":
case "customization-uidensity-menuitem-normal":
case "customization-uidensity-menuitem-touch":
this.updateUIDensity(event.target.mode);
}
};
let densityMenu = this.document.getElementById(
"customization-uidensity-menu"
);
densityMenu.addEventListener("focus", updateDensity);
densityMenu.addEventListener("mouseover", updateDensity);
let resetDensity = event => {
switch (event.target.id) {
case "customization-uidensity-menuitem-compact":
case "customization-uidensity-menuitem-normal":
case "customization-uidensity-menuitem-touch":
this.resetUIDensity();
}
};
densityMenu.addEventListener("blur", resetDensity);
densityMenu.addEventListener("mouseout", resetDensity);
this.$("customization-lwtheme-link").addEventListener("click", () => {
this.openAddonsManagerThemes();
});
},
_updateTitlebarCheckbox() {
let drawInTitlebar = Services.appinfo.drawInTitlebar;
let checkbox = this.$("customization-titlebar-visibility-checkbox");

View file

@ -25,12 +25,9 @@
</vbox>
</box>
<hbox id="customization-footer">
<checkbox id="customization-titlebar-visibility-checkbox" class="customization-checkbox"
# NB: because oncommand fires after click, by the time we've fired, the checkbox binding
# will already have switched the button's state, so this is correct:
oncommand="gCustomizeMode.toggleTitlebar(this.checked)" data-l10n-id="customize-mode-titlebar"/>
<checkbox id="customization-titlebar-visibility-checkbox" class="customization-checkbox" data-l10n-id="customize-mode-titlebar"/>
<button id="customization-toolbar-visibility-button" class="footer-button" type="menu" data-l10n-id="customize-mode-toolbars">
<menupopup id="customization-toolbar-menu" onpopupshowing="if (event.target == this) ToolbarContextMenu.onViewToolbarsPopupShowing(event)"/>
<menupopup id="customization-toolbar-menu"/>
</button>
<button id="customization-uidensity-button"
data-l10n-id="customize-mode-uidensity"
@ -39,7 +36,6 @@
hidden="true">
<panel type="arrow" id="customization-uidensity-menu"
orient="vertical"
onpopupshowing="gCustomizeMode.onUIDensityMenuShowing();"
position="topleft bottomleft"
flip="none"
role="menu">
@ -47,52 +43,34 @@
class="menuitem-iconic customization-uidensity-menuitem"
role="menuitemradio"
data-l10n-id="customize-mode-uidensity-menu-compact-unsupported"
tabindex="0"
onfocus="gCustomizeMode.updateUIDensity(this.mode);"
onmouseover="gCustomizeMode.updateUIDensity(this.mode);"
onblur="gCustomizeMode.resetUIDensity();"
onmouseout="gCustomizeMode.resetUIDensity();"
oncommand="gCustomizeMode.setUIDensity(this.mode);"/>
tabindex="0"/>
<menuitem id="customization-uidensity-menuitem-normal"
class="menuitem-iconic customization-uidensity-menuitem"
role="menuitemradio"
data-l10n-id="customize-mode-uidensity-menu-normal"
tabindex="0"
onfocus="gCustomizeMode.updateUIDensity(this.mode);"
onmouseover="gCustomizeMode.updateUIDensity(this.mode);"
onblur="gCustomizeMode.resetUIDensity();"
onmouseout="gCustomizeMode.resetUIDensity();"
oncommand="gCustomizeMode.setUIDensity(this.mode);"/>
tabindex="0"/>
#ifndef XP_MACOSX
<menuitem id="customization-uidensity-menuitem-touch"
class="menuitem-iconic customization-uidensity-menuitem"
role="menuitemradio"
data-l10n-id="customize-mode-uidensity-menu-touch"
tabindex="0"
onfocus="gCustomizeMode.updateUIDensity(this.mode);"
onmouseover="gCustomizeMode.updateUIDensity(this.mode);"
onblur="gCustomizeMode.resetUIDensity();"
onmouseout="gCustomizeMode.resetUIDensity();"
oncommand="gCustomizeMode.setUIDensity(this.mode);">
tabindex="0">
</menuitem>
<spacer hidden="true" id="customization-uidensity-touch-spacer"/>
<checkbox id="customization-uidensity-autotouchmode-checkbox"
hidden="true"
data-l10n-id="customize-mode-uidensity-auto-touch-mode-checkbox"
oncommand="gCustomizeMode.updateAutoTouchMode(this.checked)"/>
data-l10n-id="customize-mode-uidensity-auto-touch-mode-checkbox"/>
#endif
</panel>
</button>
<label is="text-link"
id="customization-lwtheme-link"
class="customization-link"
data-l10n-id="customize-mode-lwthemes-link"
onclick="gCustomizeMode.openAddonsManagerThemes();" />
data-l10n-id="customize-mode-lwthemes-link" />
<button id="whimsy-button"
type="checkbox"
class="footer-button"
oncommand="gCustomizeMode.togglePong(this.checked);"
hidden="true"/>
<spacer id="customization-footer-spacer"/>
@ -100,21 +78,17 @@
<button id="customization-touchbar-button"
class="footer-button"
hidden="true"
oncommand="gCustomizeMode.customizeTouchBar();"
data-l10n-id="customize-mode-touchbar-cmd"/>
<spacer hidden="true" id="customization-touchbar-spacer"/>
#endif
<button id="customization-undo-reset-button"
class="footer-button"
hidden="true"
oncommand="gCustomizeMode.undoReset();"
data-l10n-id="customize-mode-undo-cmd"/>
<button id="customization-reset-button"
oncommand="gCustomizeMode.reset();"
data-l10n-id="customize-mode-restore-defaults"
class="footer-button"/>
<button id="customization-done-button"
oncommand="gCustomizeMode.exit();"
data-l10n-id="customize-mode-done"
default="true"
class="footer-button"/>

View file

@ -334,7 +334,6 @@ class HistoryInView extends ViewPage {
descriptionHeader = "firefoxview-dont-remember-history-empty-header-2";
descriptionLabels = [
"firefoxview-dont-remember-history-empty-description-one",
"firefoxview-dont-remember-history-empty-description-two",
];
descriptionLink = {
url: "about:preferences#privacy",

View file

@ -299,7 +299,6 @@ class RecentlyClosedTabsInView extends ViewPage {
descriptionHeader = "firefoxview-dont-remember-history-empty-header-2";
descriptionLabels = [
"firefoxview-dont-remember-history-empty-description-one",
"firefoxview-dont-remember-history-empty-description-two",
];
descriptionLink = {
url: "about:preferences#privacy",

View file

@ -306,8 +306,8 @@ add_task(async function test_empty_states() {
"Empty state with never remember history header has the expected text."
);
ok(
emptyStateCard.descriptionEls[1].textContent.includes(
"remember your activity as you browse. To change that"
emptyStateCard.descriptionEls[0].textContent.includes(
"does not remember your browsing activity"
),
"Empty state with never remember history description has the expected text."
);

View file

@ -469,8 +469,8 @@ add_task(async function test_empty_states() {
"Empty state with never remember history header has the expected text."
);
ok(
emptyStateCard.descriptionEls[1].textContent.includes(
"remember your activity as you browse. To change that"
emptyStateCard.descriptionEls[0].textContent.includes(
"does not remember your browsing activity"
),
"Empty state with never remember history description has the expected text."
);

View file

@ -80,6 +80,8 @@ for (const type of [
"DISCOVERY_STREAM_TOPICS_LOADING",
"DISCOVERY_STREAM_USER_EVENT",
"DOWNLOAD_CHANGED",
"FAKESPOT_CTA_CLICK",
"FAKESPOT_DISMISS",
"FAKE_FOCUS_SEARCH",
"FILL_SEARCH_TERM",
"HANDOFF_SEARCH_TO_AWESOMEBAR",
@ -93,6 +95,7 @@ for (const type of [
"NEW_TAB_REHYDRATED",
"NEW_TAB_STATE_REQUEST",
"NEW_TAB_UNLOAD",
"OPEN_ABOUT_FAKESPOT",
"OPEN_DOWNLOAD_FILE",
"OPEN_LINK",
"OPEN_NEW_WINDOW",

View file

@ -131,6 +131,7 @@ export class DiscoveryStreamAdminUI extends React.PureComponent {
this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this);
this.refreshTopicSelectionCache =
this.refreshTopicSelectionCache.bind(this);
this.toggleTBRFeed = this.toggleTBRFeed.bind(this);
this.state = {
toggledStories: {},
weatherQuery: "",
@ -193,6 +194,12 @@ export class DiscoveryStreamAdminUI extends React.PureComponent {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_SHOW_PLACEHOLDER);
}
toggleTBRFeed(e) {
const feed = e.target.value;
const selectedFeed = "discoverystream.contextualContent.selectedFeed";
this.props.dispatch(ac.SetPref(selectedFeed, feed));
}
idleDaily() {
this.dispatchSimpleAction(at.DISCOVERY_STREAM_DEV_IDLE_DAILY);
}
@ -422,6 +429,14 @@ export class DiscoveryStreamAdminUI extends React.PureComponent {
const { config, layout } = this.props.state.DiscoveryStream;
const personalized =
this.props.otherPrefs["discoverystream.personalization.enabled"];
const selectedFeed =
this.props.otherPrefs["discoverystream.contextualContent.selectedFeed"];
const TBRFeeds = this.props.otherPrefs[
"discoverystream.contextualContent.feeds"
]
.split(",")
.map(s => s.trim())
.filter(item => item);
return (
<div>
<button className="button" onClick={this.restorePrefDefaults}>
@ -450,7 +465,21 @@ export class DiscoveryStreamAdminUI extends React.PureComponent {
<br />
<button className="button" onClick={this.showPlaceholder}>
Show Placeholder Cards
</button>
</button>{" "}
<select
className="button"
onChange={this.toggleTBRFeed}
value={selectedFeed}
>
{TBRFeeds.map(feed => (
<option key={feed} value={feed}>
{feed}
</option>
))}
</select>
{/* <button className="button" onClick={this.toggleTBRFeed}>
Swap TBR feeds
</button> */}
<table>
<tbody>
{prefToggles.map(pref => (

View file

@ -23,6 +23,8 @@ const PREF_SPOCS_STARTUPCACHE_ENABLED =
const PREF_LIST_FEED_ENABLED = "discoverystream.contextualContent.enabled";
const PREF_LIST_FEED_SELECTED_FEED =
"discoverystream.contextualContent.selectedFeed";
const PREF_FAKESPOT_ENABLED =
"discoverystream.contextualContent.fakespot.enabled";
const INTERSECTION_RATIO = 0.5;
const VISIBLE = "visible";
const VISIBILITY_CHANGE_EVENT = "visibilitychange";
@ -321,6 +323,7 @@ export function RecentSavesContainer({
}
export class _CardGrid extends React.PureComponent {
// eslint-disable-next-line max-statements
renderCards() {
const prefs = this.props.Prefs.values;
const {
@ -448,20 +451,20 @@ export class _CardGrid extends React.PureComponent {
}
}
}
if (listFeedEnabled) {
const listFeed = (
<ListFeed
// only display recs that match selectedFeed for ListFeed
recs={this.props.data.recommendations.filter(
item => item.feedName === listFeedSelectedFeed
)}
firstVisibleTimestamp={this.props.firstVisibleTimestamp}
type={this.props.type}
/>
);
// place the list feed as the 3rd element in the card grid
cards.splice(2, 1, listFeed);
const isFakespot = listFeedSelectedFeed === "fakespot";
const fakespotEnabled = prefs[PREF_FAKESPOT_ENABLED];
if (!isFakespot || (isFakespot && fakespotEnabled)) {
// Place the list feed as the 3rd element in the card grid
cards.splice(
2,
0,
this.renderListFeed(
this.props.data.recommendations,
listFeedSelectedFeed
)
);
}
}
let moreRecsHeader = "";
@ -538,6 +541,24 @@ export class _CardGrid extends React.PureComponent {
);
}
renderListFeed(recommendations, selectedFeed) {
const recs = recommendations.filter(item => item.feedName === selectedFeed);
const isFakespot = selectedFeed === "fakespot";
// remove duplicates from category list
const categories = [...new Set(recs.map(({ category }) => category))];
const listFeed = (
<ListFeed
// only display recs that match selectedFeed for ListFeed
recs={recs}
categories={isFakespot ? categories : []}
firstVisibleTimestamp={this.props.firstVisibleTimestamp}
type={this.props.type}
dispatch={this.props.dispatch}
/>
);
return listFeed;
}
render() {
const { data } = this.props;

View file

@ -235,55 +235,65 @@ export class _DSCard extends React.PureComponent {
onLinkClick() {
const matchesSelectedTopic = this.doesLinkTopicMatchSelectedTopic();
if (this.props.dispatch) {
this.props.dispatch(
ac.DiscoveryStreamUserEvent({
event: "CLICK",
source: this.props.type.toUpperCase(),
action_position: this.props.pos,
value: {
card_type: this.props.flightId ? "spoc" : "organic",
recommendation_id: this.props.recommendation_id,
tile_id: this.props.id,
...(this.props.shim && this.props.shim.click
? { shim: this.props.shim.click }
: {}),
fetchTimestamp: this.props.fetchTimestamp,
firstVisibleTimestamp: this.props.firstVisibleTimestamp,
scheduled_corpus_item_id: this.props.scheduled_corpus_item_id,
recommended_at: this.props.recommended_at,
received_rank: this.props.received_rank,
topic: this.props.topic,
matches_selected_topic: matchesSelectedTopic,
selected_topics: this.props.selectedTopics,
is_list_card: this.props.isListCard,
},
})
);
this.props.dispatch(
ac.ImpressionStats({
source: this.props.type.toUpperCase(),
click: 0,
window_inner_width: this.props.windowObj.innerWidth,
window_inner_height: this.props.windowObj.innerHeight,
tiles: [
{
id: this.props.id,
pos: this.props.pos,
if (this.props.isFakespot) {
this.props.dispatch(
ac.DiscoveryStreamUserEvent({
event: "FAKESPOT_CLICK",
value: {
product_id: this.props.id,
category: this.props.category || "",
},
})
);
} else {
this.props.dispatch(
ac.DiscoveryStreamUserEvent({
event: "CLICK",
source: this.props.type.toUpperCase(),
action_position: this.props.pos,
value: {
card_type: this.props.flightId ? "spoc" : "organic",
recommendation_id: this.props.recommendation_id,
tile_id: this.props.id,
...(this.props.shim && this.props.shim.click
? { shim: this.props.shim.click }
: {}),
type: this.props.flightId ? "spoc" : "organic",
recommendation_id: this.props.recommendation_id,
fetchTimestamp: this.props.fetchTimestamp,
firstVisibleTimestamp: this.props.firstVisibleTimestamp,
scheduled_corpus_item_id: this.props.scheduled_corpus_item_id,
recommended_at: this.props.recommended_at,
received_rank: this.props.received_rank,
topic: this.props.topic,
matches_selected_topic: matchesSelectedTopic,
selected_topics: this.props.selectedTopics,
is_list_card: this.props.isListCard,
},
],
})
);
})
);
this.props.dispatch(
ac.ImpressionStats({
source: this.props.type.toUpperCase(),
click: 0,
window_inner_width: this.props.windowObj.innerWidth,
window_inner_height: this.props.windowObj.innerHeight,
tiles: [
{
id: this.props.id,
pos: this.props.pos,
...(this.props.shim && this.props.shim.click
? { shim: this.props.shim.click }
: {}),
type: this.props.flightId ? "spoc" : "organic",
recommendation_id: this.props.recommendation_id,
topic: this.props.topic,
selected_topics: this.props.selectedTopics,
is_list_card: this.props.isListCard,
},
],
})
);
}
}
}
@ -540,8 +550,13 @@ export class _DSCard extends React.PureComponent {
}
render() {
const { isRecentSave, DiscoveryStream, saveToPocketCard, isListCard } =
this.props;
const {
isRecentSave,
DiscoveryStream,
saveToPocketCard,
isListCard,
isFakespot,
} = this.props;
if (this.props.placeholder || !this.state.isSeen) {
// placeholder-seen is used to ensure the loading animation is only used if the card is visible.
const placeholderClassName = this.state.isSeen ? `placeholder-seen` : ``;
@ -600,6 +615,8 @@ export class _DSCard extends React.PureComponent {
const imageGradientClassName = imageGradient
? `ds-card-image-gradient`
: ``;
const listCardClassName = isListCard ? `list-feed-card` : ``;
const fakespotClassName = isFakespot ? `fakespot` : ``;
const titleLinesName = `ds-card-title-lines-${titleLines}`;
const descLinesClassName = `ds-card-desc-lines-${descLines}`;
@ -628,12 +645,9 @@ export class _DSCard extends React.PureComponent {
</button>
);
};
return (
<article
className={`ds-card ${
isListCard ? "list-feed-card" : ""
}${compactImagesClassName} ${imageGradientClassName} ${titleLinesName} ${descLinesClassName} ${ctaButtonClassName} ${ctaButtonVariantClassName}`}
className={`ds-card ${listCardClassName} ${fakespotClassName} ${compactImagesClassName} ${imageGradientClassName} ${titleLinesName} ${descLinesClassName} ${ctaButtonClassName} ${ctaButtonVariantClassName}`}
ref={this.setContextMenuButtonHostRef}
>
{this.props.showTopics && this.props.topic && !isListCard && (
@ -675,10 +689,13 @@ export class _DSCard extends React.PureComponent {
recommended_at: this.props.recommended_at,
received_rank: this.props.received_rank,
topic: this.props.topic,
is_list_card: this.props.isListCard,
is_list_card: isListCard,
isFakespot,
category: this.props.category,
},
]}
dispatch={this.props.dispatch}
isFakespot={isFakespot}
source={this.props.type}
firstVisibleTimestamp={this.props.firstVisibleTimestamp}
/>
@ -686,62 +703,71 @@ export class _DSCard extends React.PureComponent {
{ctaButtonVariant === "variant-b" && (
<div className="cta-header">Shop Now</div>
)}
<DefaultMeta
source={source}
title={this.props.title}
excerpt={excerpt}
newSponsoredLabel={newSponsoredLabel}
timeToRead={timeToRead}
context={this.props.context}
context_type={this.props.context_type}
sponsor={this.props.sponsor}
sponsored_by_override={this.props.sponsored_by_override}
saveToPocketCard={saveToPocketCard}
ctaButtonVariant={ctaButtonVariant}
dispatch={this.props.dispatch}
spocMessageVariant={this.props.spocMessageVariant}
mayHaveThumbsUpDown={this.props.mayHaveThumbsUpDown}
onThumbsUpClick={this.onThumbsUpClick}
onThumbsDownClick={this.onThumbsDownClick}
state={this.state}
isListCard={isListCard}
/>
{isFakespot ? (
<div className="meta">
<div className="info-wrap">
<header className="title clamp">{this.props.title}</header>
</div>
</div>
) : (
<DefaultMeta
source={source}
title={this.props.title}
excerpt={excerpt}
newSponsoredLabel={newSponsoredLabel}
timeToRead={timeToRead}
context={this.props.context}
context_type={this.props.context_type}
sponsor={this.props.sponsor}
sponsored_by_override={this.props.sponsored_by_override}
saveToPocketCard={saveToPocketCard}
ctaButtonVariant={ctaButtonVariant}
dispatch={this.props.dispatch}
spocMessageVariant={this.props.spocMessageVariant}
mayHaveThumbsUpDown={this.props.mayHaveThumbsUpDown}
onThumbsUpClick={this.onThumbsUpClick}
onThumbsDownClick={this.onThumbsDownClick}
state={this.state}
isListCard={isListCard}
/>
)}
<div className="card-stp-button-hover-background">
<div className="card-stp-button-position-wrapper">
{saveToPocketCard && !isListCard && (
<>{!this.props.flightId && stpButton()}</>
)}
<DSLinkMenu
id={this.props.id}
index={this.props.pos}
dispatch={this.props.dispatch}
url={this.props.url}
title={this.props.title}
source={source}
type={this.props.type}
card_type={this.props.flightId ? "spoc" : "organic"}
pocket_id={this.props.pocket_id}
shim={this.props.shim}
bookmarkGuid={this.props.bookmarkGuid}
flightId={
!this.props.is_collection ? this.props.flightId : undefined
}
showPrivacyInfo={!!this.props.flightId}
onMenuUpdate={this.onMenuUpdate}
onMenuShow={this.onMenuShow}
saveToPocketCard={saveToPocketCard}
pocket_button_enabled={pocketButtonEnabled}
isRecentSave={isRecentSave}
recommendation_id={this.props.recommendation_id}
tile_id={this.props.id}
block_key={this.props.id}
scheduled_corpus_item_id={this.props.scheduled_corpus_item_id}
recommended_at={this.props.recommended_at}
received_rank={this.props.received_rank}
is_list_card={this.props.isListCard}
/>
{!isFakespot && (
<DSLinkMenu
id={this.props.id}
index={this.props.pos}
dispatch={this.props.dispatch}
url={this.props.url}
title={this.props.title}
source={source}
type={this.props.type}
card_type={this.props.flightId ? "spoc" : "organic"}
pocket_id={this.props.pocket_id}
shim={this.props.shim}
bookmarkGuid={this.props.bookmarkGuid}
flightId={
!this.props.is_collection ? this.props.flightId : undefined
}
showPrivacyInfo={!!this.props.flightId}
onMenuUpdate={this.onMenuUpdate}
onMenuShow={this.onMenuShow}
saveToPocketCard={saveToPocketCard}
pocket_button_enabled={pocketButtonEnabled}
isRecentSave={isRecentSave}
recommendation_id={this.props.recommendation_id}
tile_id={this.props.id}
block_key={this.props.id}
scheduled_corpus_item_id={this.props.scheduled_corpus_item_id}
recommended_at={this.props.recommended_at}
received_rank={this.props.received_rank}
is_list_card={this.props.isListCard}
/>
)}
</div>
</div>
</article>

View file

@ -2,23 +2,66 @@
* 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/. */
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import React, { useState } from "react";
import { useSelector } from "react-redux";
import { DSCard } from "../DSCard/DSCard";
const PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE =
"discoverystream.contextualContent.listFeedTitle";
import { ContextMenuButton } from "content-src/components/ContextMenu/ContextMenuButton";
import { LinkMenu } from "content-src/components/LinkMenu/LinkMenu";
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";
import { actionCreators as ac } from "common/Actions.mjs";
const PREF_LISTFEED_TITLE = "discoverystream.contextualContent.listFeedTitle";
const PREF_FAKESPOT_CATEGROY =
"discoverystream.contextualContent.fakespot.defaultCategoryTitle";
const PREF_FAKESPOT_FOOTER =
"discoverystream.contextualContent.fakespot.footerCopy";
const PREF_FAKESPOT_CTA_COPY =
"discoverystream.contextualContent.fakespot.ctaCopy";
const PREF_FAKESPOT_CTA_URL =
"discoverystream.contextualContent.fakespot.ctaUrl";
const PREF_CONTEXTUAL_CONTENT_SELECTED_FEED =
"discoverystream.contextualContent.selectedFeed";
function ListFeed({ type, firstVisibleTimestamp, recs }) {
const dispatch = useDispatch();
const listFeedTitle = useSelector(state => state.Prefs.values)[
PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE
];
function ListFeed({ type, firstVisibleTimestamp, recs, categories, dispatch }) {
const [selectedFakespotFeed, setSelectedFakespotFeed] = useState("");
const prefs = useSelector(state => state.Prefs.values);
const listFeedTitle = prefs[PREF_LISTFEED_TITLE];
const categoryTitle = prefs[PREF_FAKESPOT_CATEGROY];
const footerCopy = prefs[PREF_FAKESPOT_FOOTER];
const ctaCopy = prefs[PREF_FAKESPOT_CTA_COPY];
const ctaUrl = prefs[PREF_FAKESPOT_CTA_URL];
const isFakespot =
prefs[PREF_CONTEXTUAL_CONTENT_SELECTED_FEED] === "fakespot";
// Todo: need to remove ads while using default recommendations, remove this line once API has been updated.
const listFeedRecs = recs.filter(rec => !rec.flight_id).slice(0, 5);
let listFeedRecs = selectedFakespotFeed
? recs.filter(rec => rec.category === selectedFakespotFeed)
: recs;
function handleCtaClick() {
dispatch(
ac.OnlyToMain({
type: "FAKESPOT_CTA_CLICK",
})
);
}
function handleChange(e) {
setSelectedFakespotFeed(e.target.value);
dispatch(
ac.DiscoveryStreamUserEvent({
event: "FAKESPOT_CATEGORY",
value: {
category: e.target.value || "",
},
})
);
}
const contextMenuOptions = ["FakespotDismiss", "AboutFakespot"];
const { length: listLength } = listFeedRecs;
// determine if the list should take up all availible height or not
const fullList = listLength >= 5;
return (
listLength > 0 && (
<div
@ -27,16 +70,49 @@ function ListFeed({ type, firstVisibleTimestamp, recs }) {
}`}
>
<div className="list-feed-inner-wrapper">
<h1 className="list-feed-title" id="list-feed-title">
<span className="icon icon-newsfeed"></span>
{listFeedTitle}
</h1>
{isFakespot ? (
<div className="fakespot-heading">
<div className="dropdown-wrapper">
<select
className="fakespot-dropdown"
name="fakespot-categories"
value={selectedFakespotFeed}
onChange={handleChange}
>
<option value="">
{categoryTitle || "Holiday Gift Guide"}
</option>
{categories.map(category => (
<option key={category} value={category}>
{category}
</option>
))}
</select>
<div className="context-menu-wrapper">
<ContextMenuButton>
<LinkMenu
dispatch={dispatch}
options={contextMenuOptions}
shouldSendImpressionStats={true}
site={{ url: "https://www.fakespot.com" }}
/>
</ContextMenuButton>
</div>
</div>
<p className="fakespot-desc">{listFeedTitle}</p>
</div>
) : (
<h1 className="list-feed-title" id="list-feed-title">
<span className="icon icon-newsfeed"></span>
{listFeedTitle}
</h1>
)}
<div
className="list-feed-content"
role="menu"
aria-labelledby="list-feed-title"
>
{listFeedRecs.map((rec, index) => {
{listFeedRecs.slice(0, 5).map((rec, index) => {
if (!rec || rec.placeholder) {
return (
<DSCard
@ -76,9 +152,24 @@ function ListFeed({ type, firstVisibleTimestamp, recs }) {
recommended_at={rec.recommended_at}
received_rank={rec.received_rank}
isListCard={true}
isFakespot={isFakespot}
/>
);
})}
{isFakespot && (
<div className="fakespot-footer">
<p>{footerCopy}</p>
<SafeAnchor
className="fakespot-cta"
url={ctaUrl}
referrer={""}
onLinkClick={handleCtaClick}
dispatch={dispatch}
>
{ctaCopy}
</SafeAnchor>
</div>
)}
</div>
</div>
</div>

View file

@ -73,7 +73,7 @@
font-weight: var(--font-weight-bold);
margin: 0;
.icon-newsfeed {
.icon {
margin-inline-end: var(--space-small);
transform: none;
fill: var(--newtab-text-primary-color);
@ -89,6 +89,132 @@
flex-direction: column;
row-gap: var(--border-width);
}
.fakespot-dropdown {
background: transparent;
border: none;
border-radius: var(--border-radius-small);
-moz-context-properties: fill;
fill: currentColor;
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
margin-block: var(--space-xsmall);
padding-block: var(--space-small);
padding-inline-start: var(--space-medium);
position: relative;
max-width: 18ch;
text-overflow: ellipsis;
@media (min-width: $break-point-widest) {
max-width: none;
background-image: url('chrome://browser/skin/gift.svg');
background-repeat: no-repeat;
background-size: 16px;
background-position: left var(--space-medium) center;
padding-inline-start: var(--space-xxlarge);
}
&:hover {
background-color: var(--newtab-button-hover-background);;
}
}
}
.fakespot-heading {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
.dropdown-wrapper {
display: flex;
position: relative;
width: 100%;
}
.fakespot-desc {
padding-inline: var(--space-medium);
margin-block: 0 var(--space-small);
font-size: var(--font-size-small);
}
.context-menu-wrapper {
@include context-menu-button;
.context-menu {
inset-inline-start: auto;
inset-inline-end: var(--space-small);
inset-block-start: var(--space-xxlarge);
}
.context-menu-button {
opacity: 1;
transform: scale(1);
background-color: transparent;
border-radius: var(--border-radius-small);
box-shadow: none;
inset-inline-end: var(--space-small);
inset-block-start: var(--space-small);
&:is(:hover) {
background-color: var(--newtab-button-hover-background);
}
&:is(:focus-visible) {
outline-color: var(--newtab-button-focus-border);
background-color: var(--newtab-button-focus-background);
outline-width: 2px;
}
&:is(:active) {
background-color: var(--newtab-button-active-background);
}
}
}
}
.fakespot-footer {
align-items: center;
border-block-start: var(--border-width) solid var(--border-color-deemphasized);
border-end-start-radius: var(--border-radius-medium);
border-end-end-radius: var(--border-radius-medium);
border-start-start-radius: 0;
border-start-end-radius: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: var(--space-medium);
p {
font-size: var(--font-size-small);
margin-block-start: 0;
}
.fakespot-cta {
background-color: var(--button-background-color-primary);
border: var(--button-border);
border-color: var(--button-border-color-primary);
border-radius: var(--button-border-radius);
color: var(--button-text-color-primary);
font-size: var(--font-size-small);
font-weight: var(--button-font-weight);
padding: var(--button-padding);
text-decoration: none;
text-align: center;
align-self: stretch;
&:hover {
background-color: var(--button-background-color-primary-hover);
border-color: var(--button-border-color-primary-hover);
color: var(--button-text-color-primary-hover);
}
&:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
}
}
.layout-variant-a,
@ -168,6 +294,11 @@
padding-block: var(--space-large) var(--space-small);
position: relative;
&.fakespot {
flex-direction: row;
min-height: 75px;
}
.ds-card-link {
inset-block-start: 0;
inset-inline-start: 0;
@ -234,7 +365,7 @@
}
}
&:last-child {
&:last-child:not(.fakespot) {
border-end-start-radius: var(--border-radius-medium);
border-end-end-radius: var(--border-radius-medium);
border-start-start-radius: 0;

View file

@ -21,7 +21,8 @@ export class SafeAnchor extends React.PureComponent {
type: at.OPEN_LINK,
data: {
event: { altKey, button, ctrlKey, metaKey, shiftKey },
referrer: "https://getpocket.com/recommendations",
referrer:
this.props.referrer || "https://getpocket.com/recommendations",
// Use the anchor's url, which could have been cleaned up
url: event.currentTarget.href,
},

View file

@ -54,6 +54,7 @@ export class ImpressionStats extends React.PureComponent {
_dispatchImpressionStats() {
const { props } = this;
const { isFakespot } = props;
const cards = props.rows;
if (this.props.flightId) {
@ -86,28 +87,43 @@ export class ImpressionStats extends React.PureComponent {
}
if (this._needsImpressionStats(cards)) {
props.dispatch(
ac.DiscoveryStreamImpressionStats({
source: props.source.toUpperCase(),
window_inner_width: window.innerWidth,
window_inner_height: window.innerHeight,
tiles: cards.map(link => ({
id: link.id,
pos: link.pos,
type: this.props.flightId ? "spoc" : "organic",
...(link.shim ? { shim: link.shim } : {}),
recommendation_id: link.recommendation_id,
fetchTimestamp: link.fetchTimestamp,
scheduled_corpus_item_id: link.scheduled_corpus_item_id,
recommended_at: link.recommended_at,
received_rank: link.received_rank,
topic: link.topic,
is_list_card: link.is_list_card,
})),
firstVisibleTimestamp: this.props.firstVisibleTimestamp,
})
);
this.impressionCardGuids = cards.map(link => link.id);
if (isFakespot) {
props.dispatch(
ac.DiscoveryStreamImpressionStats({
source: props.source.toUpperCase(),
window_inner_width: window.innerWidth,
window_inner_height: window.innerHeight,
tiles: cards.map(link => ({
id: link.id,
type: "fakespot",
category: link.category,
})),
})
);
} else {
props.dispatch(
ac.DiscoveryStreamImpressionStats({
source: props.source.toUpperCase(),
window_inner_width: window.innerWidth,
window_inner_height: window.innerHeight,
tiles: cards.map(link => ({
id: link.id,
pos: link.pos,
type: this.props.flightId ? "spoc" : "organic",
...(link.shim ? { shim: link.shim } : {}),
recommendation_id: link.recommendation_id,
fetchTimestamp: link.fetchTimestamp,
scheduled_corpus_item_id: link.scheduled_corpus_item_id,
recommended_at: link.recommended_at,
received_rank: link.received_rank,
topic: link.topic,
is_list_card: link.is_list_card,
})),
firstVisibleTimestamp: this.props.firstVisibleTimestamp,
})
);
this.impressionCardGuids = cards.map(link => link.id);
}
}
}

View file

@ -381,4 +381,27 @@ export const LinkMenuOptions = {
data: { url: site.url },
}),
}),
FakespotDismiss: () => ({
id: "newtab-menu-dismiss",
action: ac.OnlyToMain({
type: at.SET_PREF,
data: {
name: "discoverystream.contextualContent.fakespot.enabled",
value: false,
},
}),
impression: ac.OnlyToMain({
type: at.FAKESPOT_DISMISS,
}),
}),
AboutFakespot: site => ({
id: "newtab-menu-about-fakespot",
action: ac.OnlyToMain({
type: at.OPEN_LINK,
data: { url: site.url },
}),
impression: ac.OnlyToMain({
type: at.OPEN_ABOUT_FAKESPOT,
}),
}),
};

View file

@ -6159,7 +6159,7 @@ main section {
font-weight: var(--font-weight-bold);
margin: 0;
}
.list-feed .list-feed-title .icon-newsfeed {
.list-feed .list-feed-title .icon {
margin-inline-end: var(--space-small);
transform: none;
fill: var(--newtab-text-primary-color);
@ -6173,6 +6173,153 @@ main section {
flex-direction: column;
row-gap: var(--border-width);
}
.list-feed .fakespot-dropdown {
background: transparent;
border: none;
border-radius: var(--border-radius-small);
-moz-context-properties: fill;
fill: currentColor;
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
margin-block: var(--space-xsmall);
padding-block: var(--space-small);
padding-inline-start: var(--space-medium);
position: relative;
max-width: 18ch;
text-overflow: ellipsis;
}
@media (min-width: 1122px) {
.list-feed .fakespot-dropdown {
max-width: none;
background-image: url("chrome://browser/skin/gift.svg");
background-repeat: no-repeat;
background-size: 16px;
background-position: left var(--space-medium) center;
padding-inline-start: var(--space-xxlarge);
}
}
.list-feed .fakespot-dropdown:hover {
background-color: var(--newtab-button-hover-background);
}
.fakespot-heading {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
.fakespot-heading .dropdown-wrapper {
display: flex;
position: relative;
width: 100%;
}
.fakespot-heading .fakespot-desc {
padding-inline: var(--space-medium);
margin-block: 0 var(--space-small);
font-size: var(--font-size-small);
}
.fakespot-heading .context-menu-wrapper .context-menu-button {
background-clip: padding-box;
background-color: var(--newtab-button-background);
background-image: url("chrome://global/skin/icons/more.svg");
background-position: 50.1%;
border: 0;
outline: 1px solid var(--newtab-border-color);
outline-width: 0;
border-radius: 100%;
box-shadow: 0 2px rgba(12, 12, 13, 0.1);
cursor: pointer;
color: var(--button-text-color);
fill: var(--newtab-button-text);
height: 27px;
inset-inline-end: -13.5px;
opacity: 0;
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 150ms;
transition-property: transform, opacity;
width: 27px;
}
.fakespot-heading .context-menu-wrapper .context-menu-button:is(:active, :focus-visible, :hover) {
opacity: 1;
transform: scale(1);
}
.fakespot-heading .context-menu-wrapper .context-menu-button:is(:hover) {
background-color: var(--newtab-button-hover-background);
}
.fakespot-heading .context-menu-wrapper .context-menu-button:is(:focus-visible) {
outline-color: var(--newtab-button-focus-border);
background-color: var(--newtab-button-focus-background);
outline-width: 4px;
}
.fakespot-heading .context-menu-wrapper .context-menu-button:is(:active) {
background-color: var(--newtab-button-active-background);
}
.fakespot-heading .context-menu-wrapper .context-menu {
inset-inline-start: auto;
inset-inline-end: var(--space-small);
inset-block-start: var(--space-xxlarge);
}
.fakespot-heading .context-menu-wrapper .context-menu-button {
opacity: 1;
transform: scale(1);
background-color: transparent;
border-radius: var(--border-radius-small);
box-shadow: none;
inset-inline-end: var(--space-small);
inset-block-start: var(--space-small);
}
.fakespot-heading .context-menu-wrapper .context-menu-button:is(:hover) {
background-color: var(--newtab-button-hover-background);
}
.fakespot-heading .context-menu-wrapper .context-menu-button:is(:focus-visible) {
outline-color: var(--newtab-button-focus-border);
background-color: var(--newtab-button-focus-background);
outline-width: 2px;
}
.fakespot-heading .context-menu-wrapper .context-menu-button:is(:active) {
background-color: var(--newtab-button-active-background);
}
.fakespot-footer {
align-items: center;
border-block-start: var(--border-width) solid var(--border-color-deemphasized);
border-end-start-radius: var(--border-radius-medium);
border-end-end-radius: var(--border-radius-medium);
border-start-start-radius: 0;
border-start-end-radius: 0;
display: flex;
flex-direction: column;
justify-content: center;
padding: var(--space-medium);
}
.fakespot-footer p {
font-size: var(--font-size-small);
margin-block-start: 0;
}
.fakespot-footer .fakespot-cta {
background-color: var(--button-background-color-primary);
border: var(--button-border);
border-color: var(--button-border-color-primary);
border-radius: var(--button-border-radius);
color: var(--button-text-color-primary);
font-size: var(--font-size-small);
font-weight: var(--button-font-weight);
padding: var(--button-padding);
text-decoration: none;
text-align: center;
align-self: stretch;
}
.fakespot-footer .fakespot-cta:hover {
background-color: var(--button-background-color-primary-hover);
border-color: var(--button-border-color-primary-hover);
color: var(--button-text-color-primary-hover);
}
.fakespot-footer .fakespot-cta:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
.layout-variant-a .ds-card-grid .list-feed-content .ds-card.list-feed-card,
.layout-variant-b .ds-card-grid .list-feed-content .ds-card.list-feed-card {
@ -6243,6 +6390,10 @@ main section {
padding-block: var(--space-large) var(--space-small);
position: relative;
}
.ds-card-grid .list-feed-content .ds-card.list-feed-card.fakespot {
flex-direction: row;
min-height: 75px;
}
.ds-card-grid .list-feed-content .ds-card.list-feed-card .ds-card-link {
inset-block-start: 0;
inset-inline-start: 0;
@ -6298,13 +6449,13 @@ main section {
width: 75px;
}
}
.ds-card-grid .list-feed-content .ds-card.list-feed-card:last-child {
.ds-card-grid .list-feed-content .ds-card.list-feed-card:last-child:not(.fakespot) {
border-end-start-radius: var(--border-radius-medium);
border-end-end-radius: var(--border-radius-medium);
border-start-start-radius: 0;
border-start-end-radius: 0;
}
.ds-card-grid .list-feed-content .ds-card.list-feed-card:last-child .card-stp-button-hover-background {
.ds-card-grid .list-feed-content .ds-card.list-feed-card:last-child:not(.fakespot) .card-stp-button-hover-background {
border-end-start-radius: var(--border-radius-medium);
border-end-end-radius: var(--border-radius-medium);
border-start-start-radius: 0;

View file

@ -153,6 +153,8 @@ for (const type of [
"DISCOVERY_STREAM_TOPICS_LOADING",
"DISCOVERY_STREAM_USER_EVENT",
"DOWNLOAD_CHANGED",
"FAKESPOT_CTA_CLICK",
"FAKESPOT_DISMISS",
"FAKE_FOCUS_SEARCH",
"FILL_SEARCH_TERM",
"HANDOFF_SEARCH_TO_AWESOMEBAR",
@ -166,6 +168,7 @@ for (const type of [
"NEW_TAB_REHYDRATED",
"NEW_TAB_STATE_REQUEST",
"NEW_TAB_UNLOAD",
"OPEN_ABOUT_FAKESPOT",
"OPEN_DOWNLOAD_FILE",
"OPEN_LINK",
"OPEN_NEW_WINDOW",
@ -690,6 +693,7 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
this.handleWeatherSubmit = this.handleWeatherSubmit.bind(this);
this.handleWeatherUpdate = this.handleWeatherUpdate.bind(this);
this.refreshTopicSelectionCache = this.refreshTopicSelectionCache.bind(this);
this.toggleTBRFeed = this.toggleTBRFeed.bind(this);
this.state = {
toggledStories: {},
weatherQuery: ""
@ -736,6 +740,11 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
showPlaceholder() {
this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_SHOW_PLACEHOLDER);
}
toggleTBRFeed(e) {
const feed = e.target.value;
const selectedFeed = "discoverystream.contextualContent.selectedFeed";
this.props.dispatch(actionCreators.SetPref(selectedFeed, feed));
}
idleDaily() {
this.dispatchSimpleAction(actionTypes.DISCOVERY_STREAM_DEV_IDLE_DAILY);
}
@ -882,6 +891,8 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
layout
} = this.props.state.DiscoveryStream;
const personalized = this.props.otherPrefs["discoverystream.personalization.enabled"];
const selectedFeed = this.props.otherPrefs["discoverystream.contextualContent.selectedFeed"];
const TBRFeeds = this.props.otherPrefs["discoverystream.contextualContent.feeds"].split(",").map(s => s.trim()).filter(item => item);
return /*#__PURE__*/external_React_default().createElement("div", null, /*#__PURE__*/external_React_default().createElement("button", {
className: "button",
onClick: this.restorePrefDefaults
@ -906,7 +917,14 @@ class DiscoveryStreamAdminUI extends (external_React_default()).PureComponent {
}, "Refresh Topic selection count"), /*#__PURE__*/external_React_default().createElement("br", null), /*#__PURE__*/external_React_default().createElement("button", {
className: "button",
onClick: this.showPlaceholder
}, "Show Placeholder Cards"), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, prefToggles.map(pref => /*#__PURE__*/external_React_default().createElement(Row, {
}, "Show Placeholder Cards"), " ", /*#__PURE__*/external_React_default().createElement("select", {
className: "button",
onChange: this.toggleTBRFeed,
value: selectedFeed
}, TBRFeeds.map(feed => /*#__PURE__*/external_React_default().createElement("option", {
key: feed,
value: feed
}, feed))), /*#__PURE__*/external_React_default().createElement("table", null, /*#__PURE__*/external_React_default().createElement("tbody", null, prefToggles.map(pref => /*#__PURE__*/external_React_default().createElement(Row, {
key: pref
}, /*#__PURE__*/external_React_default().createElement("td", null, /*#__PURE__*/external_React_default().createElement(TogglePrefCheckbox, {
checked: config[pref],
@ -1874,6 +1892,29 @@ const LinkMenuOptions = {
data: { url: site.url },
}),
}),
FakespotDismiss: () => ({
id: "newtab-menu-dismiss",
action: actionCreators.OnlyToMain({
type: actionTypes.SET_PREF,
data: {
name: "discoverystream.contextualContent.fakespot.enabled",
value: false,
},
}),
impression: actionCreators.OnlyToMain({
type: actionTypes.FAKESPOT_DISMISS,
}),
}),
AboutFakespot: site => ({
id: "newtab-menu-about-fakespot",
action: actionCreators.OnlyToMain({
type: actionTypes.OPEN_LINK,
data: { url: site.url },
}),
impression: actionCreators.OnlyToMain({
type: actionTypes.OPEN_ABOUT_FAKESPOT,
}),
}),
};
;// CONCATENATED MODULE: ./content-src/components/LinkMenu/LinkMenu.jsx
@ -2196,6 +2237,9 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom
const {
props
} = this;
const {
isFakespot
} = props;
const cards = props.rows;
if (this.props.flightId) {
this.props.dispatch(actionCreators.OnlyToMain({
@ -2224,28 +2268,41 @@ class ImpressionStats_ImpressionStats extends (external_React_default()).PureCom
}
}
if (this._needsImpressionStats(cards)) {
props.dispatch(actionCreators.DiscoveryStreamImpressionStats({
source: props.source.toUpperCase(),
window_inner_width: window.innerWidth,
window_inner_height: window.innerHeight,
tiles: cards.map(link => ({
id: link.id,
pos: link.pos,
type: this.props.flightId ? "spoc" : "organic",
...(link.shim ? {
shim: link.shim
} : {}),
recommendation_id: link.recommendation_id,
fetchTimestamp: link.fetchTimestamp,
scheduled_corpus_item_id: link.scheduled_corpus_item_id,
recommended_at: link.recommended_at,
received_rank: link.received_rank,
topic: link.topic,
is_list_card: link.is_list_card
})),
firstVisibleTimestamp: this.props.firstVisibleTimestamp
}));
this.impressionCardGuids = cards.map(link => link.id);
if (isFakespot) {
props.dispatch(actionCreators.DiscoveryStreamImpressionStats({
source: props.source.toUpperCase(),
window_inner_width: window.innerWidth,
window_inner_height: window.innerHeight,
tiles: cards.map(link => ({
id: link.id,
type: "fakespot",
category: link.category
}))
}));
} else {
props.dispatch(actionCreators.DiscoveryStreamImpressionStats({
source: props.source.toUpperCase(),
window_inner_width: window.innerWidth,
window_inner_height: window.innerHeight,
tiles: cards.map(link => ({
id: link.id,
pos: link.pos,
type: this.props.flightId ? "spoc" : "organic",
...(link.shim ? {
shim: link.shim
} : {}),
recommendation_id: link.recommendation_id,
fetchTimestamp: link.fetchTimestamp,
scheduled_corpus_item_id: link.scheduled_corpus_item_id,
recommended_at: link.recommended_at,
received_rank: link.received_rank,
topic: link.topic,
is_list_card: link.is_list_card
})),
firstVisibleTimestamp: this.props.firstVisibleTimestamp
}));
this.impressionCardGuids = cards.map(link => link.id);
}
}
}
@ -2393,7 +2450,7 @@ class SafeAnchor extends (external_React_default()).PureComponent {
metaKey,
shiftKey
},
referrer: "https://getpocket.com/recommendations",
referrer: this.props.referrer || "https://getpocket.com/recommendations",
// Use the anchor's url, which could have been cleaned up
url: event.currentTarget.href
}
@ -2991,46 +3048,56 @@ class _DSCard extends (external_React_default()).PureComponent {
onLinkClick() {
const matchesSelectedTopic = this.doesLinkTopicMatchSelectedTopic();
if (this.props.dispatch) {
this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
event: "CLICK",
source: this.props.type.toUpperCase(),
action_position: this.props.pos,
value: {
card_type: this.props.flightId ? "spoc" : "organic",
recommendation_id: this.props.recommendation_id,
tile_id: this.props.id,
...(this.props.shim && this.props.shim.click ? {
shim: this.props.shim.click
} : {}),
fetchTimestamp: this.props.fetchTimestamp,
firstVisibleTimestamp: this.props.firstVisibleTimestamp,
scheduled_corpus_item_id: this.props.scheduled_corpus_item_id,
recommended_at: this.props.recommended_at,
received_rank: this.props.received_rank,
topic: this.props.topic,
matches_selected_topic: matchesSelectedTopic,
selected_topics: this.props.selectedTopics,
is_list_card: this.props.isListCard
}
}));
this.props.dispatch(actionCreators.ImpressionStats({
source: this.props.type.toUpperCase(),
click: 0,
window_inner_width: this.props.windowObj.innerWidth,
window_inner_height: this.props.windowObj.innerHeight,
tiles: [{
id: this.props.id,
pos: this.props.pos,
...(this.props.shim && this.props.shim.click ? {
shim: this.props.shim.click
} : {}),
type: this.props.flightId ? "spoc" : "organic",
recommendation_id: this.props.recommendation_id,
topic: this.props.topic,
selected_topics: this.props.selectedTopics,
is_list_card: this.props.isListCard
}]
}));
if (this.props.isFakespot) {
this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
event: "FAKESPOT_CLICK",
value: {
product_id: this.props.id,
category: this.props.category || ""
}
}));
} else {
this.props.dispatch(actionCreators.DiscoveryStreamUserEvent({
event: "CLICK",
source: this.props.type.toUpperCase(),
action_position: this.props.pos,
value: {
card_type: this.props.flightId ? "spoc" : "organic",
recommendation_id: this.props.recommendation_id,
tile_id: this.props.id,
...(this.props.shim && this.props.shim.click ? {
shim: this.props.shim.click
} : {}),
fetchTimestamp: this.props.fetchTimestamp,
firstVisibleTimestamp: this.props.firstVisibleTimestamp,
scheduled_corpus_item_id: this.props.scheduled_corpus_item_id,
recommended_at: this.props.recommended_at,
received_rank: this.props.received_rank,
topic: this.props.topic,
matches_selected_topic: matchesSelectedTopic,
selected_topics: this.props.selectedTopics,
is_list_card: this.props.isListCard
}
}));
this.props.dispatch(actionCreators.ImpressionStats({
source: this.props.type.toUpperCase(),
click: 0,
window_inner_width: this.props.windowObj.innerWidth,
window_inner_height: this.props.windowObj.innerHeight,
tiles: [{
id: this.props.id,
pos: this.props.pos,
...(this.props.shim && this.props.shim.click ? {
shim: this.props.shim.click
} : {}),
type: this.props.flightId ? "spoc" : "organic",
recommendation_id: this.props.recommendation_id,
topic: this.props.topic,
selected_topics: this.props.selectedTopics,
is_list_card: this.props.isListCard
}]
}));
}
}
}
onSaveClick() {
@ -3252,7 +3319,8 @@ class _DSCard extends (external_React_default()).PureComponent {
isRecentSave,
DiscoveryStream,
saveToPocketCard,
isListCard
isListCard,
isFakespot
} = this.props;
if (this.props.placeholder || !this.state.isSeen) {
// placeholder-seen is used to ensure the loading animation is only used if the card is visible.
@ -3300,6 +3368,8 @@ class _DSCard extends (external_React_default()).PureComponent {
const ctaButtonClassName = ctaButtonEnabled ? `ds-card-cta-button` : ``;
const compactImagesClassName = compactImages ? `ds-card-compact-image` : ``;
const imageGradientClassName = imageGradient ? `ds-card-image-gradient` : ``;
const listCardClassName = isListCard ? `list-feed-card` : ``;
const fakespotClassName = isFakespot ? `fakespot` : ``;
const titleLinesName = `ds-card-title-lines-${titleLines}`;
const descLinesClassName = `ds-card-desc-lines-${descLines}`;
let stpButton = () => {
@ -3321,7 +3391,7 @@ class _DSCard extends (external_React_default()).PureComponent {
})));
};
return /*#__PURE__*/external_React_default().createElement("article", {
className: `ds-card ${isListCard ? "list-feed-card" : ""}${compactImagesClassName} ${imageGradientClassName} ${titleLinesName} ${descLinesClassName} ${ctaButtonClassName} ${ctaButtonVariantClassName}`,
className: `ds-card ${listCardClassName} ${fakespotClassName} ${compactImagesClassName} ${imageGradientClassName} ${titleLinesName} ${descLinesClassName} ${ctaButtonClassName} ${ctaButtonVariantClassName}`,
ref: this.setContextMenuButtonHostRef
}, this.props.showTopics && this.props.topic && !isListCard && /*#__PURE__*/external_React_default().createElement("span", {
className: "ds-card-topic",
@ -3356,14 +3426,23 @@ class _DSCard extends (external_React_default()).PureComponent {
recommended_at: this.props.recommended_at,
received_rank: this.props.received_rank,
topic: this.props.topic,
is_list_card: this.props.isListCard
is_list_card: isListCard,
isFakespot,
category: this.props.category
}],
dispatch: this.props.dispatch,
isFakespot: isFakespot,
source: this.props.type,
firstVisibleTimestamp: this.props.firstVisibleTimestamp
})), ctaButtonVariant === "variant-b" && /*#__PURE__*/external_React_default().createElement("div", {
className: "cta-header"
}, "Shop Now"), /*#__PURE__*/external_React_default().createElement(DefaultMeta, {
}, "Shop Now"), isFakespot ? /*#__PURE__*/external_React_default().createElement("div", {
className: "meta"
}, /*#__PURE__*/external_React_default().createElement("div", {
className: "info-wrap"
}, /*#__PURE__*/external_React_default().createElement("header", {
className: "title clamp"
}, this.props.title))) : /*#__PURE__*/external_React_default().createElement(DefaultMeta, {
source: source,
title: this.props.title,
excerpt: excerpt,
@ -3386,7 +3465,7 @@ class _DSCard extends (external_React_default()).PureComponent {
className: "card-stp-button-hover-background"
}, /*#__PURE__*/external_React_default().createElement("div", {
className: "card-stp-button-position-wrapper"
}, saveToPocketCard && !isListCard && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, !this.props.flightId && stpButton()), /*#__PURE__*/external_React_default().createElement(DSLinkMenu, {
}, saveToPocketCard && !isListCard && /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, !this.props.flightId && stpButton()), !isFakespot && /*#__PURE__*/external_React_default().createElement(DSLinkMenu, {
id: this.props.id,
index: this.props.pos,
dispatch: this.props.dispatch,
@ -3688,16 +3767,48 @@ const TopicsWidget = (0,external_ReactRedux_namespaceObject.connect)(state => ({
const PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE = "discoverystream.contextualContent.listFeedTitle";
const PREF_LISTFEED_TITLE = "discoverystream.contextualContent.listFeedTitle";
const PREF_FAKESPOT_CATEGROY = "discoverystream.contextualContent.fakespot.defaultCategoryTitle";
const PREF_FAKESPOT_FOOTER = "discoverystream.contextualContent.fakespot.footerCopy";
const PREF_FAKESPOT_CTA_COPY = "discoverystream.contextualContent.fakespot.ctaCopy";
const PREF_FAKESPOT_CTA_URL = "discoverystream.contextualContent.fakespot.ctaUrl";
const PREF_CONTEXTUAL_CONTENT_SELECTED_FEED = "discoverystream.contextualContent.selectedFeed";
function ListFeed({
type,
firstVisibleTimestamp,
recs
recs,
categories,
dispatch
}) {
const dispatch = (0,external_ReactRedux_namespaceObject.useDispatch)();
const listFeedTitle = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values)[PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE];
const [selectedFakespotFeed, setSelectedFakespotFeed] = (0,external_React_namespaceObject.useState)("");
const prefs = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.Prefs.values);
const listFeedTitle = prefs[PREF_LISTFEED_TITLE];
const categoryTitle = prefs[PREF_FAKESPOT_CATEGROY];
const footerCopy = prefs[PREF_FAKESPOT_FOOTER];
const ctaCopy = prefs[PREF_FAKESPOT_CTA_COPY];
const ctaUrl = prefs[PREF_FAKESPOT_CTA_URL];
const isFakespot = prefs[PREF_CONTEXTUAL_CONTENT_SELECTED_FEED] === "fakespot";
// Todo: need to remove ads while using default recommendations, remove this line once API has been updated.
const listFeedRecs = recs.filter(rec => !rec.flight_id).slice(0, 5);
let listFeedRecs = selectedFakespotFeed ? recs.filter(rec => rec.category === selectedFakespotFeed) : recs;
function handleCtaClick() {
dispatch(actionCreators.OnlyToMain({
type: "FAKESPOT_CTA_CLICK"
}));
}
function handleChange(e) {
setSelectedFakespotFeed(e.target.value);
dispatch(actionCreators.DiscoveryStreamUserEvent({
event: "FAKESPOT_CATEGORY",
value: {
category: e.target.value || ""
}
}));
}
const contextMenuOptions = ["FakespotDismiss", "AboutFakespot"];
const {
length: listLength
} = listFeedRecs;
@ -3707,7 +3818,32 @@ function ListFeed({
className: `list-feed ${fullList ? "full-height" : ""} ${listLength > 2 ? "span-2" : "span-1"}`
}, /*#__PURE__*/external_React_default().createElement("div", {
className: "list-feed-inner-wrapper"
}, /*#__PURE__*/external_React_default().createElement("h1", {
}, isFakespot ? /*#__PURE__*/external_React_default().createElement("div", {
className: "fakespot-heading"
}, /*#__PURE__*/external_React_default().createElement("div", {
className: "dropdown-wrapper"
}, /*#__PURE__*/external_React_default().createElement("select", {
className: "fakespot-dropdown",
name: "fakespot-categories",
value: selectedFakespotFeed,
onChange: handleChange
}, /*#__PURE__*/external_React_default().createElement("option", {
value: ""
}, categoryTitle || "Holiday Gift Guide"), categories.map(category => /*#__PURE__*/external_React_default().createElement("option", {
key: category,
value: category
}, category))), /*#__PURE__*/external_React_default().createElement("div", {
className: "context-menu-wrapper"
}, /*#__PURE__*/external_React_default().createElement(ContextMenuButton, null, /*#__PURE__*/external_React_default().createElement(LinkMenu, {
dispatch: dispatch,
options: contextMenuOptions,
shouldSendImpressionStats: true,
site: {
url: "https://www.fakespot.com"
}
})))), /*#__PURE__*/external_React_default().createElement("p", {
className: "fakespot-desc"
}, listFeedTitle)) : /*#__PURE__*/external_React_default().createElement("h1", {
className: "list-feed-title",
id: "list-feed-title"
}, /*#__PURE__*/external_React_default().createElement("span", {
@ -3716,7 +3852,7 @@ function ListFeed({
className: "list-feed-content",
role: "menu",
"aria-labelledby": "list-feed-title"
}, listFeedRecs.map((rec, index) => {
}, listFeedRecs.slice(0, 5).map((rec, index) => {
if (!rec || rec.placeholder) {
return /*#__PURE__*/external_React_default().createElement(DSCard, {
key: `list-card-${index}`,
@ -3752,9 +3888,18 @@ function ListFeed({
scheduled_corpus_item_id: rec.scheduled_corpus_item_id,
recommended_at: rec.recommended_at,
received_rank: rec.received_rank,
isListCard: true
isListCard: true,
isFakespot: isFakespot
});
}))));
}), isFakespot && /*#__PURE__*/external_React_default().createElement("div", {
className: "fakespot-footer"
}, /*#__PURE__*/external_React_default().createElement("p", null, footerCopy), /*#__PURE__*/external_React_default().createElement(SafeAnchor, {
className: "fakespot-cta",
url: ctaUrl,
referrer: "",
onLinkClick: handleCtaClick,
dispatch: dispatch
}, ctaCopy)))));
}
;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
@ -3780,6 +3925,7 @@ const PREF_TOPICS_AVAILABLE = "discoverystream.topicSelection.topics";
const PREF_SPOCS_STARTUPCACHE_ENABLED = "discoverystream.spocs.startupCache.enabled";
const PREF_LIST_FEED_ENABLED = "discoverystream.contextualContent.enabled";
const PREF_LIST_FEED_SELECTED_FEED = "discoverystream.contextualContent.selectedFeed";
const PREF_FAKESPOT_ENABLED = "discoverystream.contextualContent.fakespot.enabled";
const CardGrid_INTERSECTION_RATIO = 0.5;
const CardGrid_VISIBLE = "visible";
const CardGrid_VISIBILITY_CHANGE_EVENT = "visibilitychange";
@ -4030,6 +4176,7 @@ function RecentSavesContainer({
}, recentSavesCards));
}
class _CardGrid extends (external_React_default()).PureComponent {
// eslint-disable-next-line max-statements
renderCards() {
const prefs = this.props.Prefs.values;
const {
@ -4142,15 +4289,12 @@ class _CardGrid extends (external_React_default()).PureComponent {
}
}
if (listFeedEnabled) {
const listFeed = /*#__PURE__*/external_React_default().createElement(ListFeed
// only display recs that match selectedFeed for ListFeed
, {
recs: this.props.data.recommendations.filter(item => item.feedName === listFeedSelectedFeed),
firstVisibleTimestamp: this.props.firstVisibleTimestamp,
type: this.props.type
});
// place the list feed as the 3rd element in the card grid
cards.splice(2, 1, listFeed);
const isFakespot = listFeedSelectedFeed === "fakespot";
const fakespotEnabled = prefs[PREF_FAKESPOT_ENABLED];
if (!isFakespot || isFakespot && fakespotEnabled) {
// Place the list feed as the 3rd element in the card grid
cards.splice(2, 0, this.renderListFeed(this.props.data.recommendations, listFeedSelectedFeed));
}
}
let moreRecsHeader = "";
// For now this is English only.
@ -4196,6 +4340,24 @@ class _CardGrid extends (external_React_default()).PureComponent {
className: gridClassName
}, cards)));
}
renderListFeed(recommendations, selectedFeed) {
const recs = recommendations.filter(item => item.feedName === selectedFeed);
const isFakespot = selectedFeed === "fakespot";
// remove duplicates from category list
const categories = [...new Set(recs.map(({
category
}) => category))];
const listFeed = /*#__PURE__*/external_React_default().createElement(ListFeed
// only display recs that match selectedFeed for ListFeed
, {
recs: recs,
categories: isFakespot ? categories : [],
firstVisibleTimestamp: this.props.firstVisibleTimestamp,
type: this.props.type,
dispatch: this.props.dispatch
});
return listFeed;
}
render() {
const {
data

View file

@ -781,7 +781,7 @@ export const PREFS_CONFIG = new Map([
"discoverystream.contextualContent.feeds",
{
title: "CSV list of possible topics for the contextual content feed",
value: "need_to_know",
value: "need_to_know, fakespot",
},
],
[
@ -799,6 +799,41 @@ export const PREFS_CONFIG = new Map([
value: "",
},
],
[
"discoverystream.contextualContent.fakespot.defaultCategoryTitle",
{
title: "Title default category from fakespot endpoint",
value: "",
},
],
[
"discoverystream.contextualContent.fakespot.footerCopy",
{
title: "footer copy for fakespot feed",
value: "",
},
],
[
"discoverystream.contextualContent.fakespot.enabled",
{
title: "User controlled pref that displays fakespot feed",
value: true,
},
],
[
"discoverystream.contextualContent.fakespot.ctaCopy",
{
title: "cta copy for fakespot feed",
value: "",
},
],
[
"discoverystream.contextualContent.fakespot.ctaUrl",
{
title: "cta link for fakespot feed",
value: "",
},
],
[
"support.url",
{

View file

@ -103,11 +103,19 @@ const PREF_SPOCS_STARTUP_CACHE_ENABLED =
"discoverystream.spocs.startupCache.enabled";
const PREF_CONTEXTUAL_CONTENT_ENABLED =
"discoverystream.contextualContent.enabled";
const PREF_CONTEXTUAL_CONTENT_FEEDS = "discoverystream.contextualContent.feeds";
const PREF_FAKESPOT_ENABLED = "discoverystream.contextualContent.enabled";
const PREF_CONTEXTUAL_CONTENT_SELECTED_FEED =
"discoverystream.contextualContent.selectedFeed";
const PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE =
"discoverystream.contextualContent.listFeedTitle";
const PREF_CONTEXTUAL_CONTENT_FAKESPOT_FOOTER =
"discoverystream.contextualContent.fakespot.footerCopy";
const PREF_CONTEXTUAL_CONTENT_FAKESPOT_CATEGORY =
"discoverystream.contextualContent.fakespot.defaultCategoryTitle";
const PREF_CONTEXTUAL_CONTENT_FAKESPOT_CTA_COPY =
"discoverystream.contextualContent.fakespot.ctaCopy";
const PREF_CONTEXTUAL_CONTENT_FAKESPOT_CTA_URL =
"discoverystream.contextualContent.fakespot.ctaUrl";
let getHardcodedLayout;
@ -1549,9 +1557,11 @@ export class DiscoveryStreamFeed {
};
}
// eslint-disable-next-line max-statements
async getComponentFeed(feedUrl, isStartup) {
const cachedData = (await this.cache.get()) || {};
let contextualContentFeeds;
let isFakespot;
let selectedFeed;
const { feeds } = cachedData;
let feed = feeds ? feeds[feedUrl] : null;
@ -1578,11 +1588,17 @@ export class DiscoveryStreamFeed {
// Should we pass the feed param to the merino request
const contextualContentEnabled =
this.store.getState().Prefs.values[PREF_CONTEXTUAL_CONTENT_ENABLED];
contextualContentFeeds = this.store
.getState()
.Prefs.values[PREF_CONTEXTUAL_CONTENT_FEEDS]?.split(",")
.map(t => t.trim())
.filter(item => item);
selectedFeed =
this.store.getState().Prefs.values[
PREF_CONTEXTUAL_CONTENT_SELECTED_FEED
];
isFakespot = selectedFeed === "fakespot";
const fakespotEnabled =
this.store.getState().Prefs.values[PREF_FAKESPOT_ENABLED];
const shouldFetchTBRFeed =
(contextualContentEnabled && !isFakespot) ||
(contextualContentEnabled && isFakespot && fakespotEnabled);
headers.append("content-type", "application/json");
options = {
@ -1593,9 +1609,7 @@ export class DiscoveryStreamFeed {
locale: this.locale,
region: this.region,
topics,
...(contextualContentEnabled
? { feeds: contextualContentFeeds || [] }
: {}),
...(shouldFetchTBRFeed ? { feeds: [selectedFeed] } : {}),
}),
};
} else if (this.isBff) {
@ -1627,39 +1641,76 @@ export class DiscoveryStreamFeed {
received_rank: item.receivedRank,
recommended_at: feedResponse.recommendedAt,
}));
if (feedResponse.feeds && contextualContentFeeds?.length) {
contextualContentFeeds.forEach(feedName => {
feedResponse.feeds[feedName]?.recommendations.forEach(item =>
recommendations.push({
id: item.tileId,
scheduled_corpus_item_id: item.scheduledCorpusItemId,
url: item.url,
title: item.title,
topic: item.topic,
excerpt: item.excerpt,
publisher: item.publisher,
raw_image_src: item.imageUrl,
received_rank: item.receivedRank,
recommended_at: feedResponse.recommendedAt,
// property to determine if rec is used in ListFeed or not
feedName,
})
);
});
const selectedFeed =
if (feedResponse.feeds && selectedFeed) {
const selectedFeedPref =
this.store.getState().Prefs.values[
PREF_CONTEXTUAL_CONTENT_SELECTED_FEED
];
const keyName = isFakespot ? "products" : "recommendations";
const selectedFeedResponse = feedResponse.feeds[selectedFeedPref];
selectedFeedResponse?.[keyName]?.forEach(item =>
recommendations.push({
id: isFakespot ? item.id : item.tileId,
scheduled_corpus_item_id: item.scheduledCorpusItemId,
url: item.url,
title: item.title,
topic: item.topic,
excerpt: item.excerpt,
publisher: item.publisher,
raw_image_src: item.imageUrl,
received_rank: item.receivedRank,
recommended_at: feedResponse.recommendedAt,
// property to determine if rec is used in ListFeed or not
feedName: selectedFeed,
category: item.category,
})
);
const prevTitle =
this.store.getState().Prefs.values[
PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE
];
const feedTitle = feedResponse.feeds[selectedFeed].title;
const feedTitle = isFakespot
? selectedFeedResponse.headerCopy
: selectedFeedResponse.title;
if (feedTitle && feedTitle !== prevTitle) {
this.store.dispatch(
ac.SetPref(PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE, feedTitle)
);
if (isFakespot) {
this.store.dispatch(
ac.SetPref(PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE, feedTitle)
);
this.store.dispatch(
ac.SetPref(
PREF_CONTEXTUAL_CONTENT_FAKESPOT_CATEGORY,
selectedFeedResponse.defaultCategoryName
)
);
this.store.dispatch(
ac.SetPref(
PREF_CONTEXTUAL_CONTENT_FAKESPOT_FOOTER,
selectedFeedResponse.footerCopy
)
);
this.store.dispatch(
ac.SetPref(
PREF_CONTEXTUAL_CONTENT_FAKESPOT_CTA_COPY,
selectedFeedResponse.cta.ctaCopy
)
);
this.store.dispatch(
ac.SetPref(
PREF_CONTEXTUAL_CONTENT_FAKESPOT_CTA_URL,
selectedFeedResponse.cta.url
)
);
} else {
this.store.dispatch(
ac.SetPref(PREF_CONTEXTUAL_CONTENT_LISTFEED_TITLE, feedTitle)
);
}
}
}
} else if (this.isBff) {
@ -2148,6 +2199,7 @@ export class DiscoveryStreamFeed {
case PREF_SPOC_POSITIONS:
case PREF_UNIFIED_ADS_SPOCS_ENABLED:
case PREF_CONTEXTUAL_CONTENT_ENABLED:
case PREF_CONTEXTUAL_CONTENT_SELECTED_FEED:
// This is a config reset directly related to Discovery Stream pref.
this.configReset();
break;

View file

@ -923,6 +923,23 @@ export class TelemetryFeed {
}
break;
}
case "FAKESPOT_CLICK": {
const { product_id, category } = action.data.value ?? {};
Glean.newtab.fakespotClick.record({
newtab_visit_id: session.session_id,
product_id,
category,
});
break;
}
case "FAKESPOT_CATEGORY": {
const { category } = action.data.value ?? {};
Glean.newtab.fakespotCategory.record({
newtab_visit_id: session.session_id,
category,
});
break;
}
}
}
@ -1101,6 +1118,34 @@ export class TelemetryFeed {
case at.TOPIC_SELECTION_USER_SAVE:
this.handleTopicSelectionUserEvent(action);
break;
case at.FAKESPOT_DISMISS: {
const session = this.sessions.get(au.getPortIdOfSender(action));
if (session) {
Glean.newtab.fakespotDismiss.record({
newtab_visit_id: session.session_id,
});
}
break;
}
case at.FAKESPOT_CTA_CLICK: {
const session = this.sessions.get(au.getPortIdOfSender(action));
if (session) {
Glean.newtab.fakespotCtaClick.record({
newtab_visit_id: session.session_id,
});
}
break;
}
case at.OPEN_ABOUT_FAKESPOT: {
const session = this.sessions.get(au.getPortIdOfSender(action));
if (session) {
Glean.newtab.fakespotAboutClick.record({
newtab_visit_id: session.session_id,
});
}
break;
}
// The remaining action types come from ASRouter, which doesn't use
// Actions from Actions.mjs, but uses these other custom strings.
case msg.TOOLBAR_BADGE_TELEMETRY:
@ -1315,24 +1360,33 @@ export class TelemetryFeed {
const { tiles } = data;
tiles.forEach(tile => {
Glean.pocket.impression.record({
newtab_visit_id: session.session_id,
is_sponsored: tile.type === "spoc",
position: tile.pos,
tile_id: tile.id,
topic: tile.topic,
selected_topics: tile.selectedTopics,
is_list_card: tile.is_list_card,
...(tile.scheduled_corpus_item_id
? {
scheduled_corpus_item_id: tile.scheduled_corpus_item_id,
received_rank: tile.received_rank,
recommended_at: tile.recommended_at,
}
: {
recommendation_id: tile.recommendation_id,
}),
});
// if the tile has a category it is a product tile from fakespot
if (tile.type === "fakespot") {
Glean.newtab.fakespotProductImpression.record({
newtab_visit_id: session.session_id,
product_id: tile.id,
category: tile.category,
});
} else {
Glean.pocket.impression.record({
newtab_visit_id: session.session_id,
is_sponsored: tile.type === "spoc",
position: tile.pos,
tile_id: tile.id,
topic: tile.topic,
selected_topics: tile.selectedTopics,
is_list_card: tile.is_list_card,
...(tile.scheduled_corpus_item_id
? {
scheduled_corpus_item_id: tile.scheduled_corpus_item_id,
received_rank: tile.received_rank,
recommended_at: tile.recommended_at,
}
: {
recommendation_id: tile.recommendation_id,
}),
});
}
if (tile.shim) {
if (this.canSendUnifiedAdsSpocCallbacks) {
// Send unified ads callback event

View file

@ -529,6 +529,138 @@ newtab:
send_in_pings:
- newtab
fakespot_dismiss:
type: event
description: >
Recorded when a user dissmisses TBR fakespot feed
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_sensitivity:
- interaction
notification_emails:
- nbarrett@mozilla.com
expires: never
extra_keys:
newtab_visit_id: *newtab_visit_id
send_in_pings:
- newtab
fakespot_about_click:
type: event
description: >
Recorded when a user the 'About Fakespot' link in TBR fakespot feed context menu
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_sensitivity:
- interaction
notification_emails:
- nbarrett@mozilla.com
expires: never
extra_keys:
newtab_visit_id: *newtab_visit_id
send_in_pings:
- newtab
fakespot_click:
type: event
description: >
Recorded when a user clicks on a card in TBR fakespot feed
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_sensitivity:
- interaction
notification_emails:
- nbarrett@mozilla.com
expires: never
extra_keys:
newtab_visit_id: *newtab_visit_id
product_id:
description: >
id of fakespot product
type: string
category:
description: >
category of fakespot product
type: string
send_in_pings:
- newtab
fakespot_product_impression:
type: event
description: >
Recorded when a user triggers an impression on a card in TBR fakespot feed
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_sensitivity:
- interaction
notification_emails:
- nbarrett@mozilla.com
expires: never
extra_keys:
newtab_visit_id: *newtab_visit_id
product_title:
description: >
title of fakespot product
type: string
product_id:
description: >
id of fakespot product
type: string
category:
description: >
category of fakespot product
type: string
send_in_pings:
- newtab
fakespot_cta_click:
type: event
description: >
Recorded when a user clicks on the CTA in TBR fakespot feed
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_sensitivity:
- interaction
notification_emails:
- nbarrett@mozilla.com
expires: never
extra_keys:
newtab_visit_id: *newtab_visit_id
send_in_pings:
- newtab
fakespot_category:
type: event
description: >
Recorded when a user changes the category in TBR fakespot feed
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1924873
data_sensitivity:
- interaction
notification_emails:
- nbarrett@mozilla.com
expires: never
extra_keys:
newtab_visit_id: *newtab_visit_id
category:
description: >
category that user selected
type: string
send_in_pings:
- newtab
newtab.search:
enabled:
lifetime: application
@ -2496,6 +2628,34 @@ activity_stream:
extra_keys: *activity_stream_event_extra
telemetry_mirror: Activity_stream_Event_PocketThumbsUp
event_fakespot_click:
type: event
description: >
This is recorded with every user interaction on Activity Stream
elements.
This event was generated to correspond to the Legacy Telemetry event
activity_stream.event#FAKESPOT_CLICK.
bugs: *activity_stream_event_bugs
data_reviews: *activity_stream_event_data_reviews
notification_emails: *activity_stream_event_emails
expires: never
extra_keys: *activity_stream_event_extra
telemetry_mirror: Activity_stream_Event_FakespotClick
event_fakespot_category:
type: event
description: >
This is recorded with every user interaction on Activity Stream
elements.
This event was generated to correspond to the Legacy Telemetry event
activity_stream.event#FAKESPOT_CATEGORY.
bugs: *activity_stream_event_bugs
data_reviews: *activity_stream_event_data_reviews
notification_emails: *activity_stream_event_emails
expires: never
extra_keys: *activity_stream_event_extra
telemetry_mirror: Activity_stream_Event_FakespotCategory
event_pref_changed:
type: event
description: >

View file

@ -67,7 +67,10 @@ describe("DiscoveryStreamAdmin", () => {
wrapper = shallow(
<DiscoveryStreamAdminUI
dispatch={dispatch}
otherPrefs={{}}
otherPrefs={{
"discoverystream.contextualContent.selectedFeed": "foo",
"discoverystream.contextualContent.feeds": "foo, bar",
}}
state={{
DiscoveryStream: state,
Weather: {
@ -95,7 +98,10 @@ describe("DiscoveryStreamAdmin", () => {
};
wrapper = shallow(
<DiscoveryStreamAdminUI
otherPrefs={{}}
otherPrefs={{
"discoverystream.contextualContent.selectedFeed": "foo",
"discoverystream.contextualContent.feeds": "foo, bar",
}}
state={{
DiscoveryStream: state,
Weather: {

View file

@ -426,6 +426,33 @@ describe("<DSCard>", () => {
})
);
});
it("fakespot onLinkClick should dispatch with the correct events", () => {
wrapper.setProps({
id: "fooidx",
pos: 1,
type: "foo",
isFakespot: true,
category: "fakespot",
});
sandbox
.stub(wrapper.instance(), "doesLinkTopicMatchSelectedTopic")
.returns(undefined);
wrapper.instance().onLinkClick();
assert.calledWith(
dispatch,
ac.DiscoveryStreamUserEvent({
event: "FAKESPOT_CLICK",
value: {
product_id: "fooidx",
category: "fakespot",
},
})
);
});
});
describe("DSCard with CTA", () => {
@ -807,6 +834,32 @@ describe("Listfeed <DSCard />", () => {
});
});
describe("ListFeed fakespot <DSCard />", () => {
let wrapper;
let sandbox;
let dispatch;
beforeEach(() => {
sandbox = sinon.createSandbox();
dispatch = sandbox.stub();
wrapper = shallow(
<DSCard
dispatch={dispatch}
{...DEFAULT_PROPS}
isListFeed={true}
isFakespot={true}
/>
);
wrapper.setState({ isSeen: true });
});
it("should not render source element", () => {
const source_element = wrapper.find(".source");
assert.ok(!source_element.exists());
});
});
describe("<DSSource> component", () => {
it("should return a default source without compact", () => {
const wrapper = shallow(<DSSource source="Mozilla" />);

View file

@ -5,11 +5,14 @@ import { combineReducers, createStore } from "redux";
import { Provider } from "react-redux";
import React from "react";
import { DSCard } from "../../../../../content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
import { actionCreators as ac } from "common/Actions.mjs";
// import { SafeAnchor } from "../../../../../content-src/components/DiscoveryStreamComponents/SafeAnchor/SafeAnchor";
const DEFAULT_PROPS = {
type: "foo",
firstVisibleTimestamp: new Date("March 21, 2024 10:11:12").getTime(),
recs: [{}, {}, {}],
categories: [],
};
// Wrap this around any component that uses useSelector,
@ -23,10 +26,12 @@ describe("Discovery Stream <ListFeed>", () => {
let wrapper;
let sandbox;
let dispatch;
// let useStateSpy
beforeEach(() => {
sandbox = sinon.createSandbox();
dispatch = sandbox.stub();
// useStateSpy = sinon.spy(React, "useState"); // Spy on useState
wrapper = mount(
<WrapWithProvider>
<ListFeed dispatch={dispatch} {...DEFAULT_PROPS} />
@ -88,4 +93,81 @@ describe("Discovery Stream <ListFeed>", () => {
assert.ok(wrapper.find(".list-card-placeholder").exists());
assert.lengthOf(wrapper.find(".list-card-placeholder"), 5);
});
describe("fakespot <ListFeed />", () => {
const PREF_CONTEXTUAL_CONTENT_SELECTED_FEED =
"discoverystream.contextualContent.selectedFeed";
beforeEach(() => {
// mock the pref for selected feed
const state = {
...INITIAL_STATE,
Prefs: {
...INITIAL_STATE.Prefs,
values: {
...INITIAL_STATE.Prefs.values,
[PREF_CONTEXTUAL_CONTENT_SELECTED_FEED]: "fakespot",
},
},
};
wrapper = mount(
<WrapWithProvider state={state}>
<ListFeed
dispatch={dispatch}
recs={[
{ category: "foo&bar" },
{ category: "foo&bar" },
{ category: "foo&bar" },
{ category: "foo&bar" },
{ category: "bar" },
{ category: "bar" },
]}
categories={["foo&bar", "bar"]}
{...DEFAULT_PROPS}
/>
</WrapWithProvider>
);
});
it("should render fakespot category dropdown", () => {
assert.ok(wrapper.find(".fakespot-dropdown").exists());
});
it("should render heading copy, context menu, footer copy and cta", () => {
assert.ok(wrapper.find(".context-menu-wrapper").exists());
assert.ok(wrapper.find(".fakespot-desc").exists());
assert.ok(wrapper.find(".fakespot-footer p").exists());
assert.ok(wrapper.find(".fakespot-cta").exists());
});
it("when category is selected, the correct event is dispatched", () => {
const select = wrapper.find(".fakespot-dropdown");
// const barCategoryOption = wrapper.find("option[value='bar']");
select.simulate("change", { target: { value: "bar" } });
assert.calledOnce(dispatch);
assert.calledWith(
dispatch,
ac.DiscoveryStreamUserEvent({
event: "FAKESPOT_CATEGORY",
value: {
category: "bar",
},
})
);
});
it("clicking on fakespot CTA should dispatch the correct event", () => {
const safeAnchor = wrapper.find(".fakespot-cta");
const btn = safeAnchor.find("a");
btn.simulate("click");
assert.calledTwice(dispatch);
const secondCall = dispatch.getCall(1);
assert.deepEqual(
secondCall.args[0],
ac.OnlyToMain({
type: "FAKESPOT_CTA_CLICK",
})
);
});
});
});

View file

@ -221,8 +221,11 @@ export class ProfilesParent extends JSWindowActorParent {
}
case "Profiles:UpdateProfileTheme": {
let themeId = message.data;
this.enableTheme(themeId);
break;
await this.enableTheme(themeId);
// The enable theme promise resolves after the
// "lightweight-theme-styling-update" observer so we know the profile
// theme is up to date at this point.
return SelectableProfileService.currentProfile.theme;
}
}
return null;

View file

@ -32,6 +32,66 @@ XPCOMUtils.defineLazyServiceGetter(
const PROFILES_CRYPTO_SALT_LENGTH_BYTES = 16;
function loadImage(url) {
return new Promise((resolve, reject) => {
let imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
let imageContainer;
let observer = imageTools.createScriptedObserver({
sizeAvailable() {
resolve(imageContainer);
},
});
imageTools.decodeImageFromChannelAsync(
url,
Services.io.newChannelFromURI(
url,
null,
Services.scriptSecurityManager.getSystemPrincipal(),
null, // aTriggeringPrincipal
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
Ci.nsIContentPolicy.TYPE_IMAGE
),
(image, status) => {
if (!Components.isSuccessCode(status)) {
reject(new Components.Exception("Image loading failed", status));
} else {
imageContainer = image;
}
},
observer
);
});
}
async function updateTaskbar(iconUrl, profileName, strokeColor, fillColor) {
try {
let image = await loadImage(iconUrl);
if ("nsIMacDockSupport" in Ci) {
Cc["@mozilla.org/widget/macdocksupport;1"]
.getService(Ci.nsIMacDockSupport)
.setBadgeImage(image, { fillColor, strokeColor });
} else if ("nsIWinTaskbar" in Ci) {
lazy.EveryWindow.registerCallback(
"profiles",
win => {
let iconController = Cc["@mozilla.org/windows-taskbar;1"]
.getService(Ci.nsIWinTaskbar)
.getOverlayIconController(win.docShell);
iconController.setOverlayIcon(image, profileName, {
fillColor,
strokeColor,
});
},
() => {}
);
}
} catch (e) {
console.error(e);
}
}
async function attemptFlush() {
try {
await lazy.ProfileService.asyncFlush();
@ -60,6 +120,7 @@ class SelectableProfileServiceClass {
];
constructor() {
this.themeObserver = this.themeObserver.bind(this);
if (Cu.isInAutomation) {
this.#groupToolkitProfile = {
storeID: "12345678",
@ -161,6 +222,11 @@ class SelectableProfileServiceClass {
// to come after #currentProfile has been set.
this.initWindowTracker();
Services.obs.addObserver(
this.themeObserver,
"lightweight-theme-styling-update"
);
this.#initialized = true;
}
@ -171,6 +237,11 @@ class SelectableProfileServiceClass {
lazy.EveryWindow.unregisterCallback(this.#everyWindowCallbackId);
Services.obs.removeObserver(
this.themeObserver,
"lightweight-theme-styling-update"
);
await this.closeConnection();
this.#currentProfile = null;
@ -383,6 +454,28 @@ class SelectableProfileServiceClass {
*/
observe() {}
/**
* The observer function that watches for theme changes and updates the
* current profile of a theme change.
*
* @param {object} aSubject The theme data
* @param {*} aTopic Should be "lightweight-theme-styling-update"
*/
themeObserver(aSubject, aTopic) {
if (aTopic !== "lightweight-theme-styling-update") {
return;
}
let data = aSubject.wrappedJSObject;
let theme = data.theme;
this.currentProfile.theme = {
themeL10nId: theme.id,
themeFg: theme.textcolor,
themeBg: theme.accentcolor,
};
}
/**
* Init or update the current SelectableProfiles from the DB.
*/

View file

@ -101,8 +101,17 @@ export class EditProfileCard extends MozLitElement {
RPMSendAsyncMessage("Profiles:UpdateProfileName", this.profile);
}
updateTheme(newThemeId) {
RPMSendAsyncMessage("Profiles:UpdateProfileTheme", newThemeId);
async updateTheme(newThemeId) {
if (newThemeId === this.profile.themeL10nId) {
return;
}
let theme = await RPMSendQuery("Profiles:UpdateProfileTheme", newThemeId);
this.profile.themeL10nId = theme.themeL10nId;
this.profile.themeFg = theme.themeFg;
this.profile.themeBg = theme.themeBg;
this.requestUpdate();
}
async updateAvatar(newAvatar) {

View file

@ -53,7 +53,7 @@ function checkForDefaultSetting(
aRealHeight
) {
// We can get the rounded size by subtracting twice the margin.
let targetWidth = aRealWidth - 2 * RFPHelper.steppedRange(aRealWidth);
let targetWidth = aRealWidth - 2 * RFPHelper.steppedRange(aRealWidth, true);
let targetHeight = aRealHeight - 2 * RFPHelper.steppedRange(aRealHeight);
// This platform-specific code is explained in the large comment below.

View file

@ -4,23 +4,26 @@
* maximum values.
*/
let targetWidth = Services.prefs.getIntPref("privacy.window.maxInnerWidth");
let targetHeight = Services.prefs.getIntPref("privacy.window.maxInnerHeight");
OpenTest.run([
{
settingWidth: 1025,
settingHeight: 1050,
targetWidth: 1000,
targetHeight: 1000,
settingWidth: targetWidth + 25,
settingHeight: targetHeight + 50,
targetWidth,
targetHeight,
},
{
settingWidth: 9999,
settingHeight: 9999,
targetWidth: 1000,
targetHeight: 1000,
targetWidth,
targetHeight,
},
{
settingWidth: 999,
settingHeight: 999,
targetWidth: 1000,
targetHeight: 1000,
settingWidth: targetWidth - 1,
settingHeight: targetHeight - 1,
targetWidth,
targetHeight,
},
]);

View file

@ -306,19 +306,28 @@ async function calcMaximumAvailSize(aChromeWidth, aChromeHeight) {
let availWidth = window.screen.availWidth;
let availHeight = window.screen.availHeight;
// Ideally, we would round the window size as 1000x1000. But the available
// screen space might not suffice. So, we decide the size according to the
// available screen size.
let availContentWidth = Math.min(1000, availWidth - chromeUIWidth);
// Ideally, we would round the window size as
// privacy.window.maxInnerWidth x privacy.window.maxInnerHeight. But the
// available screen space might not suffice. So, we decide the size according
// to the available screen size.
let maxInnerWidth = Services.prefs.getIntPref("privacy.window.maxInnerWidth");
let maxInnerHeight = Services.prefs.getIntPref(
"privacy.window.maxInnerHeight"
);
let availContentWidth = Math.min(maxInnerWidth, availWidth - chromeUIWidth);
let availContentHeight;
// If it is GTK window, we would consider the system decorations when we
// calculating avail content height since the system decorations won't be
// reported when we get available screen dimensions.
if (AppConstants.MOZ_WIDGET_GTK) {
availContentHeight = Math.min(1000, -40 + availHeight - chromeUIHeight);
availContentHeight = Math.min(
maxInnerHeight,
-40 + availHeight - chromeUIHeight
);
} else {
availContentHeight = Math.min(1000, availHeight - chromeUIHeight);
availContentHeight = Math.min(maxInnerHeight, availHeight - chromeUIHeight);
}
// Rounded the desire size to the nearest 200x100.

View file

@ -0,0 +1,123 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
});
export const PageWireframes = {
/**
* Returns the wireframe object for the current index of the session history
* for the given tab. The wireframe will only exist with browser.history.collectWireframes.
*
* @param {Object} tab
* @return {Object} wireframe
* See dom/webidl/Document.webidl for the Wireframe dictionary
*/
getWireframeState(tab) {
if (!tab) {
return null;
}
const sessionHistory = lazy.SessionStore.getSessionHistory(tab);
return sessionHistory?.entries[sessionHistory.index]?.wireframe;
},
/**
* Returns an SVG preview for the wireframe at the current index of the session history
* for the given tab. The wireframe will only exist with browser.history.collectWireframes.
*
* @param {Object} tab
* @return {SVGElement}
*/
getWireframeElementForTab(tab) {
const wireframe = this.getWireframeState(tab);
return wireframe && this.getWireframeElement(wireframe, tab.ownerDocument);
},
/**
* Converts a color encoded as a uint32_t (Gecko's nscolor format)
* to an rgb string.
*
* @param {Number} nscolor
* An RGB color encoded in nscolor format.
* @return {String}
* A string of the form "rgb(r, g, b)".
*/
nscolorToRGB(nscolor) {
let r = nscolor & 0xff;
let g = (nscolor >> 8) & 0xff;
let b = (nscolor >> 16) & 0xff;
return `rgb(${r}, ${g}, ${b})`;
},
/**
* Converts a color encoded as a uint32_t (Gecko's nscolor format)
* to an rgb string.
*
* @param {Object} wireframe
* See Bug 1731714 and dom/webidl/Document.webidl for the Wireframe dictionary
* @param {Document} document
* A Document to crate SVG elements.
* @return {SVGElement}
* The rendered wireframe
*/
getWireframeElement(wireframe, document) {
const SVG_NS = "http://www.w3.org/2000/svg";
let svg = document.createElementNS(SVG_NS, "svg");
// Currently guessing width & height from rects on the object, it would be better to
// save these on the wireframe object itself.
let width = wireframe.rects.reduce(
(max, rect) => Math.max(max, rect.x + rect.width),
0
);
let height = wireframe.rects.reduce(
(max, rect) => Math.max(max, rect.y + rect.height),
0
);
svg.setAttributeNS(null, "viewBox", `0 0 ${width} ${height}`);
svg.style.backgroundColor = this.nscolorToRGB(wireframe.canvasBackground);
const DEFAULT_FILL = "color-mix(in srgb, black 10%, transparent)";
for (let rectObj of wireframe.rects) {
// For now we'll skip rects that have an unknown classification, since
// it's not clear how we should treat them.
if (rectObj.type == "unknown") {
continue;
}
let rectEl = document.createElementNS(SVG_NS, "rect");
rectEl.setAttribute("x", rectObj.x);
rectEl.setAttribute("y", rectObj.y);
rectEl.setAttribute("width", rectObj.width);
rectEl.setAttribute("height", rectObj.height);
let fill;
switch (rectObj.type) {
case "background": {
fill = this.nscolorToRGB(rectObj.color);
break;
}
case "image": {
fill = rectObj.color
? this.nscolorToRGB(rectObj.color)
: DEFAULT_FILL;
break;
}
case "text": {
fill = DEFAULT_FILL;
break;
}
}
rectEl.setAttribute("fill", fill);
svg.appendChild(rectEl);
}
return svg;
},
};

View file

@ -12,6 +12,7 @@ JAR_MANIFESTS += ["jar.mn"]
EXTRA_JS_MODULES.sessionstore = [
"GlobalState.sys.mjs",
"PageWireframes.sys.mjs",
"RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs",
"RunState.sys.mjs",
"SessionCookies.sys.mjs",

View file

@ -315,3 +315,5 @@ skip-if = [
["browser_windowRestore_perwindowpb.js"]
["browser_windowStateContainer.js"]
["browser_wireframe_basic.js"]

View file

@ -122,7 +122,7 @@ add_task(async function test_subframes() {
"data:text/html;charset=utf-8," +
"<iframe src=http%3A//example.com/ name=t></iframe>" +
"<a id=a1 href=http%3A//example.com/1 target=t>clickme</a>" +
"<a id=a2 href=http%3A//example.com/%23 target=t>clickme</a>";
"<a id=a2 href=http%3A//example.com/%23section target=t>clickme</a>";
// Create a new tab.
let tab = BrowserTestUtils.addTab(gBrowser, URL);
@ -291,38 +291,3 @@ add_task(async function test_slow_subframe_load() {
// Cleanup.
gBrowser.removeTab(tab);
});
/**
* Ensure that document wireframes can be persisted when they're enabled.
*/
add_task(async function test_wireframes() {
// Wireframes only works when Fission is enabled.
if (!Services.appinfo.fissionAutostart) {
ok(true, "Skipping test_wireframes when Fission is not enabled.");
return;
}
await SpecialPowers.pushPrefEnv({
set: [["browser.history.collectWireframes", true]],
});
let tab = BrowserTestUtils.addTab(gBrowser, "http://example.com");
let browser = tab.linkedBrowser;
await promiseBrowserLoaded(browser);
await TabStateFlusher.flush(browser);
let { entries } = JSON.parse(ss.getTabState(tab));
// Check the number of children.
is(entries.length, 1, "there is one shistory entry");
// Check for the wireframe
ok(entries[0].wireframe, "A wireframe was captured and serialized.");
ok(
entries[0].wireframe.rects.length,
"Several wireframe rects were captured."
);
// Cleanup.
gBrowser.removeTab(tab);
});

View file

@ -0,0 +1,42 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Ensure that document wireframes are persisted when enabled,
* and that we can generate previews for them.
*/
add_task(async function thumbnails_wireframe_basic() {
// Wireframes only works when Fission is enabled.
if (!Services.appinfo.fissionAutostart) {
ok(true, "Skipping test_wireframes when Fission is not enabled.");
return;
}
await SpecialPowers.pushPrefEnv({
set: [["browser.history.collectWireframes", true]],
});
let tab = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"https://www.example.com/"
);
await TabStateFlusher.flush(tab.linkedBrowser);
info("Checking a loaded tab");
checkWireframeForTab(tab);
await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
gBrowser.discardBrowser(tab, true);
info("Checking a discarded tab");
checkWireframeForTab(tab);
gBrowser.removeTab(tab);
});
function checkWireframeForTab(tab) {
let wireframe = PageWireframes.getWireframeState(tab);
ok(wireframe, "After load: Got wireframe state");
Assert.greater(wireframe.rects.length, 0, "After load: Got wireframe rects");
let wireframeElement = PageWireframes.getWireframeElementForTab(tab);
is(wireframeElement.tagName, "svg", "Got wireframe element");
}

View file

@ -1 +1 @@
<a href=#>clickme</a>
<a href=#section>clickme</a>

View file

@ -1 +1 @@
<a href=#>clickme</a>
<a href=#section>clickme</a>

View file

@ -1 +1 @@
<a id=a href=#>clickme</a>
<a id=a href=#section>clickme</a>

View file

@ -33,6 +33,10 @@ const { SessionStoreTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/SessionStoreTestUtils.sys.mjs"
);
const { PageWireframes } = ChromeUtils.importESModule(
"resource:///modules/sessionstore/PageWireframes.sys.mjs"
);
const ss = SessionStore;
SessionStoreTestUtils.init(this, window);

View file

@ -99,6 +99,7 @@ var SidebarController = {
? "sidebar-history-context-menu"
: undefined,
gleanEvent: Glean.history.sidebarToggle,
gleanClickEvent: Glean.sidebar.historyIconClick,
}),
],
[
@ -116,6 +117,7 @@ var SidebarController = {
contextMenuId: this.sidebarRevampEnabled
? "sidebar-synced-tabs-context-menu"
: undefined,
gleanClickEvent: Glean.sidebar.syncedTabsIconClick,
}),
],
[
@ -130,6 +132,7 @@ var SidebarController = {
iconUrl: "chrome://browser/skin/bookmark-hollow.svg",
disabled: true,
gleanEvent: Glean.bookmarks.sidebarToggle,
gleanClickEvent: Glean.sidebar.bookmarksIconClick,
}),
],
]);
@ -145,6 +148,7 @@ var SidebarController = {
// Bug 1900915 to expose as conditional tool
revampL10nId: "sidebar-menu-genai-chat-label",
iconUrl: "chrome://global/skin/icons/highlights.svg",
gleanClickEvent: Glean.sidebar.chatbotIconClick,
}
);
@ -1618,6 +1622,28 @@ var SidebarController = {
}
},
/**
* Record to Glean when any of the sidebar icons are clicked.
*
* @param {string} commandID - Command ID of the icon.
* @param {boolean} expanded - Whether the sidebar was expanded when clicked.
*/
recordIconClick(commandID, expanded) {
const sidebar = this.sidebars.get(commandID);
const isExtension = sidebar && Object.hasOwn(sidebar, "extensionId");
if (isExtension) {
const addonId = sidebar.extensionId;
Glean.sidebar.addonIconClick.record({
sidebar_open: expanded,
addon_id: AMTelemetry.getTrimmedString(addonId),
});
} else if (sidebar.gleanClickEvent) {
sidebar.gleanClickEvent.record({
sidebar_open: expanded,
});
}
},
/**
* Sets the checked state only on the menu items of the specified sidebar, or
* none if the argument is an empty string.

View file

@ -145,6 +145,104 @@ sidebar:
- rtestard@mozilla.com
expires: never
telemetry_mirror: SIDEBAR_LINK
chatbot_icon_click:
type: event
description: >
The chatbot icon was clicked.
bugs:
- https://bugzil.la/1923972
data_reviews:
- https://phabricator.services.mozilla.com/D226681
data_sensitivity:
- interaction
expires: never
notification_emails:
- vsabino@mozilla.com
send_in_pings:
- events
extra_keys:
sidebar_open:
type: boolean
description: Whether the sidebar is expanded or collapsed.
history_icon_click:
type: event
description: >
The history icon was clicked.
bugs:
- https://bugzil.la/1923972
data_reviews:
- https://phabricator.services.mozilla.com/D226681
data_sensitivity:
- interaction
expires: never
notification_emails:
- vsabino@mozilla.com
send_in_pings:
- events
extra_keys:
sidebar_open:
type: boolean
description: Whether the sidebar is expanded or collapsed.
synced_tabs_icon_click:
type: event
description: >
The synced tabs icon was clicked.
bugs:
- https://bugzil.la/1923972
data_reviews:
- https://phabricator.services.mozilla.com/D226681
data_sensitivity:
- interaction
expires: never
notification_emails:
- vsabino@mozilla.com
send_in_pings:
- events
extra_keys:
sidebar_open:
type: boolean
description: Whether the sidebar is expanded or collapsed.
bookmarks_icon_click:
type: event
description: >
The bookmarks icon was clicked.
bugs:
- https://bugzil.la/1923972
data_reviews:
- https://phabricator.services.mozilla.com/D226681
data_sensitivity:
- interaction
expires: never
notification_emails:
- vsabino@mozilla.com
send_in_pings:
- events
extra_keys:
sidebar_open:
type: boolean
description: Whether the sidebar is expanded or collapsed.
addon_icon_click:
type: event
description: >
An extension icon was clicked.
bugs:
- https://bugzil.la/1923972
data_reviews:
- https://phabricator.services.mozilla.com/D226681
data_sensitivity:
- interaction
expires: never
notification_emails:
- vsabino@mozilla.com
send_in_pings:
- events
extra_keys:
sidebar_open:
type: boolean
description: Whether the sidebar is expanded or collapsed.
addon_id:
type: string
description: The extension's ID.
history:
sidebar_toggle:
type: event

View file

@ -157,7 +157,6 @@ export class SidebarHistory extends SidebarPage {
descriptionHeader = "firefoxview-dont-remember-history-empty-header-2";
descriptionLabels = [
"firefoxview-dont-remember-history-empty-description-one",
"firefoxview-dont-remember-history-empty-description-two",
];
descriptionLink = {
url: "about:preferences#privacy",

View file

@ -241,6 +241,7 @@ export default class SidebarMain extends MozLitElement {
}
showView(view) {
window.SidebarController.recordIconClick(view, this.expanded);
window.SidebarController.toggle(view);
if (view === "viewCustomizeSidebar") {
Glean.sidebarCustomize.iconClick.record();

View file

@ -3,7 +3,7 @@
"use strict";
requestLongerTimeout(2);
requestLongerTimeout(10);
const lazy = {};
@ -36,6 +36,8 @@ add_task(async function test_metrics_initialized() {
});
add_task(async function test_sidebar_expand() {
SidebarController.toggleExpanded(false);
info("Expand the sidebar.");
EventUtils.synthesizeMouseAtCenter(SidebarController.toolbarButton, {});
await TestUtils.waitForCondition(
@ -445,3 +447,68 @@ add_task(async function test_sidebar_position_rtl_ui() {
sandbox.restore();
await SpecialPowers.popPrefEnv();
});
async function testIconClick(expanded) {
await SpecialPowers.pushPrefEnv({
set: [
["browser.ml.chat.enabled", true],
["sidebar.main.tools", "aichat,syncedtabs,history,bookmarks"],
],
});
const { sidebarMain } = SidebarController;
const gleanEvents = [
Glean.sidebar.chatbotIconClick,
Glean.sidebar.syncedTabsIconClick,
Glean.sidebar.historyIconClick,
Glean.sidebar.bookmarksIconClick,
];
sidebarMain.toolButtons.forEach((button, i) => {
SidebarController.toggleExpanded(expanded);
info(`Click the icon for: ${button.getAttribute("view")}`);
EventUtils.synthesizeMouseAtCenter(button, {});
const events = gleanEvents[i].testGetValue();
Assert.equal(events?.length, 1, "One event was reported.");
Assert.deepEqual(
events?.[0].extra,
{ sidebar_open: `${expanded}` },
`Event indicates the sidebar was ${expanded ? "expanded" : "collapsed"}.`
);
});
info("Load an extension.");
const extension = ExtensionTestUtils.loadExtension({ ...extData });
await extension.startup();
await extension.awaitMessage("sidebar");
SidebarController.toggleExpanded(expanded);
info("Click the icon for the extension.");
const extensionButton = sidebarMain.extensionButtons[0];
EventUtils.synthesizeMouseAtCenter(extensionButton, {});
const events = Glean.sidebar.addonIconClick.testGetValue();
Assert.equal(events?.length, 1, "One event was reported.");
Assert.equal(
events?.[0].extra.sidebar_open,
`${expanded}`,
`Event indicates the sidebar was ${expanded ? "expanded" : "collapsed"}.`
);
Assert.ok(events?.[0].extra.addon_id, "Event has the extension's ID.");
info("Unload the extension.");
await extension.unload();
await SpecialPowers.popPrefEnv();
Services.fog.testResetFOG();
}
add_task(async function test_icon_click_collapsed_sidebar() {
await testIconClick(false);
});
add_task(async function test_icon_click_expanded_sidebar() {
await testIconClick(true);
});

View file

@ -5,6 +5,10 @@
var { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PageWireframes: "resource:///modules/sessionstore/PageWireframes.sys.mjs",
});
const ZERO_DELAY_ACTIVATION_TIME = 300;
@ -55,6 +59,11 @@ export default class TabHoverPreviewPanel {
"browser.tabs.tooltipsShowPidAndActiveness",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"_prefCollectWireframes",
"browser.history.collectWireframes"
);
this._panelOpener = new TabPreviewPanelTimedFunction(
() => {
@ -108,6 +117,16 @@ export default class TabHoverPreviewPanel {
}
}
_hasValidWireframeState(tab) {
return (
this._prefCollectWireframes &&
this._prefDisplayThumbnail &&
tab &&
!tab.selected &&
!!lazy.PageWireframes.getWireframeState(tab)
);
}
_hasValidThumbnailState(tab) {
return (
this._prefDisplayThumbnail &&
@ -122,6 +141,11 @@ export default class TabHoverPreviewPanel {
let tab = this._tab;
if (!this._hasValidThumbnailState(tab)) {
let wireframeElement = lazy.PageWireframes.getWireframeElementForTab(tab);
if (wireframeElement) {
this._thumbnailElement = wireframeElement;
this._updatePreview();
}
return;
}
let thumbnailCanvas = this._win.document.createElement("canvas");
@ -229,7 +253,8 @@ export default class TabHoverPreviewPanel {
);
thumbnailContainer.classList.toggle(
"hide-thumbnail",
!this._hasValidThumbnailState(this._tab)
!this._hasValidThumbnailState(this._tab) &&
!this._hasValidWireframeState(this._tab)
);
if (thumbnailContainer.firstChild != this._thumbnailElement) {
thumbnailContainer.replaceChildren();

View file

@ -82,7 +82,7 @@
return {
".tab-background":
"selected=visuallyselected,fadein,multiselected,dragover-createGroup",
".tab-line": "selected=visuallyselected,multiselected",
".tab-group-line": "selected=visuallyselected,multiselected",
".tab-loading-burst": "pinned,bursting,notselectedsinceload",
".tab-content":
"pinned,selected=visuallyselected,titlechanged,attention",

View file

@ -552,11 +552,20 @@
}
this.#maxTabsPerRow = tabsPerRow;
}
let selectedTabs = gBrowser.selectedTabs;
let otherSelectedTabs = selectedTabs.filter(
selectedTab => selectedTab != tab
);
let dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
let dataTransferOrderedTabs;
if (!fromTabList) {
let selectedTabs = gBrowser.selectedTabs;
let otherSelectedTabs = selectedTabs.filter(
selectedTab => selectedTab != tab
);
dataTransferOrderedTabs = [tab].concat(otherSelectedTabs);
} else {
// Dragging an item in the tabs list doesn't change the currently
// selected tabs, and it's not possible to select multiple tabs from
// the list, thus handle only the dragged tab in this case.
dataTransferOrderedTabs = [tab];
}
let dt = event.dataTransfer;
for (let i = 0; i < dataTransferOrderedTabs.length; i++) {
@ -2085,25 +2094,18 @@
"dragover-createGroup",
true
);
let groupColorCode = gBrowser.tabGroupMenu.nextUnusedColor;
this.style.setProperty(
"--dragover-tab-group-color",
`var(--tab-group-color-${groupColorCode})`
);
this.style.setProperty(
"--dragover-tab-group-color-invert",
`var(--tab-group-color-${groupColorCode}-invert)`
);
this.style.setProperty(
"--dragover-tab-group-color-pale",
`var(--tab-group-color-${groupColorCode}-pale)`
);
this.#setDragOverGroupColor(gBrowser.tabGroupMenu.nextUnusedColor);
} else {
this.removeAttribute("movingtab-createGroup");
}
}
if (gBrowser._tabGroupsEnabled && !("groupDropIndex" in dragData)) {
this.#setDragOverGroupColor(
this.allTabs[dragData.animDropIndex].group?.color
);
}
if (newIndex == oldIndex) {
return;
}
@ -2119,6 +2121,27 @@
}
}
#setDragOverGroupColor(groupColorCode) {
if (!groupColorCode) {
this.style.removeProperty("--dragover-tab-group-color");
this.style.removeProperty("--dragover-tab-group-color-invert");
this.style.removeProperty("--dragover-tab-group-color-pale");
return;
}
this.style.setProperty(
"--dragover-tab-group-color",
`var(--tab-group-color-${groupColorCode})`
);
this.style.setProperty(
"--dragover-tab-group-color-invert",
`var(--tab-group-color-${groupColorCode}-invert)`
);
this.style.setProperty(
"--dragover-tab-group-color-pale",
`var(--tab-group-color-${groupColorCode}-pale)`
);
}
_finishAnimateTabMove() {
if (!this.hasAttribute("movingtab")) {
return;
@ -2131,6 +2154,7 @@
this.removeAttribute("movingtab");
this.removeAttribute("movingtab-createGroup");
this.#setDragOverGroupColor(null);
gNavToolbox.removeAttribute("movingtab");
this._handleTabSelect();

View file

@ -251,3 +251,95 @@ add_task(async function test_move_to_different_tab_bar() {
await BrowserTestUtils.closeWindow(newWindow2);
});
add_task(async function test_drag_and_drop_to_bookmark_toolbar() {
await SpecialPowers.pushPrefEnv({
set: [["browser.toolbars.bookmarks.visibility", "always"]],
});
await PlacesUtils.bookmarks.eraseEverything();
registerCleanupFunction(async () => {
await PlacesUtils.bookmarks.eraseEverything();
});
await testWithNewWindow(async function (newWindow) {
is(
await PlacesUtils.bookmarks.fetch({
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
index: 0,
}),
null,
"The bookmark toolbar shouldn't have any item"
);
const bookmarkToolbar =
newWindow.document.getElementById("PlacesToolbarItems");
// Wait if the bookmark toolbar initialization hasn't finished.
await BrowserTestUtils.waitForMutationCondition(
bookmarkToolbar,
{ attributes: true, childNodes: true, attributeFilter: ["collapsed"] },
() => !bookmarkToolbar.collapsed && !bookmarkToolbar.childNodes.length
);
newWindow.gBrowser.removeTab(newWindow.gBrowser.selectedTab);
const list = newWindow.document.getElementById(
"allTabsMenu-allTabsView-tabs"
);
assertOrder(getOrderOfList(list), [1, 2, 3, 4, 5], "0-th tab is closed");
is(
toIndex(newWindow.gBrowser.selectedTab.linkedBrowser.currentURI.spec),
1,
"1th tab is active"
);
const { PlacesTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PlacesTestUtils.sys.mjs"
);
const bookmarkPromise = PlacesTestUtils.waitForNotification(
"bookmark-added",
events => events.some(e => e.url == URL5)
);
// Drag and drop the 5st tab to the bookmark toolbar, while the active tab
// is the 1th tab.
const rows = list.querySelectorAll("toolbaritem");
EventUtils.synthesizeDrop(
rows[4],
bookmarkToolbar,
null,
"move",
newWindow,
newWindow,
{ clientX: 0, clientY: 0 }
);
await bookmarkPromise;
is(
(
await PlacesUtils.bookmarks.fetch({
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
index: 0,
})
).url.href,
URL5,
"5th tab should be bookmarked"
);
is(
await PlacesUtils.bookmarks.fetch({
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
index: 1,
}),
null,
"No other tabs should be bookmarked"
);
});
await SpecialPowers.popPrefEnv();
});

View file

@ -341,6 +341,68 @@ add_task(async function thumbnailTests() {
});
});
/**
* Verify that non-selected tabs display a wireframe in their preview
* when enabled, and the tab is unable to provide a thumbnail (e.g. unloaded).
*/
add_task(async function wireframeTests() {
const { TabStateFlusher } = ChromeUtils.importESModule(
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
);
await SpecialPowers.pushPrefEnv({
set: [
["browser.tabs.hoverPreview.showThumbnails", true],
["browser.history.collectWireframes", true],
],
});
const tab1 = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"data:text/html,<html><head><title>First New Tab</title></head><body>Hello</body></html>"
);
const tab2 = await BrowserTestUtils.openNewForegroundTab(
gBrowser,
"about:blank"
);
// Discard the first tab so it can't provide a thumbnail image
await TabStateFlusher.flush(tab1.linkedBrowser);
gBrowser.discardBrowser(tab1, true);
const previewPanel = document.getElementById("tab-preview-panel");
let thumbnailUpdated = BrowserTestUtils.waitForEvent(
previewPanel,
"previewThumbnailUpdated",
false,
evt => evt.detail.thumbnail
);
await openPreview(tab1);
await thumbnailUpdated;
Assert.ok(
previewPanel.querySelectorAll(".tab-preview-thumbnail-container svg")
.length,
"Tab1 preview contains wireframe"
);
const previewHidden = BrowserTestUtils.waitForPopupEvent(
previewPanel,
"hidden"
);
BrowserTestUtils.removeTab(tab1);
BrowserTestUtils.removeTab(tab2);
await SpecialPowers.popPrefEnv();
// Removing the tab should close the preview.
await previewHidden;
// Move the mouse outside of the tab strip.
EventUtils.synthesizeMouseAtCenter(document.documentElement, {
type: "mouseover",
});
});
/**
* make sure delay is applied when mouse leaves tabstrip
* but not when moving between tabs on the tabstrip

View file

@ -19,6 +19,7 @@ ChromeUtils.defineLazyGetter(lazy, "SearchModeSwitcherL10n", () => {
* Implements the SearchModeSwitcher in the urlbar.
*/
export class SearchModeSwitcher {
static DEFAULT_ICON = lazy.UrlbarUtils.ICON.SEARCH_GLASS;
#engineListNeedsRebuild = true;
#popup;
#input;
@ -208,8 +209,9 @@ export class SearchModeSwitcher {
try {
await lazy.UrlbarSearchUtils.init();
} catch {
// We should still work if the SearchService is not working.
console.error("Search service failed to init");
}
let { label, icon } = await this.#getDisplayedEngineDetails(
this.#input.searchMode
);
@ -217,18 +219,26 @@ export class SearchModeSwitcher {
const keywordEnabled = lazy.UrlbarPrefs.get("keyword.enabled");
const inSearchMode = this.#input.searchMode;
if (!keywordEnabled && !inSearchMode) {
icon = lazy.UrlbarUtils.ICON.SEARCH_GLASS;
icon = SearchModeSwitcher.DEFAULT_ICON;
}
let iconUrl = icon ? `url(${icon})` : "";
this.#input.document.getElementById(
"searchmode-switcher-icon"
).style.listStyleImage = iconUrl;
this.#input.document.l10n.setAttributes(
this.#toolbarbutton,
"urlbar-searchmode-button",
{ engine: label }
);
if (label) {
this.#input.document.l10n.setAttributes(
this.#toolbarbutton,
"urlbar-searchmode-button2",
{ engine: label }
);
} else {
this.#input.document.l10n.setAttributes(
this.#toolbarbutton,
"urlbar-searchmode-button-no-engine"
);
}
let labelEl = this.#input.document.getElementById(
"searchmode-switcher-title"
@ -252,6 +262,10 @@ export class SearchModeSwitcher {
}
async #getDisplayedEngineDetails(searchMode = null) {
if (!Services.search.hasSuccessfullyInitialized) {
return { label: null, icon: SearchModeSwitcher.DEFAULT_ICON };
}
if (!searchMode || searchMode.engineName) {
let engine = searchMode
? lazy.UrlbarSearchUtils.getEngineByName(searchMode.engineName)
@ -271,7 +285,13 @@ export class SearchModeSwitcher {
async #rebuildSearchModeList() {
let container = this.#popup.querySelector(".panel-subview-body");
container.replaceChildren();
let engines = await Services.search.getVisibleEngines();
let engines = [];
try {
engines = await Services.search.getVisibleEngines();
} catch {
console.error("Failed to fetch engines");
}
let frag = this.#input.document.createDocumentFragment();
let remoteContainer = this.#input.document.createXULElement("vbox");
remoteContainer.className = "remote-options";

View file

@ -192,6 +192,9 @@ const PREF_URLBAR_DEFAULTS = new Map([
// Timeout for Merino fetches (ms).
["merino.timeoutMs", 200],
// Set default NER threshold value of 0.5
["nerThreshold", [0.5, "float"]],
// Whether addresses and search results typed into the address bar
// should be opened in new tabs by default.
["openintab", false],

View file

@ -140,39 +140,17 @@ class ProviderQuickSuggest extends UrlbarProvider {
promises.push(this._fetchMerinoSuggestions(queryContext, searchString));
}
// Wait for both sources to finish before adding a suggestion.
// Wait for both sources to finish.
let values = await Promise.all(promises);
if (instance != this.queryInstance) {
return;
}
let suggestions = values.flat();
// Ensure all suggestions have a `score` by falling back to the default
// score as necessary. If `quickSuggestScoreMap` is defined, override scores
// with the values it defines. It maps telemetry types to scores.
let scoreMap = lazy.UrlbarPrefs.get("quickSuggestScoreMap");
for (let suggestion of suggestions) {
if (isNaN(suggestion.score)) {
suggestion.score = DEFAULT_SUGGESTION_SCORE;
}
if (scoreMap) {
let telemetryType = this.#getSuggestionTelemetryType(suggestion);
if (scoreMap.hasOwnProperty(telemetryType)) {
let score = parseFloat(scoreMap[telemetryType]);
if (!isNaN(score)) {
suggestion.score = score;
}
}
}
let suggestions = await this.#filterAndSortSuggestions(values.flat());
if (instance != this.queryInstance) {
return;
}
suggestions.sort((a, b) => b.score - a.score);
// All suggestions should have the following keys at this point. They are
// required for looking up the features that manage them.
let requiredKeys = ["source", "provider"];
// Convert each suggestion into a result and add it. Don't add more than
// `maxResults` visible results so we don't spam the muxer.
let remainingCount = queryContext.maxResults ?? 10;
@ -181,16 +159,6 @@ class ProviderQuickSuggest extends UrlbarProvider {
break;
}
for (let key of requiredKeys) {
if (!suggestion[key]) {
this.logger.error(
`Suggestion is missing required key '${key}': ` +
JSON.stringify(suggestion)
);
continue;
}
}
let canAdd = await this._canAddSuggestion(suggestion);
if (instance != this.queryInstance) {
return;
@ -210,6 +178,74 @@ class ProviderQuickSuggest extends UrlbarProvider {
}
}
async #filterAndSortSuggestions(suggestions) {
let requiredKeys = ["source", "provider"];
let scoreMap = lazy.UrlbarPrefs.get("quickSuggestScoreMap");
let suggestionsByFeature = new Map();
let indexesBySuggestion = new Map();
for (let i = 0; i < suggestions.length; i++) {
let suggestion = suggestions[i];
// Discard suggestions that don't have the required keys, which are used
// to look up their features. Normally this shouldn't ever happen.
if (!requiredKeys.every(key => suggestion[key])) {
this.logger.error(
"Suggestion is missing one or more required keys: " +
JSON.stringify({ requiredKeys, suggestion })
);
continue;
}
// Ensure all suggestions have scores. `quickSuggestScoreMap`, if defined,
// maps telemetry types to score overrides.
if (isNaN(suggestion.score)) {
suggestion.score = DEFAULT_SUGGESTION_SCORE;
}
if (scoreMap) {
let telemetryType = this.#getSuggestionTelemetryType(suggestion);
if (scoreMap.hasOwnProperty(telemetryType)) {
let score = parseFloat(scoreMap[telemetryType]);
if (!isNaN(score)) {
suggestion.score = score;
}
}
}
// Save some state used below to build the final list of suggestions.
let feature = this.#getFeature(suggestion);
let featureSuggestions = suggestionsByFeature.get(feature);
if (!featureSuggestions) {
featureSuggestions = [];
suggestionsByFeature.set(feature, featureSuggestions);
}
featureSuggestions.push(suggestion);
indexesBySuggestion.set(suggestion, i);
}
// Let each feature filter its suggestions.
suggestions = (
await Promise.all(
[...suggestionsByFeature].map(([feature, featureSuggestions]) =>
feature
? feature.filterSuggestions(featureSuggestions)
: Promise.resolve(featureSuggestions)
)
)
).flat();
// Sort the suggestions. When scores are equal, sort by original index to
// ensure a stable sort.
suggestions.sort((a, b) => {
return (
b.score - a.score ||
indexesBySuggestion.get(a) - indexesBySuggestion.get(b)
);
});
return suggestions;
}
onImpression(state, queryContext, controller, providerVisibleResults) {
// Legacy Suggest telemetry should be recorded when a Suggest result is
// visible at the end of an engagement on any result.
@ -702,17 +738,22 @@ class ProviderQuickSuggest extends UrlbarProvider {
suggestion.categories,
true // adjustment needed b/c Merino uses the original encoding
);
Glean.suggestRelevance.status.success.add(1);
let oldScore = suggestion.score;
if (isNaN(oldScore)) {
oldScore = DEFAULT_SUGGESTION_SCORE;
}
suggestion.score = (oldScore + score) / 2;
Glean.suggestRelevance.outcome[
suggestion.score >= oldScore ? "boosted" : "decreased"
].add(1);
this.logger.debug(
`Updated the suggestion score from '${oldScore}' to '${suggestion.score.toFixed(
2
)}'`
);
} catch (error) {
Glean.suggestRelevance.status.failure.add(1);
this.logger.error(
`Failed to update the suggestion score: '${error}'`
);

View file

@ -1023,3 +1023,46 @@ suggest:
expires: "never"
data_sensitivity:
- technical
# Relevance scoring & ranking metrics for Firefox Suggest.
# This feature is still in the experimentation phase and all the metrics are
# set to expire by 136 for now.
suggest_relevance:
status:
type: labeled_counter
description: >
Count the successful / failed attempts of relevance scoring in
Firefox Suggest.
labels:
- success
- failure
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1926315
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1926315
notification_emails:
- disco-team@mozilla.com
- najiang@mozilla.com
expires: 136
data_sensitivity:
- technical
outcome:
type: labeled_counter
description: >
For each successful scoring, count whether the relevance score gets
boosted or decreased over the original score. Note that given how the
score is calculated, it's practically impossible to have the two scores
tied. If that's the case anyhow, it will increment the "boosted" counter.
labels:
- boosted
- decreased
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1926315
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1926315
notification_emails:
- disco-team@mozilla.com
- najiang@mozilla.com
expires: 136
data_sensitivity:
- technical

View file

@ -72,6 +72,7 @@ EXTRA_JS_MODULES["urlbar/private"] += [
"private/FakespotSuggestions.sys.mjs",
"private/ImpressionCaps.sys.mjs",
"private/MDNSuggestions.sys.mjs",
"private/MLSuggest.sys.mjs",
"private/PocketSuggestions.sys.mjs",
"private/SuggestBackendJs.sys.mjs",
"private/SuggestBackendRust.sys.mjs",

View file

@ -178,10 +178,33 @@ export class BaseFeature {
return null;
}
/**
* If the feature corresponds to a type of suggestion, the subclass may
* override this method as necessary. It will be called once per query with
* all of the feature's suggestions that matched the query. It should return
* the subset that should be shown to the user. This is useful in cases where
* a source (Rust, Merino) may return many suggestions for the feature but
* only some of them should be shown, and the criteria for determining which
* to show are external to the source.
*
* `makeResult()` can also be used to filter suggestions by returning null for
* suggestions that should be discarded. Use `filterSuggestions()` when you
* need to know all matching suggestions in order to decide which to show.
*
* @param {Array} suggestions
* The suggestions that matched a query.
* @returns {Array}
* The subset of `suggestions` that should be shown (typically all).
*/
async filterSuggestions(suggestions) {
return suggestions;
}
/**
* If the feature corresponds to a type of suggestion, the subclass should
* override this method. It should return a new `UrlbarResult` for a given
* suggestion, which can come from either remote settings or Merino.
* suggestion, which can come from either remote settings or Merino, or null
* if no result should be shown for the suggestion.
*
* @param {UrlbarQueryContext} _queryContext
* The query context.
@ -191,8 +214,8 @@ export class BaseFeature {
* The search string that was used to fetch the suggestion. It may be
* different from `queryContext.searchString` due to trimming, lower-casing,
* etc. This is included as a param in case it's useful.
* @returns {UrlbarResult}
* A new result for the suggestion.
* @returns {UrlbarResult|null}
* A new result for the suggestion or null if a result should not be shown.
*/
async makeResult(_queryContext, _suggestion, _searchString) {
return null;

View file

@ -0,0 +1,314 @@
/* 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 https://mozilla.org/MPL/2.0/. */
/**
* MLSuggest helps with ML based suggestions around intents and location.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
createEngine: "chrome://global/content/ml/EngineProcess.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
});
/**
* These INTENT_OPTIONS and NER_OPTIONS will go to remote setting server and depends
* on https://bugzilla.mozilla.org/show_bug.cgi?id=1923553
*/
const INTENT_OPTIONS = {
taskName: "text-classification",
modelId: "mozilla/mobilebert-uncased-finetuned-LoRA-intent-classifier",
modelRevision: "v0.1.0",
dtype: "q8",
};
const NER_OPTIONS = {
taskName: "token-classification",
modelId: "mozilla/distilbert-uncased-NER-LoRA",
modelRevision: "v0.1.1",
dtype: "q8",
};
// List of prepositions used in subject cleaning.
const PREPOSITIONS = ["in", "at", "on", "for", "to", "near"];
/**
* Class for handling ML-based suggestions using intent and NER models.
*
* @class
*/
class _MLSuggest {
#modelEngines = {};
/**
* Initializes the intent and NER models.
*/
async initialize() {
await Promise.all([
this.#initializeModelEngine(INTENT_OPTIONS),
this.#initializeModelEngine(NER_OPTIONS),
]);
}
/**
* Generates ML-based suggestions by finding intent, detecting entities, and
* combining locations.
*
* @param {string} query
* The user's input query.
* @returns {object | null}
* The suggestion result including intent, location, and subject, or null if
* an error occurs.
* {string} intent
* The predicted intent label of the query.
* - {object|null} location: The detected location from the query, which is
* an object with `city` and `state` fields:
* - {string|null} city: The detected city, or `null` if no city is found.
* - {string|null} state: The detected state, or `null` if no state is found.
* {string} subject
* The subject of the query after location is removed.
* {object} metrics
* The combined metrics from NER model results, representing additional
* information about the model's performance.
*/
async makeSuggestions(query) {
let intentRes, nerResult;
try {
[intentRes, nerResult] = await Promise.all([
this._findIntent(query),
this._findNER(query),
]);
} catch (error) {
return null;
}
if (!intentRes || !nerResult) {
return null;
}
const locationResVal = await this.#combineLocations(
nerResult,
lazy.UrlbarPrefs.get("nerThreshold")
);
return {
intent: intentRes,
location: locationResVal,
subject: this.#findSubjectFromQuery(query, locationResVal),
metrics: this.#sumObjectsByKey(intentRes.metrics, nerResult.metrics),
};
}
/**
* Shuts down all initialized engines.
*/
async shutdown() {
for (const [key, engine] of Object.entries(this.#modelEngines)) {
try {
await engine.terminate?.();
} finally {
// Remove each engine after termination
delete this.#modelEngines[key];
}
}
}
/**
* Helper method to generate a unique key for model engines.
*
* @param {object} options
* The options object containing taskName and modelId.
* @returns {string}
* The key for the model engine.
*/
#getmodelEnginesKey(options) {
return `${options.taskName}-${options.modelId}`;
}
async #initializeModelEngine(options) {
const engineId = this.#getmodelEnginesKey(options);
// uses cache if engine was used
if (this.#modelEngines[engineId]) {
return this.#modelEngines[engineId];
}
const engine = await lazy.createEngine({ ...options, engineId });
// Cache the engine
this.#modelEngines[engineId] = engine;
return engine;
}
/**
* Finds the intent of the query using the intent classification model.
* (This has been made public to enable testing)
*
* @param {string} query
* The user's input query.
* @param {object} options
* The options for the engine pipeline
* @returns {string|null}
* The predicted intent label or null if the model is not initialized.
*/
async _findIntent(query, options = {}) {
const engineIntentClassifier =
this.#modelEngines[this.#getmodelEnginesKey(INTENT_OPTIONS)];
if (!engineIntentClassifier) {
return null;
}
const res = await engineIntentClassifier.run({
args: [query],
options,
});
// Return the first label from the result
return res[0].label;
}
/**
* Finds named entities in the query using the NER model.
* (This has been made public to enable testing)
*
* @param {string} query
* The user's input query.
* @param {object} options
* The options for the engine pipeline
* @returns {object[] | null}
* The NER results or null if the model is not initialized.
*/
async _findNER(query, options = {}) {
const engineNER = this.#modelEngines[this.#getmodelEnginesKey(NER_OPTIONS)];
return engineNER?.run({ args: [query], options });
}
/**
* Combines location tokens detected by NER into separate city and state
* components. This method processes city, state, and combined city-state
* entities, returning an object with `city` and `state` fields.
*
* Handles the following entity types:
* - B-CITY, I-CITY: Identifies city tokens.
* - B-STATE, I-STATE: Identifies state tokens.
* - B-CITYSTATE, I-CITYSTATE: Identifies tokens that represent a combined
* city and state.
*
* @param {object[]} nerResult
* The NER results containing tokens and their corresponding entity labels.
* @param {number} nerThreshold
* The confidence threshold for including entities. Tokens with a confidence
* score below this threshold will be ignored.
* @returns {object}
* An object with `city` and `state` fields:
* - {string|null} city: The detected city, or `null` if no city is found.
* - {string|null} state: The detected state, or `null` if no state is found.
*/
async #combineLocations(nerResult, nerThreshold) {
let cityResult = [];
let stateResult = [];
let cityStateResult = [];
for (let i = 0; i < nerResult.length; i++) {
const res = nerResult[i];
// Handle B-CITY, I-CITY
if (
(res.entity === "B-CITY" || res.entity === "I-CITY") &&
res.score > nerThreshold
) {
if (res.word.startsWith("##") && cityResult.length) {
cityResult[cityResult.length - 1] += res.word.slice(2);
} else {
cityResult.push(res.word);
}
}
// Handle B-STATE, I-STATE
else if (
(res.entity === "B-STATE" || res.entity === "I-STATE") &&
res.score > nerThreshold
) {
if (res.word.startsWith("##") && stateResult.length) {
stateResult[stateResult.length - 1] += res.word.slice(2);
} else {
stateResult.push(res.word);
}
}
// Handle B-CITYSTATE, I-CITYSTATE
else if (
(res.entity === "B-CITYSTATE" || res.entity === "I-CITYSTATE") &&
res.score > nerThreshold
) {
if (res.word.startsWith("##") && cityStateResult.length) {
cityStateResult[cityStateResult.length - 1] += res.word.slice(2);
} else {
cityStateResult.push(res.word);
}
}
}
// Handle city_state as combined and split into city and state
if (cityStateResult.length) {
let cityStateSplit = cityStateResult.join(" ").split(",");
return {
city: cityStateSplit[0]?.trim() || null,
state: cityStateSplit[1]?.trim() || null,
};
}
// Return city and state as separate components if detected
return {
city: cityResult.join(" ").trim() || null,
state: stateResult.join(" ").trim() || null,
};
}
#findSubjectFromQuery(query, location) {
// If location is null or no city/state, return the entire query
if (!location || (!location.city && !location.state)) {
return query;
}
// Remove the city and state from the query
let subjectWithoutLocation = query;
if (location.city) {
subjectWithoutLocation = subjectWithoutLocation
.replace(location.city, "")
.trim();
}
if (location.state) {
subjectWithoutLocation = subjectWithoutLocation
.replace(location.state, "")
.trim();
}
// Remove leftover commas, trailing whitespace, and unnecessary punctuation
subjectWithoutLocation = subjectWithoutLocation
.replaceAll(",", "")
.replace(/\s+/g, " ")
.trim();
return this.#cleanSubject(subjectWithoutLocation);
}
#cleanSubject(subject) {
let end = PREPOSITIONS.find(
p => subject === p || subject.endsWith(" " + p)
);
if (end) {
subject = subject.substring(0, subject.length - end.length).trimEnd();
}
return subject;
}
#sumObjectsByKey(...objs) {
return objs.reduce((a, b) => {
for (let k in b) {
if (b.hasOwnProperty(k)) a[k] = (a[k] || 0) + b[k];
}
return a;
}, {});
}
}
// Export the singleton instance
export var MLSuggest = new _MLSuggest();

View file

@ -231,7 +231,15 @@ export class Weather extends BaseFeature {
}
}
async makeResult(queryContext, _suggestion, searchString) {
async filterSuggestions(suggestions) {
// Rust will return many suggestions when the query matches multiple cities,
// one suggestion per city. All suggestions will have the same score, but
// they'll be ordered by population size from largest to smallest. Take the
// first suggestion, the one with the largest population.
return suggestions.length ? [suggestions[0]] : suggestions;
}
async makeResult(queryContext, suggestion, searchString) {
// The Rust component doesn't enforce a minimum keyword length, so discard
// the suggestion if the search string isn't long enough. This conditional
// will always be false for the JS backend since in that case keywords are
@ -244,10 +252,20 @@ export class Weather extends BaseFeature {
this.#merino = new lazy.MerinoClient(this.constructor.name);
}
// Set up location params to pass to Merino. We need to null-check each
// suggestion property because `MerinoClient` will stringify null values.
let otherParams = {};
for (let key of ["city", "region", "country"]) {
if (suggestion[key]) {
otherParams[key] = suggestion[key];
}
}
let merino = this.#merino;
let fetchInstance = (this.#fetchInstance = {});
let suggestions = await merino.fetch({
query: "",
otherParams,
providers: [MERINO_PROVIDER],
timeoutMs: this.#timeoutMs,
extraLatencyHistogram: HISTOGRAM_LATENCY,
@ -260,7 +278,7 @@ export class Weather extends BaseFeature {
if (!suggestions.length) {
return null;
}
let suggestion = suggestions[0];
suggestion = suggestions[0];
let unit = Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c";
return Object.assign(

View file

@ -320,6 +320,11 @@ support-files = ["file_urlbar_edit_dos.html"]
["browser_middleClick.js"]
fail-if = ["a11y_checks"] # Bug 1854660 clicked element may not be focusable and/or labeled
["browser_MLSuggest_integration.js"]
support-files = [
"!/toolkit/components/ml/tests/browser/head.js",
]
["browser_move_tab_to_new_window.js"]
["browser_new_tab_urlbar_reset.js"]
@ -415,6 +420,8 @@ https_first_disabled = true
["browser_restrict_keywords.js"]
["browser_restrict_keywords_autofill.js"]
["browser_resultSpan.js"]
["browser_result_menu.js"]

View file

@ -0,0 +1,271 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test for MLSuggest.sys.mjs.
*/
"use strict";
ChromeUtils.defineESModuleGetters(this, {
MLSuggest: "resource:///modules/urlbar/private/MLSuggest.sys.mjs",
});
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/toolkit/components/ml/tests/browser/head.js",
this
);
let nerResultsMap = {
"restaurants in seattle, wa": [
{
entity: "B-CITYSTATE",
score: 0.9999846816062927,
index: 3,
word: "seattle",
},
{
entity: "I-CITYSTATE",
score: 0.9999918341636658,
index: 4,
word: ",",
},
{
entity: "I-CITYSTATE",
score: 0.9999667406082153,
index: 5,
word: "wa",
},
],
"hotels in new york, ny": [
{
entity: "B-CITYSTATE",
score: 0.999022364616394,
index: 3,
word: "new",
},
{
entity: "I-CITYSTATE",
score: 0.9999206066131592,
index: 4,
word: "york",
},
{
entity: "I-CITYSTATE",
score: 0.9999917149543762,
index: 5,
word: ",",
},
{
entity: "I-CITYSTATE",
score: 0.9999532103538513,
index: 6,
word: "ny",
},
],
"restaurants seattle": [
{
entity: "B-CITY",
score: 0.9980050921440125,
index: 2,
word: "seattle",
},
],
"restaurants in seattle": [
{
entity: "B-CITY",
score: 0.9980319738388062,
index: 3,
word: "seattle",
},
],
"restaurants near seattle": [
{
entity: "B-CITY",
score: 0.998751163482666,
index: 3,
word: "seattle",
},
],
"seattle restaurants": [
{
entity: "B-CITY",
score: 0.8563504219055176,
index: 1,
word: "seattle",
},
],
"seattle wa restaurants": [
{
entity: "B-CITY",
score: 0.5729296207427979,
index: 1,
word: "seattle",
},
{
entity: "B-STATE",
score: 0.7850125432014465,
index: 2,
word: "wa",
},
],
"seattle, wa restaurants": [
{
entity: "B-CITYSTATE",
score: 0.9999499320983887,
index: 1,
word: "seattle",
},
{
entity: "I-CITYSTATE",
score: 0.9999974370002747,
index: 2,
word: ",",
},
{
entity: "I-CITYSTATE",
score: 0.9999855160713196,
index: 3,
word: "wa",
},
],
"dumplings in ca": [
{
entity: "B-STATE",
score: 0.998980700969696,
index: 4,
word: "ca",
},
],
};
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.nerThreshold", 0.5]],
});
// Stub these out so we don't end up invoking engine calls for now
// until end-to-end engine calls work
sinon.stub(MLSuggest, "_findIntent").returns("yelp_intent");
sinon.stub(MLSuggest, "_findNER").callsFake(query => {
return nerResultsMap[query] || [];
});
registerCleanupFunction(async function () {
sinon.restore();
});
});
async function setup() {
const { removeMocks, remoteClients } = await createAndMockMLRemoteSettings({
autoDownloadFromRemoteSettings: false,
});
await SpecialPowers.pushPrefEnv({
set: [
// Enabled by default.
["browser.ml.enable", true],
["browser.ml.logLevel", "All"],
["browser.ml.modelCacheTimeout", 1000],
],
});
return {
remoteClients,
async cleanup() {
await removeMocks();
await waitForCondition(
() => EngineProcess.areAllEnginesTerminated(),
"Waiting for all of the engines to be terminated.",
100,
200
);
},
};
}
// Helper function to test suggestions
async function testSuggestion(
query,
expectedCity,
expectedState,
remoteClients
) {
let suggestion = MLSuggest.makeSuggestions(query);
await remoteClients["ml-onnx-runtime"].rejectPendingDownloads(1);
suggestion = await suggestion;
info("Got suggestion for query: " + query);
info("Got suggestion: " + JSON.stringify(suggestion));
Assert.ok(suggestion, "MLSuggest returned a result");
if (expectedCity) {
Assert.equal(
suggestion.location.city,
expectedCity,
"City extraction is correct"
);
}
if (expectedState) {
Assert.equal(
suggestion.location.state,
expectedState,
"State extraction is correct"
);
}
}
add_task(async function test_MLSuggest() {
const { cleanup, remoteClients } = await setup();
await MLSuggest.initialize();
await testSuggestion(
"restaurants in seattle, wa",
"seattle",
"wa",
remoteClients
);
await testSuggestion(
"hotels in new york, ny",
"new york",
"ny",
remoteClients
);
await testSuggestion("restaurants seattle", "seattle", null, remoteClients);
await testSuggestion(
"restaurants in seattle",
"seattle",
null,
remoteClients
);
await testSuggestion(
"restaurants near seattle",
"seattle",
null,
remoteClients
);
await testSuggestion("seattle restaurants", "seattle", null, remoteClients);
await testSuggestion(
"seattle wa restaurants",
"seattle",
"wa",
remoteClients
);
await testSuggestion(
"seattle, wa restaurants",
"seattle",
"wa",
remoteClients
);
await testSuggestion("dumplings in ca", null, "ca", remoteClients);
Assert.strictEqual(
Services.prefs.getFloatPref("browser.urlbar.nerThreshold"),
0.5,
"nerThreshold pref should have the expected default value"
);
await MLSuggest.shutdown();
await EngineProcess.destroyMLEngine();
await cleanup();
});

View file

@ -0,0 +1,90 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
// Tests autofill functionality for restrict keywords (@tabs, @bookmarks,
// @history, @actions) by typing the full or partial keyword to enter local
// search mode.
"use strict";
ChromeUtils.defineESModuleGetters(this, {
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
});
let gFluentStrings = new Localization(["browser/browser.ftl"]);
add_setup(async function () {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.searchRestrictKeywords.featureGate", true]],
});
});
async function assertAutofill(
searchString,
searchMode,
entry,
userEventAction
) {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: searchString,
});
if (userEventAction) {
userEventAction();
}
await UrlbarTestUtils.assertSearchMode(window, {
...searchMode,
entry,
restrictType: "keyword",
});
await UrlbarTestUtils.exitSearchMode(window);
}
async function getSearchModeKeywords() {
let searchModeKeys = [
"urlbar-search-mode-bookmarks",
"urlbar-search-mode-tabs",
"urlbar-search-mode-history",
"urlbar-search-mode-actions",
];
let [bookmarks, tabs, history, actions] = await Promise.all(
searchModeKeys.map(key =>
gFluentStrings.formatValue(key).then(str => `@${str.toLowerCase()}`)
)
);
return [bookmarks, tabs, history, actions];
}
add_task(async function test_autofill_enters_search_mode() {
let [bookmarks, tabs, history, actions] = await getSearchModeKeywords();
const keywordToToken = new Map([
[history, UrlbarTokenizer.RESTRICT.HISTORY],
[bookmarks, UrlbarTokenizer.RESTRICT.BOOKMARK],
[tabs, UrlbarTokenizer.RESTRICT.OPENPAGE],
[actions, UrlbarTokenizer.RESTRICT.ACTION],
]);
for (const [keyword, token] of keywordToToken) {
let searchMode = UrlbarUtils.searchModeForToken(token);
let searchString = `${keyword} `;
info("Test full keyword");
await assertAutofill(searchString, searchMode, "typed", null);
info("Test partial keyword autofill by pressing right arrow");
searchString = keyword.slice(0, 3);
await assertAutofill(searchString, searchMode, "typed", () =>
EventUtils.synthesizeKey("KEY_ArrowRight")
);
info("Test partial keyword autofill by pressing enter");
await assertAutofill(searchString, searchMode, "keywordoffer", () =>
EventUtils.synthesizeKey("KEY_Enter")
);
}
});

View file

@ -785,3 +785,63 @@ add_task(async function test_readonly() {
await closedPopupPromise;
gBrowser.removeCurrentTab();
});
add_task(async function test_search_service_fail() {
let newWin = await BrowserTestUtils.openNewBrowserWindow();
const stub = sinon
.stub(UrlbarSearchUtils, "init")
.rejects(new Error("Initialization failed"));
Services.search.wrappedJSObject.forceInitializationStatusForTests(
"not initialized"
);
// Force updateSearchIcon to be triggered
await SpecialPowers.pushPrefEnv({
set: [["keyword.enabled", false]],
});
let searchModeSwitcherButton = newWin.document.getElementById(
"searchmode-switcher-icon"
);
const searchGlassIconUrl = UrlbarUtils.ICON.SEARCH_GLASS;
// match and capture the URL inside `url("...")`
let regex = /url\("([^"]+)"\)/;
let searchModeSwitcherIconUrl = await BrowserTestUtils.waitForCondition(
() => searchModeSwitcherButton.style.listStyleImage.match(regex),
"Waiting for the search mode switcher icon to update after exiting search mode."
);
Assert.equal(
searchModeSwitcherIconUrl[1],
searchGlassIconUrl,
"The search mode switcher should have the search glass icon url since the search service init failed."
);
info("Open search mode switcher");
let popup = await UrlbarTestUtils.openSearchModeSwitcher(newWin);
info("Ensure local search modes are present in popup");
let localSearchModes = ["bookmarks", "history", "tabs"];
for (let searchMode of localSearchModes) {
popup.querySelector(`#search-button-${searchMode}`);
Assert.ok("Local search modes should be present");
}
let localSearchButton = popup.querySelector(
`#search-button-${localSearchModes[0]}`
);
let popupHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
localSearchButton.click();
await popupHidden;
stub.restore();
Services.search.wrappedJSObject.forceInitializationStatusForTests("success");
await BrowserTestUtils.closeWindow(newWin);
});

View file

@ -527,12 +527,6 @@ add_task(async function selected_result_input_field() {
});
add_task(async function selected_result_weather() {
// TODO bug 1925735: Remove this and the
// `eslint-disable-next-line no-unreachable` lines below
Assert.ok(true, "Skipping weather task: see bug 1925735");
return;
// eslint-disable-next-line no-unreachable
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.quickactions.enabled", false]],
});
@ -542,7 +536,7 @@ add_task(async function selected_result_weather() {
let provider = "UrlbarProviderQuickSuggest";
await doTest(async () => {
await openPopup(MerinoTestUtils.WEATHER_KEYWORD);
await openPopup("weather");
await selectRowByProvider(provider);
await doEnter();
@ -556,7 +550,6 @@ add_task(async function selected_result_weather() {
]);
});
// eslint-disable-next-line no-unreachable
await cleanupQuickSuggest();
await SpecialPowers.popPrefEnv();
});

View file

@ -108,10 +108,7 @@ async function ensureQuickSuggestInit({ ...args } = {}) {
}),
],
},
{
type: "weather",
weather: MerinoTestUtils.WEATHER_RS_DATA,
},
lazy.QuickSuggestTestUtils.weatherRecord(),
{
type: "exposure-suggestions",
suggestion_type: "aaa",

View file

@ -53,14 +53,6 @@ const RESPONSE_HISTOGRAM_VALUES = {
no_suggestion: 4,
};
const WEATHER_KEYWORD = "weather";
const WEATHER_RS_DATA = {
keywords: [WEATHER_KEYWORD],
min_keyword_length: 3,
score: "0.29",
};
const WEATHER_SUGGESTION = {
title: "Weather for San Francisco",
url: "https://example.com/weather",
@ -172,24 +164,6 @@ class _MerinoTestUtils {
return { ...GEOLOCATION_DATA.custom_details.geolocation };
}
/**
* @returns {string}
* The weather keyword in `WEATHER_RS_DATA`. Can be used as a search string
* to match the weather suggestion.
*/
get WEATHER_KEYWORD() {
return WEATHER_KEYWORD;
}
/**
* @returns {object}
* Default remote settings data that sets up `WEATHER_KEYWORD` as the
* keyword for the weather suggestion.
*/
get WEATHER_RS_DATA() {
return { ...WEATHER_RS_DATA };
}
/**
* @returns {object}
* A mock weather suggestion.
@ -459,6 +433,22 @@ class MockMerinoServer {
}
set response(value) {
this.#response = value;
this.#requestHandler = null;
}
/**
* If you need more control over responses than is allowed by setting
* `server.response`, you can use this to register a callback that will be
* called on each request. To unregister the callback, pass null or set
* `server.response`.
*
* @param {Function | null} callback
* This function will be called on each request and passed the
* `nsIHttpRequest`. It should return a response object as described by the
* `server.response` jsdoc.
*/
set requestHandler(callback) {
this.#requestHandler = callback;
}
/**
@ -705,7 +695,7 @@ class MockMerinoServer {
// Now set up and finish the response.
httpResponse.processAsync();
let { response } = this;
let response = this.#requestHandler?.(httpRequest) || this.response;
let finishResponse = () => {
let status = response.status || 200;
@ -784,6 +774,7 @@ class MockMerinoServer {
#url = null;
#baseURL = null;
#response = null;
#requestHandler = null;
#requests = [];
#nextRequestDeferred = null;
#nextDelayedResponseID = 0;

View file

@ -652,6 +652,111 @@ class _QuickSuggestTestUtils {
};
}
/**
* Returns a remote settings weather record.
*
* @returns {object}
* A weather record for storing in remote settings.
*/
weatherRecord({
keywords = ["weather"],
min_keyword_length = undefined,
score = 0.29,
} = {}) {
let [maxLen, maxWordCount] = keywords.reduce(
([len, wordCount], kw) => [
Math.max(len, kw.length),
Math.max(wordCount, kw.split(/\s+/).filter(s => !!s).length),
],
[0, 0]
);
return {
type: "weather",
attachment: {
keywords,
min_keyword_length,
score,
max_keyword_length: maxLen,
max_keyword_word_count: maxWordCount,
},
};
}
/**
* Returns a remote settings geonames record populated with some cities.
*
* @returns {object}
* A geonames record for storing in remote settings.
*/
geonamesRecord() {
let geonames = [
// Waterloo, AL
{
id: 1,
name: "Waterloo",
feature_class: "P",
feature_code: "PPL",
country_code: "US",
admin1_code: "AL",
population: 200,
alternate_names: ["waterloo"],
},
// AL
{
id: 2,
name: "Alabama",
feature_class: "A",
feature_code: "ADM1",
country_code: "US",
admin1_code: "AL",
population: 4530315,
alternate_names: ["al", "alabama"],
},
// Waterloo, IA
{
id: 3,
name: "Waterloo",
feature_class: "P",
feature_code: "PPLA2",
country_code: "US",
admin1_code: "IA",
population: 68460,
alternate_names: ["waterloo"],
},
// IA
{
id: 4,
name: "Iowa",
feature_class: "A",
feature_code: "ADM1",
country_code: "US",
admin1_code: "IA",
population: 2955010,
alternate_names: ["ia", "iowa"],
},
];
let [maxLen, maxWordCount] = geonames.reduce(
([len, wordCount], geoname) => [
Math.max(len, ...geoname.alternate_names.map(n => n.length)),
Math.max(
wordCount,
...geoname.alternate_names.map(
n => n.split(/\s+/).filter(s => !!s).length
)
),
],
[0, 0]
);
return {
type: "geonames",
attachment: {
geonames,
max_alternate_name_length: maxLen,
max_alternate_name_word_count: maxWordCount,
},
};
}
/**
* Returns an expected AMO (addons) result that can be passed to
* `check_results()` in xpcshell tests regardless of whether the Rust backend
@ -767,6 +872,7 @@ class _QuickSuggestTestUtils {
weatherResult({
source,
provider,
city = null,
telemetryType = undefined,
temperatureUnit = undefined,
} = {}) {
@ -788,7 +894,7 @@ class _QuickSuggestTestUtils {
source: "merino",
provider: "accuweather",
dynamicType: "weather",
city: lazy.MerinoTestUtils.WEATHER_SUGGESTION.city_name,
city: city || lazy.MerinoTestUtils.WEATHER_SUGGESTION.city_name,
temperature:
lazy.MerinoTestUtils.WEATHER_SUGGESTION.current_conditions
.temperature[temperatureUnit],

View file

@ -54,4 +54,3 @@ tags = "search-telemetry"
tags = "search-telemetry"
["browser_weather.js"]
skip-if = ["true"] # Bug 1925735

View file

@ -12,12 +12,7 @@ requestLongerTimeout(5);
add_setup(async function () {
await QuickSuggestTestUtils.ensureQuickSuggestInit({
remoteSettingsRecords: [
{
type: "weather",
weather: MerinoTestUtils.WEATHER_RS_DATA,
},
],
remoteSettingsRecords: [QuickSuggestTestUtils.weatherRecord()],
prefs: [["weather.featureGate", true]],
});
await MerinoTestUtils.initWeather();
@ -27,7 +22,7 @@ add_setup(async function () {
add_task(async function dom() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: MerinoTestUtils.WEATHER_KEYWORD,
value: "weather",
});
let resultIndex = 1;
@ -56,7 +51,7 @@ add_task(async function test_weather_result_selection() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: MerinoTestUtils.WEATHER_KEYWORD,
value: "weather",
});
info(`Select the weather result`);
@ -82,13 +77,9 @@ add_task(async function test_weather_result_selection() {
add_task(async function showLessFrequentlyCapReached_manySearches() {
// Set up a min keyword length and cap.
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "weather",
weather: {
keywords: ["weather"],
min_keyword_length: 3,
},
},
QuickSuggestTestUtils.weatherRecord({
min_keyword_length: 3,
}),
{
type: "configuration",
configuration: {
@ -165,10 +156,7 @@ add_task(async function showLessFrequentlyCapReached_manySearches() {
await UrlbarTestUtils.promisePopupClose(window);
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "weather",
weather: MerinoTestUtils.WEATHER_RS_DATA,
},
QuickSuggestTestUtils.weatherRecord(),
]);
UrlbarPrefs.clear("weather.minKeywordLength");
});
@ -178,13 +166,9 @@ add_task(async function showLessFrequentlyCapReached_manySearches() {
add_task(async function showLessFrequentlyCapReached_oneSearch() {
// Set up a min keyword length and cap.
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "weather",
weather: {
keywords: ["weather"],
min_keyword_length: 3,
},
},
QuickSuggestTestUtils.weatherRecord({
min_keyword_length: 3,
}),
{
type: "configuration",
configuration: {
@ -240,10 +224,7 @@ add_task(async function showLessFrequentlyCapReached_oneSearch() {
gURLBar.view.resultMenu.hidePopup(true);
await UrlbarTestUtils.promisePopupClose(window);
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "weather",
weather: MerinoTestUtils.WEATHER_RS_DATA,
},
QuickSuggestTestUtils.weatherRecord(),
]);
UrlbarPrefs.clear("weather.minKeywordLength");
});
@ -252,7 +233,7 @@ add_task(async function showLessFrequentlyCapReached_oneSearch() {
add_task(async function notInterested() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: MerinoTestUtils.WEATHER_KEYWORD,
value: "weather",
});
await doDismissTest("not_interested");
});
@ -261,7 +242,7 @@ add_task(async function notInterested() {
add_task(async function notRelevant() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: MerinoTestUtils.WEATHER_KEYWORD,
value: "weather",
});
await doDismissTest("not_relevant");
});
@ -366,7 +347,7 @@ async function doSessionOngoingCommandTest(command) {
// Trigger the suggestion.
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: MerinoTestUtils.WEATHER_KEYWORD,
value: "weather",
});
let resultIndex = 1;
@ -397,7 +378,7 @@ add_task(async function manage() {
await BrowserTestUtils.withNewTab({ gBrowser }, async browser => {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: MerinoTestUtils.WEATHER_KEYWORD,
value: "weather",
});
let resultIndex = 1;
@ -449,7 +430,7 @@ add_task(async function simpleUI() {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: MerinoTestUtils.WEATHER_KEYWORD,
value: "weather",
});
let resultIndex = 1;

View file

@ -32,6 +32,16 @@ function makeTestSuggestions() {
];
}
function makeTestSuggestionsWithInvalidCategories() {
return [
{
title: "suggestion",
categories: [-1], // "Education"
score: 0.2,
},
];
}
const MERINO_SUGGESTIONS = [
{
provider: "adm",
@ -75,6 +85,12 @@ const EXPECTED_WIKIPEDIA_RESULT =
let gSandbox;
add_setup(async () => {
// FOG needs a profile directory to put its data in.
do_get_profile();
// FOG needs to be initialized in order for data to flow.
Services.fog.initializeFOG();
await QuickSuggestTestUtils.ensureQuickSuggestInit({
merinoSuggestions: MERINO_SUGGESTIONS,
prefs: [
@ -139,7 +155,7 @@ add_task(async function test_interest_mode() {
Assert.less(
suggestions[1].score,
0.2,
"The score should not be lowered for irrelevant suggestion"
"The score should be lowered for irrelevant suggestion"
);
Services.prefs.clearUserPref(PREF_RANKING_MODE);
@ -209,3 +225,48 @@ add_task(async function test_interest_mode_end2end() {
Services.prefs.clearUserPref(PREF_RANKING_MODE);
});
add_task(async function test_telemetry_interest_mode() {
Services.prefs.setStringPref(PREF_RANKING_MODE, "interest");
Services.fog.testResetFOG();
Assert.equal(null, Glean.suggestRelevance.status.success.testGetValue());
Assert.equal(null, Glean.suggestRelevance.status.failure.testGetValue());
Assert.equal(null, Glean.suggestRelevance.outcome.boosted.testGetValue());
Assert.equal(null, Glean.suggestRelevance.outcome.decreased.testGetValue());
const suggestions = makeTestSuggestions();
await UrlbarProviderQuickSuggest._test_applyRanking(suggestions);
// The scoring should succeed for both suggestions with one boosted score
// and one decreased score.
Assert.equal(2, Glean.suggestRelevance.status.success.testGetValue());
Assert.equal(null, Glean.suggestRelevance.status.failure.testGetValue());
Assert.equal(1, Glean.suggestRelevance.outcome.boosted.testGetValue());
Assert.equal(1, Glean.suggestRelevance.outcome.decreased.testGetValue());
Services.prefs.clearUserPref(PREF_RANKING_MODE);
});
add_task(async function test_telemetry_interest_mode_with_failures() {
Services.prefs.setStringPref(PREF_RANKING_MODE, "interest");
Services.fog.testResetFOG();
Assert.equal(null, Glean.suggestRelevance.status.success.testGetValue());
Assert.equal(null, Glean.suggestRelevance.status.failure.testGetValue());
Assert.equal(null, Glean.suggestRelevance.outcome.boosted.testGetValue());
Assert.equal(null, Glean.suggestRelevance.outcome.decreased.testGetValue());
const suggestions = makeTestSuggestionsWithInvalidCategories();
await UrlbarProviderQuickSuggest._test_applyRanking(suggestions);
// The scoring should fail.
Assert.equal(null, Glean.suggestRelevance.status.success.testGetValue());
Assert.equal(1, Glean.suggestRelevance.status.failure.testGetValue());
Assert.equal(null, Glean.suggestRelevance.outcome.boosted.testGetValue());
Assert.equal(null, Glean.suggestRelevance.outcome.decreased.testGetValue());
Services.prefs.clearUserPref(PREF_RANKING_MODE);
});

View file

@ -9,16 +9,17 @@
const HISTOGRAM_LATENCY = "FX_URLBAR_MERINO_LATENCY_WEATHER_MS";
const HISTOGRAM_RESPONSE = "FX_URLBAR_MERINO_RESPONSE_WEATHER";
const { WEATHER_RS_DATA, WEATHER_SUGGESTION } = MerinoTestUtils;
const { WEATHER_SUGGESTION } = MerinoTestUtils;
add_setup(async () => {
await QuickSuggestTestUtils.ensureQuickSuggestInit({
prefs: [["suggest.quicksuggest.nonsponsored", true]],
prefs: [
["suggest.quicksuggest.nonsponsored", true],
["weather.featureGate", true],
],
remoteSettingsRecords: [
{
type: "weather",
weather: WEATHER_RS_DATA,
},
QuickSuggestTestUtils.weatherRecord(),
QuickSuggestTestUtils.geonamesRecord(),
],
});
@ -47,7 +48,7 @@ async function doBasicDisableAndEnableTest(pref) {
});
// No suggestion should be returned for a search.
let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
let context = createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
@ -66,7 +67,7 @@ async function doBasicDisableAndEnableTest(pref) {
UrlbarPrefs.set(pref, true);
// The suggestion should be returned for a search.
context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
context = createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
@ -93,7 +94,7 @@ add_task(async function noSuggestion() {
let { suggestions } = MerinoTestUtils.server.response.body;
MerinoTestUtils.server.response.body.suggestions = [];
let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
let context = createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
@ -127,7 +128,7 @@ add_task(async function networkError() {
});
await MerinoTestUtils.server.withNetworkError(async () => {
let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
let context = createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
@ -154,7 +155,7 @@ add_task(async function httpError() {
MerinoTestUtils.server.response = { status: 500 };
let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
let context = createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
@ -192,7 +193,7 @@ add_task(async function clientTimeout() {
// response.
let responsePromise = QuickSuggest.weather._test_merino.waitForNextResponse();
let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
let context = createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
@ -325,7 +326,7 @@ async function doLocaleTest({ shouldRunTask, osUnit, unitsByLocale }) {
callback: async () => {
info("Checking locale: " + locale);
await check_results({
context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
context: createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
@ -337,7 +338,7 @@ async function doLocaleTest({ shouldRunTask, osUnit, unitsByLocale }) {
);
Services.prefs.setBoolPref("intl.regional_prefs.use_os_locales", true);
await check_results({
context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
context: createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
@ -360,7 +361,7 @@ add_task(async function block() {
);
// Do a search so we can get an actual result.
let context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
let context = createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
@ -394,7 +395,7 @@ add_task(async function block() {
);
// Do a second search. Nothing should be returned.
context = createContext(MerinoTestUtils.WEATHER_KEYWORD, {
context = createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
@ -417,7 +418,7 @@ add_task(async function nimbusOverride() {
// Verify a search works as expected with the default remote settings weather
// record (which was added in the init task).
await check_results({
context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
context: createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
@ -431,7 +432,7 @@ add_task(async function nimbusOverride() {
// The suggestion shouldn't be returned anymore.
await check_results({
context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
context: createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
@ -443,7 +444,7 @@ add_task(async function nimbusOverride() {
// The suggestion should be returned again.
await check_results({
context: createContext(MerinoTestUtils.WEATHER_KEYWORD, {
context: createContext("weather", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
@ -451,6 +452,113 @@ add_task(async function nimbusOverride() {
});
});
// Does a query that contains a city but no region.
//
// Note that the Rust component handles city/region parsing and has extensive
// tests for that. Here we only need to make sure that Merino is called with the
// city/region parsed by Rust and that the urlbar result has the correct city.
add_task(async function cityWithoutRegion() {
// "waterloo" matches both Waterloo, IA and Waterloo, AL. The Rust component
// will return an array containing suggestions for both, and the suggestions
// will have the same score. We should make a urlbar result for the first
// suggestion in the array, which will be Waterloo, IA since it has a larger
// population.
await doCityTest({
query: "waterloo",
city: "Waterloo",
region: "IA",
});
});
// Does a query that contains a city and a region.
add_task(async function cityWithRegion() {
await doCityTest({
query: "waterloo al",
city: "Waterloo",
region: "AL",
});
});
// When the query doesn't have a city, no location params should be passed to
// Merino.
add_task(async function noCity() {
await doCityTest({
query: "weather",
city: null,
region: null,
country: null,
expectedResultCity: WEATHER_SUGGESTION.city_name,
});
});
async function doCityTest({
query,
city,
region,
country = "US",
expectedResultCity = city,
}) {
let expectedParams = {
q: "",
city,
region,
country,
};
let merinoCallCount = 0;
MerinoTestUtils.server.requestHandler = req => {
merinoCallCount++;
// If this fails, the Rust component returned multiple suggestions, which is
// fine and expected when a query matches multiple cities, but we should
// only ever make a urlbar result for the first one.
Assert.equal(merinoCallCount, 1, "Merino should be called only once");
let params = new URLSearchParams(req.queryString);
for (let [key, value] of Object.entries(expectedParams)) {
Assert.strictEqual(
params.get(key),
value,
"Param should be correct: " + key
);
}
let suggestion = { ...WEATHER_SUGGESTION };
if (city) {
suggestion = {
...suggestion,
title: "Weather for " + city,
city_name: city,
};
}
return {
body: {
request_id: "request_id",
suggestions: [suggestion],
},
};
};
let context = createContext(query, {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
await check_results({
context,
matches: [
QuickSuggestTestUtils.weatherResult({ city: expectedResultCity }),
],
});
Assert.equal(
merinoCallCount,
1,
"Merino should have beeen called exactly once"
);
MerinoTestUtils.server.requestHandler = null;
}
function assertDisabled({ message }) {
info("Asserting feature is disabled");
if (message) {

View file

@ -6,17 +6,13 @@
"use strict";
const { WEATHER_RS_DATA } = MerinoTestUtils;
add_setup(async () => {
await QuickSuggestTestUtils.ensureQuickSuggestInit({
remoteSettingsRecords: [
{
type: "weather",
weather: WEATHER_RS_DATA,
},
remoteSettingsRecords: [QuickSuggestTestUtils.weatherRecord()],
prefs: [
["suggest.quicksuggest.nonsponsored", true],
["weather.featureGate", true],
],
prefs: [["suggest.quicksuggest.nonsponsored", true]],
});
await MerinoTestUtils.initWeather();
});
@ -41,27 +37,6 @@ add_task(async function () {
});
});
// * Settings data: empty
// * Nimbus values: none
// * Min keyword length pref: none
// * Expected: no suggestion
add_task(async function () {
await doKeywordsTest({
desc: "Empty settings",
settingsData: {},
tests: {
"": false,
w: false,
we: false,
wea: false,
weat: false,
weath: false,
weathe: false,
weather: false,
},
});
});
// * Settings data: keywords and min keyword length > 0
// * Nimbus values: none
// * Min keyword length pref: none
@ -89,7 +64,8 @@ add_task(async function () {
// * Settings data: keywords and min keyword length = 0
// * Nimbus values: none
// * Min keyword length pref: 6
// * Expected: use settings keywords and min keyword length pref
// * Expected: no prefix matching because when min keyword length = 0, the Rust
// component requires full keywords to be typed
add_task(async function () {
await doKeywordsTest({
desc: "Settings only, min keyword length = 0, pref exists",
@ -105,7 +81,7 @@ add_task(async function () {
wea: false,
weat: false,
weath: false,
weathe: true,
weathe: false,
weather: true,
},
});
@ -136,28 +112,6 @@ add_task(async function () {
});
});
// * Settings data: empty
// * Nimbus values: empty
// * Min keyword length pref: none
// * Expected: no suggestion
add_task(async function () {
await doKeywordsTest({
desc: "Settings: empty; Nimbus: empty",
settingsData: {},
nimbusValues: {},
tests: {
"": false,
w: false,
we: false,
wea: false,
weat: false,
weath: false,
weathe: false,
weather: false,
},
});
});
// * Settings data: keywords and min keyword length > 0
// * Nimbus values: min keyword length = 0
// * Min keyword length pref: none
@ -324,12 +278,11 @@ async function doKeywordsTest({
nimbusCleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues);
}
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "weather",
weather: settingsData,
},
]);
let records = [];
if (settingsData) {
records.push(QuickSuggestTestUtils.weatherRecord(settingsData));
}
await QuickSuggestTestUtils.setRemoteSettingsRecords(records);
if (minKeywordLength) {
UrlbarPrefs.set("weather.minKeywordLength", minKeywordLength);
@ -358,10 +311,7 @@ async function doKeywordsTest({
await nimbusCleanup?.();
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "weather",
weather: MerinoTestUtils.WEATHER_RS_DATA,
},
QuickSuggestTestUtils.weatherRecord(),
]);
UrlbarPrefs.clear("weather.minKeywordLength");
@ -521,15 +471,18 @@ add_task(async function () {
nimbusValues: {
weatherKeywordsMinimumLength: 3,
},
// The Rust component will use the min keyword length in the RS config. The
// Nimbus value will be ignored.
tests: [
{
minKeywordLength: 3,
canIncrement: true,
searches: {
we: false,
wea: true,
weat: true,
wea: false,
weat: false,
weath: true,
weathe: true,
},
},
{
@ -538,8 +491,9 @@ add_task(async function () {
searches: {
we: false,
wea: false,
weat: true,
weat: false,
weath: true,
weathe: true,
},
},
{
@ -550,6 +504,18 @@ add_task(async function () {
wea: false,
weat: false,
weath: true,
weathe: true,
},
},
{
minKeywordLength: 6,
canIncrement: true,
searches: {
we: false,
wea: false,
weat: false,
weath: false,
weathe: true,
},
},
],
@ -567,15 +533,18 @@ add_task(async function () {
weatherKeywordsMinimumLength: 3,
weatherKeywordsMinimumLengthCap: 6,
},
// The Rust component will use the min keyword length in the RS config. The
// Nimbus value will be ignored.
tests: [
{
minKeywordLength: 3,
canIncrement: true,
searches: {
we: false,
wea: true,
weat: true,
wea: false,
weat: false,
weath: true,
weathe: true,
},
},
{
@ -584,8 +553,9 @@ add_task(async function () {
searches: {
we: false,
wea: false,
weat: true,
weat: false,
weath: true,
weathe: true,
},
},
{
@ -596,6 +566,7 @@ add_task(async function () {
wea: false,
weat: false,
weath: true,
weathe: true,
},
},
{
@ -639,16 +610,14 @@ async function doIncrementTest({
nimbusCleanup = await UrlbarTestUtils.initNimbusFeature(nimbusValues);
}
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "weather",
weather,
},
{
let records = [QuickSuggestTestUtils.weatherRecord(weather)];
if (configuration) {
records.push({
type: "configuration",
configuration,
},
]);
});
}
await QuickSuggestTestUtils.setRemoteSettingsRecords(records);
let expectedResult = QuickSuggestTestUtils.weatherResult();
@ -692,10 +661,7 @@ async function doIncrementTest({
await nimbusCleanup?.();
await QuickSuggestTestUtils.setRemoteSettingsRecords([
{
type: "weather",
weather: MerinoTestUtils.WEATHER_RS_DATA,
},
QuickSuggestTestUtils.weatherRecord(),
]);
UrlbarPrefs.clear("weather.minKeywordLength");
}

View file

@ -55,8 +55,5 @@ skip-if = ["true"] # Bug 1880214
["test_suggestionsMap.js"]
["test_weather.js"]
skip-if = ["true"] # Bug 1925735
["test_weather_keywords.js"]
# skip-if = ["verify"] # Bug 1880214 - Takes a very long time due to add_tasks_with_rust()
skip-if = ["true"] # Bug 1925735

View file

@ -401,12 +401,6 @@ bin/libfreebl_64int_3.so
#endif
#endif
; [ minidump-analyzer ]
;
#ifdef MOZ_CRASHREPORTER
@BINPATH@/minidump-analyzer@BIN_SUFFIX@
#endif
; [ Ping Sender ]
;
@BINPATH@/pingsender@BIN_SUFFIX@

View file

@ -1579,7 +1579,6 @@ ${RemoveDefaultBrowserAgentShortcut}
Push "xpcom.dll"
Push "crashreporter.exe"
Push "default-browser-agent.exe"
Push "minidump-analyzer.exe"
Push "nmhproxy.exe"
Push "pingsender.exe"
Push "updater.exe"

View file

@ -642,9 +642,12 @@ urlbar-result-action-calculator-result = = { $result }
# Searchmode Switcher button
# Variables:
# $engine (String): the current default search engine.
urlbar-searchmode-button =
.label = { $engine }, Pick a Search Engine
.tooltiptext = { $engine }, Pick a Search Engine
urlbar-searchmode-button2 =
.label = { $engine }, pick a search engine
.tooltiptext = { $engine }, pick a search engine
urlbar-searchmode-button-no-engine =
.label = No shortcut selected, pick a shortcut
.tooltiptext = No shortcut selected, pick a shortcut
urlbar-searchmode-dropmarker =
.tooltiptext = Pick a Search Engine
urlbar-searchmode-bookmarks =
@ -1026,6 +1029,10 @@ data-reporting-notification-button =
# Label for the indicator shown in the private browsing window titlebar.
private-browsing-indicator-label = Private browsing
# Tooltip for the indicator shown in the private browsing window titlebar.
private-browsing-indicator-tooltip =
.tooltiptext = Private browsing
# Tooltip for the indicator shown in the window titlebar when content analysis is active.
# Variables:
# $agentName (String): The name of the DLP agent that is connected

View file

@ -270,7 +270,6 @@ firefoxview-choose-browser-button = Choose browser
firefoxview-dont-remember-history-empty-header-2 = Youre in control of what { -brand-short-name } remembers
firefoxview-dont-remember-history-empty-description-one = Right now, { -brand-short-name } does not remember your browsing activity. To change that, <a data-l10n-name="history-settings-url-two">update your history settings</a>.
firefoxview-dont-remember-history-empty-description-two = Based on your current settings, { -brand-short-name } doesnt remember your activity as you browse. To change that, <a data-l10n-name="history-settings-url-two">change your history settings to remember your history</a>.
##

View file

@ -116,6 +116,7 @@ newtab-menu-save-to-pocket = Save to { -pocket-brand-name }
newtab-menu-delete-pocket = Delete from { -pocket-brand-name }
newtab-menu-archive-pocket = Archive in { -pocket-brand-name }
newtab-menu-show-privacy-info = Our sponsors & your privacy
newtab-menu-about-fakespot = About { -fakespot-brand-name }
## Message displayed in a modal window to explain privacy and provide context for sponsored content.

View file

@ -16,7 +16,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"af": {
"pin": false,
@ -35,7 +35,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"an": {
"pin": false,
@ -54,7 +54,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ar": {
"pin": false,
@ -73,7 +73,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ast": {
"pin": false,
@ -92,7 +92,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"az": {
"pin": false,
@ -111,7 +111,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"be": {
"pin": false,
@ -130,7 +130,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"bg": {
"pin": false,
@ -149,7 +149,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"bn": {
"pin": false,
@ -168,7 +168,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"bo": {
"pin": false,
@ -187,7 +187,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"br": {
"pin": false,
@ -206,7 +206,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"brx": {
"pin": false,
@ -225,7 +225,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"bs": {
"pin": false,
@ -244,7 +244,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ca": {
"pin": false,
@ -263,7 +263,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ca-valencia": {
"pin": false,
@ -282,7 +282,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"cak": {
"pin": false,
@ -301,7 +301,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ckb": {
"pin": false,
@ -320,7 +320,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"cs": {
"pin": false,
@ -339,7 +339,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"cy": {
"pin": false,
@ -358,7 +358,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"da": {
"pin": false,
@ -377,7 +377,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"de": {
"pin": false,
@ -396,7 +396,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"dsb": {
"pin": false,
@ -415,7 +415,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"el": {
"pin": false,
@ -434,7 +434,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"en-CA": {
"pin": false,
@ -453,7 +453,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"en-GB": {
"pin": false,
@ -472,7 +472,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"eo": {
"pin": false,
@ -491,7 +491,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"es-AR": {
"pin": false,
@ -510,7 +510,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"es-CL": {
"pin": false,
@ -529,7 +529,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"es-ES": {
"pin": false,
@ -548,7 +548,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"es-MX": {
"pin": false,
@ -567,7 +567,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"et": {
"pin": false,
@ -586,7 +586,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"eu": {
"pin": false,
@ -605,7 +605,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"fa": {
"pin": false,
@ -624,7 +624,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ff": {
"pin": false,
@ -643,7 +643,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"fi": {
"pin": false,
@ -662,7 +662,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"fr": {
"pin": false,
@ -681,7 +681,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"fur": {
"pin": false,
@ -700,7 +700,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"fy-NL": {
"pin": false,
@ -719,7 +719,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ga-IE": {
"pin": false,
@ -738,7 +738,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"gd": {
"pin": false,
@ -757,7 +757,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"gl": {
"pin": false,
@ -776,7 +776,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"gn": {
"pin": false,
@ -795,7 +795,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"gu-IN": {
"pin": false,
@ -814,7 +814,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"he": {
"pin": false,
@ -833,7 +833,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"hi-IN": {
"pin": false,
@ -852,7 +852,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"hr": {
"pin": false,
@ -871,7 +871,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"hsb": {
"pin": false,
@ -890,7 +890,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"hu": {
"pin": false,
@ -909,7 +909,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"hy-AM": {
"pin": false,
@ -928,7 +928,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"hye": {
"pin": false,
@ -947,7 +947,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ia": {
"pin": false,
@ -966,7 +966,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"id": {
"pin": false,
@ -985,7 +985,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"is": {
"pin": false,
@ -1004,7 +1004,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"it": {
"pin": false,
@ -1023,7 +1023,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ja": {
"pin": false,
@ -1040,7 +1040,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ja-JP-mac": {
"pin": false,
@ -1048,7 +1048,7 @@
"macosx64",
"macosx64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ka": {
"pin": false,
@ -1067,7 +1067,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"kab": {
"pin": false,
@ -1086,7 +1086,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"kk": {
"pin": false,
@ -1105,7 +1105,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"km": {
"pin": false,
@ -1124,7 +1124,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"kn": {
"pin": false,
@ -1143,7 +1143,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ko": {
"pin": false,
@ -1162,7 +1162,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"lij": {
"pin": false,
@ -1181,7 +1181,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"lo": {
"pin": false,
@ -1200,7 +1200,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"lt": {
"pin": false,
@ -1219,7 +1219,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ltg": {
"pin": false,
@ -1238,7 +1238,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"lv": {
"pin": false,
@ -1257,7 +1257,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"meh": {
"pin": false,
@ -1276,7 +1276,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"mk": {
"pin": false,
@ -1295,7 +1295,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"mr": {
"pin": false,
@ -1314,7 +1314,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ms": {
"pin": false,
@ -1333,7 +1333,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"my": {
"pin": false,
@ -1352,7 +1352,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"nb-NO": {
"pin": false,
@ -1371,7 +1371,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ne-NP": {
"pin": false,
@ -1390,7 +1390,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"nl": {
"pin": false,
@ -1409,7 +1409,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"nn-NO": {
"pin": false,
@ -1428,7 +1428,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"oc": {
"pin": false,
@ -1447,7 +1447,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"pa-IN": {
"pin": false,
@ -1466,7 +1466,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"pl": {
"pin": false,
@ -1485,7 +1485,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"pt-BR": {
"pin": false,
@ -1504,7 +1504,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"pt-PT": {
"pin": false,
@ -1523,7 +1523,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"rm": {
"pin": false,
@ -1542,7 +1542,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ro": {
"pin": false,
@ -1561,7 +1561,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ru": {
"pin": false,
@ -1580,7 +1580,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"sat": {
"pin": false,
@ -1599,7 +1599,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"sc": {
"pin": false,
@ -1618,7 +1618,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"scn": {
"pin": false,
@ -1637,7 +1637,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"sco": {
"pin": false,
@ -1656,7 +1656,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"si": {
"pin": false,
@ -1675,7 +1675,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"sk": {
"pin": false,
@ -1694,7 +1694,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"skr": {
"pin": false,
@ -1713,7 +1713,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"sl": {
"pin": false,
@ -1732,7 +1732,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"son": {
"pin": false,
@ -1751,7 +1751,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"sq": {
"pin": false,
@ -1770,7 +1770,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"sr": {
"pin": false,
@ -1789,7 +1789,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"sv-SE": {
"pin": false,
@ -1808,7 +1808,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"szl": {
"pin": false,
@ -1827,7 +1827,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ta": {
"pin": false,
@ -1846,7 +1846,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"te": {
"pin": false,
@ -1865,7 +1865,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"tg": {
"pin": false,
@ -1884,7 +1884,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"th": {
"pin": false,
@ -1903,7 +1903,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"tl": {
"pin": false,
@ -1922,7 +1922,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"tr": {
"pin": false,
@ -1941,7 +1941,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"trs": {
"pin": false,
@ -1960,7 +1960,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"uk": {
"pin": false,
@ -1979,7 +1979,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"ur": {
"pin": false,
@ -1998,7 +1998,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"uz": {
"pin": false,
@ -2017,7 +2017,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"vi": {
"pin": false,
@ -2036,7 +2036,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"wo": {
"pin": false,
@ -2055,7 +2055,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"xh": {
"pin": false,
@ -2074,7 +2074,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"zh-CN": {
"pin": false,
@ -2093,7 +2093,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
},
"zh-TW": {
"pin": false,
@ -2112,6 +2112,6 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f5dcc7056c5bbdcb0daca0ea50c5d81fe6671268"
"revision": "8a3709b87e86b667ff8179ef5c5a4f6ad18d539e"
}
}

View file

@ -277,12 +277,13 @@
}
&:hover > .toolbarbutton-icon {
background-color: #d70022;
color: white;
background-color: var(--button-background-color-destructive);
color: var(--button-text-color-destructive);
}
&:hover:active > .toolbarbutton-icon {
background-color: #ff0039;
background-color: var(--button-background-color-destructive-active);
color: var(--button-text-color-destructive-active);
}
@media not (-moz-gtk-csd-close-button) {

View file

@ -487,13 +487,24 @@ menupopup::part(drop-indicator) {
/* Private browsing indicator */
#private-browsing-indicator-with-label {
.private-browsing-indicator-with-label {
align-items: center;
margin-inline: 7px;
:root:not([privatebrowsingmode=temporary]) & {
:root:not([privatebrowsingmode=temporary]) &,
#nav-bar:not([tabs-hidden]) > & {
display: none;
}
#nav-bar[tabs-hidden] > & {
margin-inline-end: 12px;
/* Hide the private browsing indicator
label when vertical tabs are enabled */
> .private-browsing-indicator-label {
display: none;
}
}
}
.private-browsing-indicator-icon {

View file

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="context-fill" fill-opacity="context-fill-opacity">
<path d="M13.1944 3.61165H12.3805C12.9771 2.70165 12.8767 1.46449 12.0772 0.665708C11.191 -0.221903 9.64544 -0.221903 8.75783 0.665708C8.7015 0.722764 8.65022 0.786319 8.60183 0.854931C8.55778 0.794264 8.51083 0.73793 8.45956 0.687375C7.54378 -0.228403 6.056 -0.228403 5.14022 0.687375C4.34722 1.48037 4.24394 2.70382 4.82606 3.61165H3.80556C2.80817 3.61165 2 4.41982 2 5.41721V6.13943C2 6.73237 2.28961 7.25382 2.73089 7.58243L2.72222 7.58387V11.9172C2.72222 12.9146 3.53039 13.7228 4.52778 13.7228H12.4722C13.4696 13.7228 14.2778 12.9146 14.2778 11.9172V7.58387L14.2691 7.58243C14.7104 7.25382 15 6.73237 15 6.13943V5.41721C15 4.41982 14.1918 3.61165 13.1944 3.61165ZM13.3389 4.69499L13.9167 5.27276V6.28387L13.3389 6.86165H9.04167V4.69499H13.3389ZM9.52411 1.43199C10.0029 0.953875 10.8349 0.954597 11.3116 1.43199C11.8049 1.92526 11.8049 2.72621 11.3116 3.21949C11.0148 3.51704 9.97694 3.61671 9.15578 3.59649C9.15867 3.44482 9.15867 3.27582 9.15289 3.09454C9.17744 2.39037 9.28361 1.67249 9.52411 1.43199ZM5.9065 3.24043C5.41322 2.74715 5.41322 1.94621 5.9065 1.45293C6.15278 1.20665 6.47633 1.08387 6.79989 1.08387C7.12344 1.08387 7.447 1.20738 7.69328 1.45365C7.93161 1.69199 8.03778 2.39832 8.06378 3.09599C8.05944 3.28376 8.06017 3.4571 8.06378 3.61165H7.51922C6.8295 3.58493 6.14122 3.47515 5.9065 3.24043ZM3.08333 5.27276L3.66111 4.69499C3.66111 4.69499 7.9215 4.70365 7.95833 4.70293V6.86165H3.66111L3.08333 6.28387V5.27276ZM4.38333 12.6394L3.80556 12.0617V7.94499H7.95833V12.6394H4.38333ZM13.1944 12.0617L12.6167 12.6394H9.04167V7.94499H13.1944V12.0617Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -171,6 +171,7 @@
skin/classic/browser/forward.svg (../shared/icons/forward.svg)
skin/classic/browser/fullscreen.svg (../shared/icons/fullscreen.svg)
skin/classic/browser/fullscreen-exit.svg (../shared/icons/fullscreen-exit.svg)
skin/classic/browser/gift.svg (../shared/icons/gift.svg)
skin/classic/browser/history.svg (../shared/icons/history.svg)
skin/classic/browser/home.svg (../shared/icons/home.svg)
skin/classic/browser/import.svg (../shared/icons/import.svg)

View file

@ -723,6 +723,29 @@
/* Tab Groups */
/*
* .tab-group-line needs to span the drop shadows + space between tabs in the
* same tab group so that the whole tab group appears to be underlined by an
* unbroken line. However, the last tab in a tab group should have its group
* underline stop at the right edge of the tab itself, otherwise it looks
* like the tab group extends too far past the right edge of the tab.
*/
.tab-group-line {
display: none;
background-color: var(--tab-group-color, var(--dragover-tab-group-color));
height: 2px;
margin: 0 calc(-1 * var(--tab-overflow-clip-margin)) calc(-1 * var(--tab-block-margin));
.tabbrowser-tab:last-of-type > .tab-stack > .tab-background > & {
margin-inline-end: 0;
}
tab-group &,
#tabbrowser-tabs[movingtab]:not([movingtab-createGroup]) &:is([selected],[multiselected]) {
display: flex;
}
}
tab-group {
/*
* Let the tab bar flexbox distribute space between all tabs evenly, regardless of
@ -730,23 +753,6 @@ tab-group {
*/
display: contents;
/*
* .tab-group-line needs to span the drop shadows + space between tabs in the
* same tab group so that the whole tab group appears to be underlined by an
* unbroken line. However, the last tab in a tab group should have its group
* underline stop at the right edge of the tab itself, otherwise it looks
* like the tab group extends too far past the right edge of the tab.
*/
.tab-group-line {
background-color: var(--tab-group-color);
height: 2px;
margin: 0 calc(-1 * var(--tab-overflow-clip-margin)) calc(-1 * var(--tab-block-margin));
.tabbrowser-tab:last-of-type > .tab-stack > .tab-background > & {
margin-inline-end: 0;
}
}
&[collapsed] > .tabbrowser-tab {
min-width: 0;
max-width: 0;
@ -851,19 +857,24 @@ tab-group {
opacity: 0;
margin: 0;
position: absolute;
&:checked + .tab-group-editor-swatch {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
}
.tab-group-editor-swatch {
display: block;
width: 16px;
height: 16px;
border-radius: 20%;
border-radius: var(--border-radius-small);
background-color: light-dark(var(--tabgroup-swatch-color), var(--tabgroup-swatch-color-invert));
input:checked + & {
outline: var(--focus-outline);
outline-color: currentColor;
outline-offset: var(--focus-outline-offset);
}
input:focus-visible + & {
outline-color: var(--focus-outline-color);
box-shadow: 0 0 0 calc(var(--focus-outline-width) * 3) color-mix(in srgb, var(--focus-outline-color) 50%, transparent);
}
}
.tab-group-edit-actions,

View file

@ -392,6 +392,13 @@
#identity-icon-box {
max-width: 80px;
}
/* This label expresses the non secure status. However, as the padlock icon is
enough to tell the status to user, hide the label when window gets small.
Except this usage, #identity-icon-label is used to show additional
information that could not tell by only icons. */
#identity-box[pageproxystate="valid"].notSecureText #identity-icon-label {
display: none;
}
/* Contenxtual identity labels are user-customizable and can be very long,
so we only show the colored icon when the window gets small. */
#userContext-label {

View file

@ -167,11 +167,12 @@
}
.titlebar-close:hover {
stroke: white;
background-color: hsl(355,86%,49%);
background-color: var(--button-background-color-destructive);
stroke: var(--button-text-color-destructive);
&:active {
background-color: hsl(355,82%,69%);
background-color: var(--button-background-color-destructive-active);
stroke: var(--button-text-color-destructive-active);
}
}
}

View file

@ -78,6 +78,19 @@ class Breakpoint extends PureComponent {
}
};
onKeyDown = event => {
// Handling only the Enter/Space keys, bail if another key was pressed
if (event.key !== "Enter" && event.key !== " ") {
return;
}
if (event.shiftKey) {
this.onDoubleClick();
return;
}
this.selectBreakpoint(event);
};
selectBreakpoint = event => {
event.preventDefault();
const { selectSpecificLocation } = this.props;
@ -151,6 +164,10 @@ class Breakpoint extends PureComponent {
onClick: this.selectBreakpoint,
onDoubleClick: this.onDoubleClick,
onContextMenu: this.onContextMenu,
onKeyDown: this.onKeyDown,
role: "button",
tabIndex: 0,
title: text,
},
input({
id: breakpoint.id,
@ -167,7 +184,6 @@ class Breakpoint extends PureComponent {
id: labelId,
className: "breakpoint-label cm-s-mozilla devtools-monospace",
onClick: this.selectBreakpoint,
title: text,
},
span({
className: "cm-highlighted",

Some files were not shown because too many files have changed in this diff Show more