ImportantDatesSuggestions.sys.mjs (7895B)
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 import { SuggestProvider } from "moz-src:///browser/components/urlbar/private/SuggestFeature.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs", 11 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 12 UrlbarSearchUtils: 13 "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs", 14 UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 15 }); 16 17 const SHOW_COUNTDOWN_THRESHOLD_DAYS = 30; 18 const MS_PER_DAY = 1000 * 60 * 60 * 24; 19 20 /** 21 * A Suggest feature that manages important-dates suggestions. 22 */ 23 export class ImportantDatesSuggestions extends SuggestProvider { 24 get enablingPreferences() { 25 return ["importantDatesFeatureGate", "suggest.importantDates"]; 26 } 27 28 get primaryUserControlledPreferences() { 29 return ["suggest.importantDates"]; 30 } 31 32 get rustSuggestionType() { 33 return "Dynamic"; 34 } 35 36 get dynamicRustSuggestionTypes() { 37 return ["important_dates"]; 38 } 39 40 isSuggestionSponsored(suggestion) { 41 if (suggestion.data?.result?.payload?.hasOwnProperty("isSponsored")) { 42 return suggestion.data.result.payload.isSponsored; 43 } 44 return false; 45 } 46 47 getSuggestionTelemetryType(suggestion) { 48 if (suggestion.data?.result?.payload?.hasOwnProperty("telemetryType")) { 49 return suggestion.data.result.payload.telemetryType; 50 } 51 return this.dynamicRustSuggestionTypes[0]; 52 } 53 54 async makeResult(queryContext, suggestion, _searchString) { 55 if ( 56 !suggestion.data?.result?.payload || 57 typeof suggestion.data.result.payload != "object" 58 ) { 59 this.logger.warn("Unexpected remote settings suggestion"); 60 return null; 61 } 62 63 return this.#makeDateResult(queryContext, suggestion.data.result.payload); 64 } 65 66 /** 67 * @typedef DatePayload 68 * The shape of an important dates suggestion payload. 69 * @property {(string | [string, string])[]} dates 70 * An array of dates or date tuples in the format "YYYY-MM-DD". 71 * For a date tuple, the first value represents the start and the second 72 * the end date. This is assumed to be ordered oldest to newest. 73 * @property {string} name 74 * The name of the event. 75 */ 76 77 /** 78 * Returns a string about the date of an event that can be displayed. 79 * 80 * @param {string | [string, string]} dateStr 81 * A string in the format "YYYY-MM-DD" representing a single 82 * day or a tuple of strings representing start and end days. 83 * @returns {string} 84 * A localized string about the date of the event. 85 */ 86 #formatDateOrRange(dateStr) { 87 if (Array.isArray(dateStr)) { 88 let format = new Intl.DateTimeFormat(Services.locale.appLocaleAsBCP47, { 89 year: "numeric", 90 month: "long", 91 day: "numeric", 92 }); 93 let startDate = new Date(dateStr[0] + "T00:00"); 94 let endDate = new Date(dateStr[1] + "T00:00"); 95 return format.formatRange(startDate, endDate); 96 } 97 98 let format = new Intl.DateTimeFormat(Services.locale.appLocaleAsBCP47, { 99 weekday: "long", 100 year: "numeric", 101 month: "long", 102 day: "numeric", 103 }); 104 let date = new Date(dateStr + "T00:00"); 105 return format.format(date); 106 } 107 108 /** 109 * Returns a l10n object about the name of and time until an event 110 * that can be used as the description of a urlbar result. 111 * 112 * @param {string | [string, string]} dateStr 113 * A string in the format "YYYY-MM-DD" representing a single 114 * day or a tuple of strings representing start and end days. 115 * @param {string} name 116 * The name of the event. 117 * @returns {?object} 118 * A l10n object or null if the event is in the past. 119 */ 120 #formatDateCountdown(dateStr, name) { 121 if (Array.isArray(dateStr)) { 122 let daysUntilStart = this.#getDaysUntil(dateStr[0]); 123 let daysUntilEnd = this.#getDaysUntil(dateStr[1]); 124 if (daysUntilEnd < 0) { 125 throw new Error("Date lies in the past."); 126 } 127 if (daysUntilStart > 0) { 128 return { 129 id: "urlbar-result-dates-countdown-range", 130 args: { daysUntilStart, name }, 131 }; 132 } 133 if (daysUntilEnd > 0) { 134 return { 135 id: "urlbar-result-dates-ongoing", 136 args: { daysUntilEnd, name }, 137 }; 138 } 139 return { 140 id: "urlbar-result-dates-ends-today", 141 args: { name }, 142 }; 143 } 144 145 let daysUntil = this.#getDaysUntil(dateStr); 146 if (daysUntil < 0) { 147 throw new Error("Date lies in the past."); 148 } 149 if (daysUntil > 0) { 150 return { 151 id: "urlbar-result-dates-countdown", 152 args: { daysUntilStart: daysUntil, name }, 153 }; 154 } 155 return { 156 id: "urlbar-result-dates-today", 157 args: { name }, 158 }; 159 } 160 161 /** 162 * Returns the number of days until the specified date. 163 * 164 * @param {string} dateStr 165 * A string in the format "YYYY-MM-DD". 166 * @returns {number} 167 * The time until the input date in days. 168 */ 169 #getDaysUntil(dateStr) { 170 let now = new Date(); 171 now.setHours(0, 0, 0, 0); 172 let date = new Date(dateStr + "T00:00"); 173 174 let msUntil = date.getTime() - now.getTime(); 175 // Round to account for potential DST. 176 return Math.round(msUntil / MS_PER_DAY); 177 } 178 179 /** 180 * Creates a urlbar result from an important dates payload. 181 * 182 * @param {UrlbarQueryContext} queryContext 183 * The query context. 184 * @param {DatePayload} payload 185 * The important dates payload. 186 * @returns {?UrlbarResult} 187 * A urlbar result or null if all instances are in the past. 188 */ 189 #makeDateResult(queryContext, payload) { 190 let eventDateOrRange = payload.dates.find( 191 // Find first entry where the end date is in the future. 192 // This is always the upcoming occurrence since dates is ordered by time. 193 d => this.#getDaysUntil(Array.isArray(d) ? d[1] : d) >= 0 194 ); 195 if (!eventDateOrRange) { 196 // All instances of the event are in the past. 197 return null; 198 } 199 200 let daysUntilStart = this.#getDaysUntil( 201 Array.isArray(eventDateOrRange) ? eventDateOrRange[0] : eventDateOrRange 202 ); 203 204 let description, descriptionL10n; 205 if (daysUntilStart > SHOW_COUNTDOWN_THRESHOLD_DAYS) { 206 description = payload.name; 207 } else { 208 descriptionL10n = { 209 ...this.#formatDateCountdown(eventDateOrRange, payload.name), 210 }; 211 } 212 213 let dateString = this.#formatDateOrRange(eventDateOrRange); 214 return new lazy.UrlbarResult({ 215 type: lazy.UrlbarUtils.RESULT_TYPE.SEARCH, 216 source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH, 217 isBestMatch: true, 218 hideRowLabel: true, 219 richSuggestionIconSize: 24, 220 payload: { 221 title: dateString, 222 description, 223 engine: lazy.UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate) 224 .name, 225 descriptionL10n, 226 query: payload.name, 227 lowerCaseSuggestion: payload.name.toLowerCase(), 228 icon: "chrome://browser/skin/calendar-24.svg", 229 helpUrl: lazy.QuickSuggest.HELP_URL, 230 isManageable: true, 231 isBlockable: true, 232 }, 233 highlights: { 234 title: lazy.UrlbarUtils.HIGHLIGHT.ALL, 235 }, 236 }); 237 } 238 239 onEngagement(_queryContext, controller, details, _searchString) { 240 switch (details.selType) { 241 case "manage": 242 // "manage" is handled by UrlbarInput, no need to do anything here. 243 break; 244 case "dismiss": { 245 let { result } = details; 246 lazy.QuickSuggest.dismissResult(result); 247 result.acknowledgeDismissalL10n = { 248 id: "firefox-suggest-dismissal-acknowledgment-one", 249 }; 250 controller.removeResult(result); 251 break; 252 } 253 } 254 } 255 }