forked from mirrors/gecko-dev
529 lines
16 KiB
JavaScript
529 lines
16 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 {
|
|
UrlbarProvider,
|
|
UrlbarUtils,
|
|
} from "resource:///modules/UrlbarUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
|
|
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
|
|
UrlbarProviderTopSites: "resource:///modules/UrlbarProviderTopSites.sys.mjs",
|
|
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
|
|
UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
|
|
});
|
|
|
|
const WEATHER_PROVIDER_DISPLAY_NAME = "AccuWeather";
|
|
|
|
const TELEMETRY_PREFIX = "contextual.services.quicksuggest";
|
|
|
|
const TELEMETRY_SCALARS = {
|
|
BLOCK: `${TELEMETRY_PREFIX}.block_weather`,
|
|
CLICK: `${TELEMETRY_PREFIX}.click_weather`,
|
|
EXPOSURE: `${TELEMETRY_PREFIX}.exposure_weather`,
|
|
HELP: `${TELEMETRY_PREFIX}.help_weather`,
|
|
IMPRESSION: `${TELEMETRY_PREFIX}.impression_weather`,
|
|
};
|
|
|
|
const WEATHER_DYNAMIC_TYPE = "weather";
|
|
const WEATHER_VIEW_TEMPLATE = {
|
|
attributes: {
|
|
selectable: true,
|
|
},
|
|
children: [
|
|
{
|
|
name: "currentConditions",
|
|
tag: "span",
|
|
children: [
|
|
{
|
|
name: "currently",
|
|
tag: "div",
|
|
},
|
|
{
|
|
name: "currentTemperature",
|
|
tag: "div",
|
|
children: [
|
|
{
|
|
name: "temperature",
|
|
tag: "span",
|
|
},
|
|
{
|
|
name: "weatherIcon",
|
|
tag: "img",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "summary",
|
|
tag: "span",
|
|
overflowable: true,
|
|
children: [
|
|
{
|
|
name: "top",
|
|
tag: "div",
|
|
children: [
|
|
{
|
|
name: "topNoWrap",
|
|
tag: "span",
|
|
children: [
|
|
{ name: "title", tag: "span", classList: ["urlbarView-title"] },
|
|
{
|
|
name: "titleSeparator",
|
|
tag: "span",
|
|
classList: ["urlbarView-title-separator"],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "url",
|
|
tag: "span",
|
|
classList: ["urlbarView-url"],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "middle",
|
|
tag: "div",
|
|
children: [
|
|
{
|
|
name: "middleNoWrap",
|
|
tag: "span",
|
|
overflowable: true,
|
|
children: [
|
|
{
|
|
name: "summaryText",
|
|
tag: "span",
|
|
},
|
|
{
|
|
name: "summaryTextSeparator",
|
|
tag: "span",
|
|
},
|
|
{
|
|
name: "highLow",
|
|
tag: "span",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "highLowWrap",
|
|
tag: "span",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "bottom",
|
|
tag: "div",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
/**
|
|
* A provider that returns a suggested url to the user based on what
|
|
* they have currently typed so they can navigate directly.
|
|
*/
|
|
class ProviderWeather extends UrlbarProvider {
|
|
constructor(...args) {
|
|
super(...args);
|
|
lazy.UrlbarResult.addDynamicResultType(WEATHER_DYNAMIC_TYPE);
|
|
lazy.UrlbarView.addDynamicViewTemplate(
|
|
WEATHER_DYNAMIC_TYPE,
|
|
WEATHER_VIEW_TEMPLATE
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the name of this provider.
|
|
*
|
|
* @returns {string} the name of this provider.
|
|
*/
|
|
get name() {
|
|
return "Weather";
|
|
}
|
|
|
|
/**
|
|
* The type of the provider.
|
|
*
|
|
* @returns {UrlbarUtils.PROVIDER_TYPE}
|
|
*/
|
|
get type() {
|
|
return UrlbarUtils.PROVIDER_TYPE.NETWORK;
|
|
}
|
|
|
|
/**
|
|
* @returns {object} An object mapping from mnemonics to scalar names.
|
|
*/
|
|
get TELEMETRY_SCALARS() {
|
|
return { ...TELEMETRY_SCALARS };
|
|
}
|
|
|
|
getPriority(context) {
|
|
if (!context.searchString) {
|
|
// Zero-prefix suggestions have the same priority as top sites.
|
|
return lazy.UrlbarProviderTopSites.PRIORITY;
|
|
}
|
|
return super.getPriority(context);
|
|
}
|
|
|
|
/**
|
|
* Whether this provider should be invoked for the given context.
|
|
* If this method returns false, the providers manager won't start a query
|
|
* with this provider, to save on resources.
|
|
*
|
|
* @param {UrlbarQueryContext} queryContext The query context object
|
|
* @returns {boolean} Whether this provider should be invoked for the search.
|
|
*/
|
|
isActive(queryContext) {
|
|
this.#resultFromLastQuery = null;
|
|
|
|
// If the sources don't include search or the user used a restriction
|
|
// character other than search, don't allow any suggestions.
|
|
if (
|
|
!queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.SEARCH) ||
|
|
(queryContext.restrictSource &&
|
|
queryContext.restrictSource != UrlbarUtils.RESULT_SOURCE.SEARCH)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
queryContext.isPrivate ||
|
|
queryContext.searchMode ||
|
|
// `QuickSuggest.weather` will be undefined if `QuickSuggest` hasn't been
|
|
// initialized.
|
|
!lazy.QuickSuggest.weather?.suggestion
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
let { keywords } = lazy.QuickSuggest.weather;
|
|
if (!keywords) {
|
|
// Show the suggestion only on zero prefix (empty search string).
|
|
return !queryContext.searchString;
|
|
}
|
|
|
|
return keywords.has(queryContext.searchString.trim());
|
|
}
|
|
|
|
/**
|
|
* Starts querying. Extended classes should return a Promise resolved when the
|
|
* provider is done searching AND returning results.
|
|
*
|
|
* @param {UrlbarQueryContext} queryContext The query context object
|
|
* @param {Function} addCallback Callback invoked by the provider to add a new
|
|
* result. A UrlbarResult should be passed to it.
|
|
* @returns {Promise}
|
|
*/
|
|
async startQuery(queryContext, addCallback) {
|
|
let { suggestion } = lazy.QuickSuggest.weather;
|
|
if (!suggestion) {
|
|
return;
|
|
}
|
|
|
|
let unit = Services.locale.regionalPrefsLocales[0] == "en-US" ? "f" : "c";
|
|
let result = new lazy.UrlbarResult(
|
|
UrlbarUtils.RESULT_TYPE.DYNAMIC,
|
|
UrlbarUtils.RESULT_SOURCE.SEARCH,
|
|
{
|
|
url: suggestion.url,
|
|
iconId: suggestion.current_conditions.icon_id,
|
|
helpUrl: lazy.QuickSuggest.HELP_URL,
|
|
helpL10n: {
|
|
id: lazy.UrlbarPrefs.get("resultMenu")
|
|
? "urlbar-result-menu-learn-more-about-firefox-suggest"
|
|
: "firefox-suggest-urlbar-learn-more",
|
|
},
|
|
isBlockable: true,
|
|
blockL10n: {
|
|
id: lazy.UrlbarPrefs.get("resultMenu")
|
|
? "urlbar-result-menu-dismiss-firefox-suggest"
|
|
: "firefox-suggest-urlbar-block",
|
|
},
|
|
requestId: suggestion.request_id,
|
|
source: suggestion.source,
|
|
merinoProvider: suggestion.provider,
|
|
dynamicType: WEATHER_DYNAMIC_TYPE,
|
|
city: suggestion.city_name,
|
|
temperatureUnit: unit,
|
|
temperature: suggestion.current_conditions.temperature[unit],
|
|
currentConditions: suggestion.current_conditions.summary,
|
|
forecast: suggestion.forecast.summary,
|
|
high: suggestion.forecast.high[unit],
|
|
low: suggestion.forecast.low[unit],
|
|
shouldNavigate: true,
|
|
}
|
|
);
|
|
|
|
result.suggestedIndex = queryContext.searchString ? 1 : 0;
|
|
|
|
addCallback(this, result);
|
|
this.#resultFromLastQuery = result;
|
|
}
|
|
|
|
/**
|
|
* This is called only for dynamic result types, when the urlbar view updates
|
|
* the view of one of the results of the provider. It should return an object
|
|
* describing the view update.
|
|
*
|
|
* @param {UrlbarResult} result
|
|
* The result whose view will be updated.
|
|
* @param {Map} idsByName
|
|
* A Map from an element's name, as defined by the provider; to its ID in
|
|
* the DOM, as defined by the browser.This is useful if parts of the view
|
|
* update depend on element IDs, as some ARIA attributes do.
|
|
* @returns {object} An object describing the view update.
|
|
*/
|
|
getViewUpdate(result, idsByName) {
|
|
let uppercaseUnit = result.payload.temperatureUnit.toUpperCase();
|
|
|
|
return {
|
|
currently: {
|
|
l10n: {
|
|
id: "firefox-suggest-weather-currently",
|
|
cacheable: true,
|
|
},
|
|
},
|
|
temperature: {
|
|
l10n: {
|
|
id: "firefox-suggest-weather-temperature",
|
|
args: {
|
|
value: result.payload.temperature,
|
|
unit: uppercaseUnit,
|
|
},
|
|
cacheable: true,
|
|
excludeArgsFromCacheKey: true,
|
|
},
|
|
},
|
|
weatherIcon: {
|
|
attributes: { iconId: result.payload.iconId },
|
|
},
|
|
title: {
|
|
l10n: {
|
|
id: "firefox-suggest-weather-title",
|
|
args: { city: result.payload.city },
|
|
cacheable: true,
|
|
excludeArgsFromCacheKey: true,
|
|
},
|
|
},
|
|
url: {
|
|
textContent: result.payload.url,
|
|
},
|
|
summaryText: {
|
|
l10n: {
|
|
id: "firefox-suggest-weather-summary-text",
|
|
args: {
|
|
currentConditions: result.payload.currentConditions,
|
|
forecast: result.payload.forecast,
|
|
},
|
|
cacheable: true,
|
|
excludeArgsFromCacheKey: true,
|
|
},
|
|
},
|
|
highLow: {
|
|
l10n: {
|
|
id: "firefox-suggest-weather-high-low",
|
|
args: {
|
|
high: result.payload.high,
|
|
low: result.payload.low,
|
|
unit: uppercaseUnit,
|
|
},
|
|
cacheable: true,
|
|
excludeArgsFromCacheKey: true,
|
|
},
|
|
},
|
|
highLowWrap: {
|
|
l10n: {
|
|
id: "firefox-suggest-weather-high-low",
|
|
args: {
|
|
high: result.payload.high,
|
|
low: result.payload.low,
|
|
unit: uppercaseUnit,
|
|
},
|
|
},
|
|
},
|
|
bottom: {
|
|
l10n: {
|
|
id: "firefox-suggest-weather-sponsored",
|
|
args: { provider: WEATHER_PROVIDER_DISPLAY_NAME },
|
|
cacheable: true,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Called when the result's block button is picked. If the provider can block
|
|
* the result, it should do so and return true. If the provider cannot block
|
|
* the result, it should return false. The meaning of "blocked" depends on the
|
|
* provider and the type of result.
|
|
*
|
|
* @param {UrlbarQueryContext} queryContext
|
|
* The query context.
|
|
* @param {UrlbarResult} result
|
|
* The result that should be blocked.
|
|
* @returns {boolean}
|
|
* Whether the result was blocked.
|
|
*/
|
|
blockResult(queryContext, result) {
|
|
this.logger.info("Blocking weather result");
|
|
lazy.UrlbarPrefs.set("suggest.weather", false);
|
|
this.#recordEngagementTelemetry(result, queryContext.isPrivate, "block");
|
|
return true;
|
|
}
|
|
|
|
onResultsShown(queryContext, results) {
|
|
Services.telemetry.keyedScalarAdd(
|
|
TELEMETRY_SCALARS.EXPOSURE,
|
|
// Telemetry indexes are 1-based.
|
|
results[0].rowIndex + 1,
|
|
1
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Called when the user starts and ends an engagement with the urlbar. For
|
|
* details on parameters, see UrlbarProvider.onEngagement().
|
|
*
|
|
* @param {boolean} isPrivate
|
|
* True if the engagement is in a private context.
|
|
* @param {string} state
|
|
* The state of the engagement, one of: start, engagement, abandonment,
|
|
* discard
|
|
* @param {UrlbarQueryContext} queryContext
|
|
* The engagement's query context. This is *not* guaranteed to be defined
|
|
* when `state` is "start". It will always be defined for "engagement" and
|
|
* "abandonment".
|
|
* @param {object} details
|
|
* This is defined only when `state` is "engagement" or "abandonment", and
|
|
* it describes the search string and picked result.
|
|
*/
|
|
onEngagement(isPrivate, state, queryContext, details) {
|
|
let result = this.#resultFromLastQuery;
|
|
this.#resultFromLastQuery = null;
|
|
|
|
// Impression and clicked telemetry are both recorded on engagement. We
|
|
// define "impression" to mean a weather result was present in the view when
|
|
// any result was picked.
|
|
if (state == "engagement" && queryContext) {
|
|
// Find the weather result that's currently visible in the view. It's
|
|
// probably the result from the last query so check it first, but due to
|
|
// the async nature of how results are added to the view and made visible,
|
|
// it may not be.
|
|
if (
|
|
result &&
|
|
(result.rowIndex < 0 ||
|
|
queryContext.view?.visibleResults?.[result.rowIndex] != result)
|
|
) {
|
|
// The result from the last query isn't visible.
|
|
result = null;
|
|
}
|
|
|
|
// If the result isn't visible, find a visible one.
|
|
if (!result) {
|
|
result = queryContext.view?.visibleResults?.find(
|
|
r => r.providerName == this.name
|
|
);
|
|
}
|
|
|
|
// Finally, record telemetry if there's a visible result.
|
|
if (result) {
|
|
this.#recordEngagementTelemetry(
|
|
result,
|
|
isPrivate,
|
|
details.selIndex == result.rowIndex ? details.selType : ""
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Records engagement telemetry. This should be called only at the end of an
|
|
* engagement when a weather result is present or when a weather result is
|
|
* blocked.
|
|
*
|
|
* @param {UrlbarResult} result
|
|
* The weather result that was present (and possibly picked) at the end of
|
|
* the engagement or that was blocked.
|
|
* @param {boolean} isPrivate
|
|
* Whether the engagement is in a private context.
|
|
* @param {string} selType
|
|
* This parameter indicates the part of the row the user picked, if any, and
|
|
* should be one of the following values:
|
|
*
|
|
* - "": The user didn't pick the row or any part of it
|
|
* - "weather": The user picked the main part of the row
|
|
* - "help": The user picked the help button
|
|
* - "block": The user picked the block button or used the key shortcut
|
|
*
|
|
* An empty string means the user picked some other row to end the
|
|
* engagement, not the weather row. In that case only impression telemetry
|
|
* will be recorded.
|
|
*
|
|
* A non-empty string means the user picked the weather row or some part of
|
|
* it, and both impression and click telemetry will be recorded. The
|
|
* non-empty-string values come from the `details.selType` passed in to
|
|
* `onEngagement()`; see `TelemetryEvent.typeFromElement()`.
|
|
*/
|
|
#recordEngagementTelemetry(result, isPrivate, selType) {
|
|
// Indexes recorded in quick suggest telemetry are 1-based, so add 1 to the
|
|
// 0-based `result.rowIndex`.
|
|
let telemetryResultIndex = result.rowIndex + 1;
|
|
|
|
// impression scalars
|
|
Services.telemetry.keyedScalarAdd(
|
|
TELEMETRY_SCALARS.IMPRESSION,
|
|
telemetryResultIndex,
|
|
1
|
|
);
|
|
|
|
// scalars related to clicking the result and other elements in its row
|
|
let clickScalars = [];
|
|
switch (selType) {
|
|
case "weather":
|
|
clickScalars.push(TELEMETRY_SCALARS.CLICK);
|
|
break;
|
|
case "help":
|
|
clickScalars.push(TELEMETRY_SCALARS.HELP);
|
|
break;
|
|
case "block":
|
|
clickScalars.push(TELEMETRY_SCALARS.BLOCK);
|
|
break;
|
|
default:
|
|
if (selType) {
|
|
this.logger.error(
|
|
"Engagement telemetry error, unknown selType: " + selType
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
for (let scalar of clickScalars) {
|
|
Services.telemetry.keyedScalarAdd(scalar, telemetryResultIndex, 1);
|
|
}
|
|
|
|
// engagement event
|
|
Services.telemetry.recordEvent(
|
|
lazy.QuickSuggest.TELEMETRY_EVENT_CATEGORY,
|
|
"engagement",
|
|
selType == "weather" ? "click" : selType || "impression_only",
|
|
"",
|
|
{
|
|
match_type: "firefox-suggest",
|
|
position: String(telemetryResultIndex),
|
|
suggestion_type: "weather",
|
|
source: result.payload.source,
|
|
}
|
|
);
|
|
}
|
|
|
|
// The result we added during the most recent query.
|
|
#resultFromLastQuery = null;
|
|
}
|
|
|
|
export var UrlbarProviderWeather = new ProviderWeather();
|