UrlbarProviderQuickSuggestContextualOptIn.sys.mjs (9373B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /** 6 * This module exports a provider that offers a search engine when the user is 7 * typing a search engine domain. 8 */ 9 10 import { 11 UrlbarProvider, 12 UrlbarUtils, 13 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"; 14 15 const lazy = {}; 16 17 ChromeUtils.defineESModuleGetters(lazy, { 18 UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs", 19 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 20 UrlbarProviderTopSites: 21 "moz-src:///browser/components/urlbar/UrlbarProviderTopSites.sys.mjs", 22 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 23 }); 24 25 const DYNAMIC_RESULT_TYPE = "quickSuggestContextualOptIn"; 26 const VIEW_TEMPLATE = { 27 children: [ 28 { 29 name: "no-wrap", 30 tag: "span", 31 classList: ["urlbarView-no-wrap"], 32 children: [ 33 { 34 name: "icon", 35 tag: "img", 36 classList: ["urlbarView-favicon"], 37 }, 38 { 39 name: "text-container", 40 tag: "span", 41 children: [ 42 { 43 name: "title", 44 tag: "strong", 45 }, 46 { 47 name: "description", 48 tag: "span", 49 children: [ 50 { 51 name: "learn_more", 52 tag: "a", 53 attributes: { 54 "data-l10n-name": "learn-more-link", 55 selectable: true, 56 }, 57 }, 58 ], 59 }, 60 ], 61 }, 62 ], 63 }, 64 ], 65 }; 66 67 /** 68 * Initializes this provider's dynamic result. To be called after the creation 69 * of the provider singleton. 70 */ 71 function initializeDynamicResult() { 72 lazy.UrlbarResult.addDynamicResultType(DYNAMIC_RESULT_TYPE); 73 lazy.UrlbarView.addDynamicViewTemplate(DYNAMIC_RESULT_TYPE, VIEW_TEMPLATE); 74 } 75 76 /** 77 * Class used to create the provider. 78 */ 79 export class UrlbarProviderQuickSuggestContextualOptIn extends UrlbarProvider { 80 constructor() { 81 super(); 82 } 83 84 /** 85 * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>} 86 */ 87 get type() { 88 return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; 89 } 90 91 #shouldDisplayContextualOptIn(queryContext = null) { 92 if ( 93 queryContext && 94 (queryContext.isPrivate || 95 queryContext.restrictSource || 96 queryContext.searchString || 97 queryContext.searchMode) 98 ) { 99 return false; 100 } 101 102 // If the feature is disabled, or the user has already opted in, don't show 103 // the onboarding. 104 if ( 105 !lazy.UrlbarPrefs.get("quickSuggestEnabled") || 106 !lazy.UrlbarPrefs.get("quicksuggest.contextualOptIn") || 107 lazy.UrlbarPrefs.get("quicksuggest.online.enabled") 108 ) { 109 return false; 110 } 111 112 let lastDismissedTime = lazy.UrlbarPrefs.get( 113 "quicksuggest.contextualOptIn.lastDismissedTime" 114 ); 115 if (!lastDismissedTime) { 116 return true; 117 } 118 119 let dismissedCount = lazy.UrlbarPrefs.get( 120 "quicksuggest.contextualOptIn.dismissedCount" 121 ); 122 123 let reshowAfterPeriodDays; 124 switch (dismissedCount) { 125 case 1: { 126 reshowAfterPeriodDays = lazy.UrlbarPrefs.get( 127 "quicksuggest.contextualOptIn.firstReshowAfterPeriodDays" 128 ); 129 break; 130 } 131 case 2: { 132 reshowAfterPeriodDays = lazy.UrlbarPrefs.get( 133 "quicksuggest.contextualOptIn.secondReshowAfterPeriodDays" 134 ); 135 break; 136 } 137 case 3: { 138 reshowAfterPeriodDays = lazy.UrlbarPrefs.get( 139 "quicksuggest.contextualOptIn.thirdReshowAfterPeriodDays" 140 ); 141 break; 142 } 143 default: { 144 return false; 145 } 146 } 147 148 let time = reshowAfterPeriodDays * 24 * 60 * 60; 149 return Date.now() / 1000 - lastDismissedTime > time; 150 } 151 152 async isActive(queryContext) { 153 if (!this.#shouldDisplayContextualOptIn(queryContext)) { 154 return false; 155 } 156 157 // Evaluate impressions in order to dismiss. 158 let firstImpressionTime = lazy.UrlbarPrefs.get( 159 "quicksuggest.contextualOptIn.firstImpressionTime" 160 ); 161 if (!firstImpressionTime) { 162 return true; 163 } 164 165 let impressionCount = lazy.UrlbarPrefs.get( 166 "quicksuggest.contextualOptIn.impressionCount" 167 ); 168 let impressionLimit = lazy.UrlbarPrefs.get( 169 "quicksuggest.contextualOptIn.impressionLimit" 170 ); 171 172 if (impressionCount < impressionLimit) { 173 return true; 174 } 175 176 let daysLimit = lazy.UrlbarPrefs.get( 177 "quicksuggest.contextualOptIn.impressionDaysLimit" 178 ); 179 let timeLimit = daysLimit * 24 * 60 * 60; 180 if (Date.now() / 1000 - firstImpressionTime < timeLimit) { 181 return true; 182 } 183 184 this.#dismiss(); 185 186 return false; 187 } 188 189 getPriority() { 190 return lazy.UrlbarProviderTopSites.PRIORITY; 191 } 192 193 /** 194 * This is called only for dynamic result types, when the urlbar view updates 195 * the view of one of the results of the provider. It should return an object 196 * describing the view update. 197 * 198 * @returns {object} An object describing the view update. 199 */ 200 getViewUpdate() { 201 return { 202 icon: { 203 attributes: { 204 src: "chrome://branding/content/icon32.png", 205 }, 206 }, 207 title: { 208 l10n: { 209 id: "urlbar-firefox-suggest-contextual-opt-in-title-1", 210 }, 211 }, 212 description: { 213 l10n: { 214 id: "urlbar-firefox-suggest-contextual-opt-in-description-3", 215 }, 216 }, 217 }; 218 } 219 220 onBeforeSelection(result, element) { 221 if (element.getAttribute("name") == "learn_more") { 222 this.#a11yAlertRow(element.closest(".urlbarView-row")); 223 } 224 } 225 226 #a11yAlertRow(row) { 227 let alertText = row.querySelector( 228 ".urlbarView-dynamic-quickSuggestContextualOptIn-title" 229 ).textContent; 230 let decription = row 231 .querySelector( 232 ".urlbarView-dynamic-quickSuggestContextualOptIn-description" 233 ) 234 .cloneNode(true); 235 // Remove the "Learn More" link. 236 decription.firstElementChild?.remove(); 237 alertText += ". " + decription.textContent; 238 row.ownerGlobal.A11yUtils.announce({ raw: alertText }); 239 } 240 241 onImpression(state, _queryContext, _controller, _resultsAndIndexes, details) { 242 if (state == "engagement" && details.provider == this.name) { 243 return; 244 } 245 246 let impressionCount = lazy.UrlbarPrefs.get( 247 "quicksuggest.contextualOptIn.impressionCount" 248 ); 249 lazy.UrlbarPrefs.set( 250 "quicksuggest.contextualOptIn.impressionCount", 251 impressionCount + 1 252 ); 253 254 let firstImpressionTime = lazy.UrlbarPrefs.get( 255 "quicksuggest.contextualOptIn.firstImpressionTime" 256 ); 257 if (!firstImpressionTime) { 258 lazy.UrlbarPrefs.set( 259 "quicksuggest.contextualOptIn.firstImpressionTime", 260 Date.now() / 1000 261 ); 262 } 263 } 264 265 onEngagement(queryContext, controller, details) { 266 this._handleCommand(details.element, controller, details.result); 267 } 268 269 _handleCommand(element, controller, result, container) { 270 let commandName = element?.getAttribute("name"); 271 switch (commandName) { 272 case "learn_more": 273 controller.browserWindow.openHelpLink("firefox-suggest"); 274 break; 275 case "allow": 276 lazy.UrlbarPrefs.set("quicksuggest.online.enabled", true); 277 break; 278 case "dismiss": 279 this.#dismiss(); 280 break; 281 default: 282 return; 283 } 284 285 // Remove the result if it shouldn't be active anymore due to above 286 // actions. 287 if (!this.#shouldDisplayContextualOptIn()) { 288 if (result) { 289 controller.removeResult(result); 290 } else { 291 // This is for when the UI is outside of standard results, after 292 // one-off search buttons. 293 container.hidden = true; 294 } 295 } 296 } 297 298 #dismiss() { 299 lazy.UrlbarPrefs.set("quicksuggest.contextualOptIn.firstImpressionTime", 0); 300 lazy.UrlbarPrefs.set("quicksuggest.contextualOptIn.impressionCount", 0); 301 302 lazy.UrlbarPrefs.set( 303 "quicksuggest.contextualOptIn.lastDismissedTime", 304 Date.now() / 1000 305 ); 306 let dismissedCount = lazy.UrlbarPrefs.get( 307 "quicksuggest.contextualOptIn.dismissedCount" 308 ); 309 lazy.UrlbarPrefs.set( 310 "quicksuggest.contextualOptIn.dismissedCount", 311 dismissedCount + 1 312 ); 313 } 314 315 /** 316 * Starts querying. 317 * 318 * @param {UrlbarQueryContext} queryContext 319 * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback 320 * Callback invoked by the provider to add a new result. 321 */ 322 async startQuery(queryContext, addCallback) { 323 let result = new lazy.UrlbarResult({ 324 type: UrlbarUtils.RESULT_TYPE.DYNAMIC, 325 source: UrlbarUtils.RESULT_SOURCE.SEARCH, 326 suggestedIndex: 0, 327 payload: { 328 buttons: [ 329 { 330 l10n: { 331 id: "urlbar-firefox-suggest-contextual-opt-in-allow", 332 }, 333 attributes: { primary: true, name: "allow" }, 334 }, 335 { 336 l10n: { 337 id: "urlbar-firefox-suggest-contextual-opt-in-dismiss", 338 }, 339 attributes: { name: "dismiss" }, 340 }, 341 ], 342 dynamicType: DYNAMIC_RESULT_TYPE, 343 }, 344 }); 345 addCallback(this, result); 346 } 347 } 348 349 initializeDynamicResult();