forked from mirrors/gecko-dev

It isn't possible to manually test this right now until we set a cap in the remote settings config (bug 1852353), but the automated tests cover it. Depends on D187830 Differential Revision: https://phabricator.services.mozilla.com/D187835
309 lines
9.7 KiB
JavaScript
309 lines
9.7 KiB
JavaScript
/* 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/. */
|
|
|
|
import { BaseFeature } from "resource:///modules/urlbar/private/BaseFeature.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
|
|
QuickSuggestRemoteSettings:
|
|
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
|
|
SuggestionsMap:
|
|
"resource:///modules/urlbar/private/QuickSuggestRemoteSettings.sys.mjs",
|
|
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
|
|
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
|
|
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
|
|
});
|
|
|
|
const RESULT_MENU_COMMAND = {
|
|
HELP: "help",
|
|
NOT_INTERESTED: "not_interested",
|
|
NOT_RELEVANT: "not_relevant",
|
|
SHOW_LESS_FREQUENTLY: "show_less_frequently",
|
|
};
|
|
|
|
/**
|
|
* A feature that manages Pocket suggestions in remote settings.
|
|
*/
|
|
export class PocketSuggestions extends BaseFeature {
|
|
constructor() {
|
|
super();
|
|
this.#lowConfidenceSuggestionsMap = new lazy.SuggestionsMap();
|
|
this.#highConfidenceSuggestionsMap = new lazy.SuggestionsMap();
|
|
}
|
|
|
|
get shouldEnable() {
|
|
return (
|
|
lazy.UrlbarPrefs.get("quickSuggestRemoteSettingsEnabled") &&
|
|
lazy.UrlbarPrefs.get("pocketFeatureGate") &&
|
|
lazy.UrlbarPrefs.get("suggest.pocket") &&
|
|
lazy.UrlbarPrefs.get("suggest.quicksuggest.nonsponsored")
|
|
);
|
|
}
|
|
|
|
get enablingPreferences() {
|
|
return ["suggest.pocket", "suggest.quicksuggest.nonsponsored"];
|
|
}
|
|
|
|
get merinoProvider() {
|
|
return "pocket";
|
|
}
|
|
|
|
get showLessFrequentlyCount() {
|
|
let count = lazy.UrlbarPrefs.get("pocket.showLessFrequentlyCount") || 0;
|
|
return Math.max(count, 0);
|
|
}
|
|
|
|
get canShowLessFrequently() {
|
|
let cap =
|
|
lazy.UrlbarPrefs.get("pocketShowLessFrequentlyCap") ||
|
|
lazy.QuickSuggestRemoteSettings.config.show_less_frequently_cap ||
|
|
0;
|
|
return !cap || this.showLessFrequentlyCount < cap;
|
|
}
|
|
|
|
enable(enabled) {
|
|
if (enabled) {
|
|
lazy.QuickSuggestRemoteSettings.register(this);
|
|
} else {
|
|
lazy.QuickSuggestRemoteSettings.unregister(this);
|
|
this.#lowConfidenceSuggestionsMap.clear();
|
|
this.#highConfidenceSuggestionsMap.clear();
|
|
}
|
|
}
|
|
|
|
async queryRemoteSettings(searchString) {
|
|
// If the search string matches high confidence suggestions, they should be
|
|
// treated as top picks. Otherwise try to match low confidence suggestions.
|
|
let is_top_pick = false;
|
|
let suggestions = this.#highConfidenceSuggestionsMap.get(searchString);
|
|
if (suggestions.length) {
|
|
is_top_pick = true;
|
|
} else {
|
|
suggestions = this.#lowConfidenceSuggestionsMap.get(searchString);
|
|
}
|
|
|
|
let lowerSearchString = searchString.toLocaleLowerCase();
|
|
return suggestions.map(suggestion => {
|
|
// Add `full_keyword` to each matched suggestion. It should be the longest
|
|
// keyword that starts with the user's search string.
|
|
let full_keyword = lowerSearchString;
|
|
let keywords = is_top_pick
|
|
? suggestion.highConfidenceKeywords
|
|
: suggestion.lowConfidenceKeywords;
|
|
for (let keyword of keywords) {
|
|
if (
|
|
keyword.startsWith(lowerSearchString) &&
|
|
full_keyword.length < keyword.length
|
|
) {
|
|
full_keyword = keyword;
|
|
}
|
|
}
|
|
return { ...suggestion, is_top_pick, full_keyword };
|
|
});
|
|
}
|
|
|
|
async onRemoteSettingsSync(rs) {
|
|
let records = await rs.get({ filters: { type: "pocket-suggestions" } });
|
|
if (!this.isEnabled) {
|
|
return;
|
|
}
|
|
|
|
let lowMap = new lazy.SuggestionsMap();
|
|
let highMap = new lazy.SuggestionsMap();
|
|
|
|
this.logger.debug(`Got ${records.length} records`);
|
|
for (let record of records) {
|
|
let { buffer } = await rs.attachments.download(record);
|
|
if (!this.isEnabled) {
|
|
return;
|
|
}
|
|
|
|
let suggestions = JSON.parse(new TextDecoder("utf-8").decode(buffer));
|
|
this.logger.debug(`Adding ${suggestions.length} suggestions`);
|
|
|
|
await lowMap.add(suggestions, {
|
|
keywordsProperty: "lowConfidenceKeywords",
|
|
mapKeyword:
|
|
lazy.SuggestionsMap.MAP_KEYWORD_PREFIXES_STARTING_AT_FIRST_WORD,
|
|
});
|
|
if (!this.isEnabled) {
|
|
return;
|
|
}
|
|
|
|
await highMap.add(suggestions, {
|
|
keywordsProperty: "highConfidenceKeywords",
|
|
});
|
|
if (!this.isEnabled) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.#lowConfidenceSuggestionsMap = lowMap;
|
|
this.#highConfidenceSuggestionsMap = highMap;
|
|
}
|
|
|
|
makeResult(queryContext, suggestion, searchString) {
|
|
if (!this.isEnabled) {
|
|
// The feature is disabled on the client, but Merino may still return
|
|
// suggestions anyway, and we filter them out here.
|
|
return null;
|
|
}
|
|
|
|
// If the user hasn't clicked the "Show less frequently" command, the
|
|
// suggestion can be shown. Otherwise, the suggestion can be shown if the
|
|
// user typed more than one word with at least `showLessFrequentlyCount`
|
|
// characters after the first word, including spaces.
|
|
if (this.showLessFrequentlyCount) {
|
|
let spaceIndex = searchString.search(/\s/);
|
|
if (
|
|
spaceIndex < 0 ||
|
|
searchString.length - spaceIndex < this.showLessFrequentlyCount
|
|
) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
let isBestMatch =
|
|
suggestion.is_top_pick &&
|
|
lazy.UrlbarPrefs.get("bestMatchEnabled") &&
|
|
lazy.UrlbarPrefs.get("suggest.bestmatch");
|
|
|
|
let url = new URL(suggestion.url);
|
|
url.searchParams.set("utm_medium", "firefox-desktop");
|
|
url.searchParams.set("utm_source", "firefox-suggest");
|
|
url.searchParams.set(
|
|
"utm_campaign",
|
|
"pocket-collections-in-the-address-bar"
|
|
);
|
|
url.searchParams.set("utm_content", "treatment");
|
|
|
|
return Object.assign(
|
|
new lazy.UrlbarResult(
|
|
lazy.UrlbarUtils.RESULT_TYPE.URL,
|
|
lazy.UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
|
|
...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
|
|
url: url.href,
|
|
originalUrl: suggestion.url,
|
|
title: [suggestion.title, lazy.UrlbarUtils.HIGHLIGHT.TYPED],
|
|
description: isBestMatch ? suggestion.description : "",
|
|
// Use the favicon for non-best matches so the icon exactly matches
|
|
// the Pocket favicon in the user's history and tabs.
|
|
icon: isBestMatch
|
|
? "chrome://global/skin/icons/pocket.svg"
|
|
: "chrome://global/skin/icons/pocket-favicon.ico",
|
|
shouldShowUrl: true,
|
|
bottomTextL10n: {
|
|
id: "firefox-suggest-pocket-bottom-text",
|
|
args: {
|
|
keywordSubstringTyped: searchString,
|
|
keywordSubstringNotTyped: suggestion.full_keyword.substring(
|
|
searchString.length
|
|
),
|
|
},
|
|
},
|
|
helpUrl: lazy.QuickSuggest.HELP_URL,
|
|
})
|
|
),
|
|
{
|
|
isRichSuggestion: true,
|
|
richSuggestionIconSize: isBestMatch ? 24 : 16,
|
|
showFeedbackMenu: true,
|
|
}
|
|
);
|
|
}
|
|
|
|
handleCommand(view, result, selType) {
|
|
switch (selType) {
|
|
case RESULT_MENU_COMMAND.HELP:
|
|
// "help" is handled by UrlbarInput, no need to do anything here.
|
|
break;
|
|
// selType == "dismiss" when the user presses the dismiss key shortcut.
|
|
case "dismiss":
|
|
case RESULT_MENU_COMMAND.NOT_RELEVANT:
|
|
// PocketSuggestions adds the UTM parameters to the original URL and
|
|
// returns it as payload.url in the result. However, as
|
|
// UrlbarProviderQuickSuggest filters suggestions with original URL of
|
|
// provided suggestions, need to use the original URL when adding to the
|
|
// block list.
|
|
lazy.QuickSuggest.blockedSuggestions.add(result.payload.originalUrl);
|
|
result.acknowledgeDismissalL10n = {
|
|
id: "firefox-suggest-dismissal-acknowledgment-one",
|
|
};
|
|
view.controller.removeResult(result);
|
|
break;
|
|
case RESULT_MENU_COMMAND.NOT_INTERESTED:
|
|
lazy.UrlbarPrefs.set("suggest.pocket", false);
|
|
result.acknowledgeDismissalL10n = {
|
|
id: "firefox-suggest-dismissal-acknowledgment-all",
|
|
};
|
|
view.controller.removeResult(result);
|
|
break;
|
|
case RESULT_MENU_COMMAND.SHOW_LESS_FREQUENTLY:
|
|
view.acknowledgeFeedback(result);
|
|
this.incrementShowLessFrequentlyCount();
|
|
if (!this.canShowLessFrequently) {
|
|
view.invalidateResultMenuCommands();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
getResultCommands(result) {
|
|
let commands = [];
|
|
|
|
if (!result.isBestMatch && this.canShowLessFrequently) {
|
|
commands.push({
|
|
name: RESULT_MENU_COMMAND.SHOW_LESS_FREQUENTLY,
|
|
l10n: {
|
|
id: "firefox-suggest-command-show-less-frequently",
|
|
},
|
|
});
|
|
}
|
|
|
|
commands.push(
|
|
{
|
|
l10n: {
|
|
id: "firefox-suggest-command-dont-show-this",
|
|
},
|
|
children: [
|
|
{
|
|
name: RESULT_MENU_COMMAND.NOT_RELEVANT,
|
|
l10n: {
|
|
id: "firefox-suggest-command-not-relevant",
|
|
},
|
|
},
|
|
{
|
|
name: RESULT_MENU_COMMAND.NOT_INTERESTED,
|
|
l10n: {
|
|
id: "firefox-suggest-command-not-interested",
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{ name: "separator" },
|
|
{
|
|
name: RESULT_MENU_COMMAND.HELP,
|
|
l10n: {
|
|
id: "urlbar-result-menu-learn-more-about-firefox-suggest",
|
|
},
|
|
}
|
|
);
|
|
|
|
return commands;
|
|
}
|
|
|
|
incrementShowLessFrequentlyCount() {
|
|
if (this.canShowLessFrequently) {
|
|
lazy.UrlbarPrefs.set(
|
|
"pocket.showLessFrequentlyCount",
|
|
this.showLessFrequentlyCount + 1
|
|
);
|
|
}
|
|
}
|
|
|
|
#lowConfidenceSuggestionsMap;
|
|
#highConfidenceSuggestionsMap;
|
|
}
|